-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
611 additions
and
3 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "turbo-updater" | ||
version = "0.1.0" | ||
edition = "2021" | ||
description = "Minimal wrapper around update-informer to provide npm registry support and consistent UI" | ||
license = "MPL-2.0" | ||
publish = false | ||
|
||
[dependencies] | ||
colored = "2.0" | ||
serde = { version = "1.0.126", features = ["derive"] } | ||
strip-ansi-escapes = "0.1.1" | ||
terminal_size = "0.2" | ||
thiserror = "1.0" | ||
update-informer = "0.5.0" | ||
ureq = { version = "2.3.0", features = ["json"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
use std::time::Duration; | ||
|
||
use colored::*; | ||
use serde::Deserialize; | ||
use thiserror::Error as ThisError; | ||
use update_informer::{Check, Package, Registry, Result as UpdateResult}; | ||
|
||
mod ui; | ||
|
||
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(800); | ||
const DEFAULT_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); | ||
|
||
#[derive(ThisError, Debug)] | ||
pub enum UpdateNotifierError { | ||
#[error("Failed to write to terminal")] | ||
RenderError(#[from] ui::utils::GetDisplayLengthError), | ||
} | ||
|
||
#[derive(Deserialize)] | ||
struct NpmVersionData { | ||
version: String, | ||
} | ||
|
||
struct NPMRegistry; | ||
|
||
impl Registry for NPMRegistry { | ||
const NAME: &'static str = "npm_registry"; | ||
fn get_latest_version(pkg: &Package, _timeout: Duration) -> UpdateResult<Option<String>> { | ||
let url = format!( | ||
"https://turbo.build/api/binaries/latest?package={pkg}", | ||
pkg = pkg | ||
); | ||
let resp = ureq::get(&url).timeout(_timeout).call()?; | ||
let result = resp.into_json::<NpmVersionData>().unwrap(); | ||
Ok(Some(result.version)) | ||
} | ||
} | ||
|
||
pub fn check_for_updates( | ||
package_name: &str, | ||
github_repo: &str, | ||
footer: Option<&str>, | ||
current_version: &str, | ||
timeout: Option<Duration>, | ||
interval: Option<Duration>, | ||
) -> Result<(), UpdateNotifierError> { | ||
let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); | ||
let interval = interval.unwrap_or(DEFAULT_INTERVAL); | ||
let informer = update_informer::new(NPMRegistry, package_name, current_version) | ||
.timeout(timeout) | ||
.interval(interval); | ||
if let Ok(Some(version)) = informer.check_version() { | ||
let latest_version = version.to_string(); | ||
let msg = format!( | ||
" | ||
Update available {version_prefix}{current_version} ≫ {latest_version} | ||
Changelog: {github_repo}/releases/tag/{latest_version} | ||
Run \"{update_cmd}\" to update | ||
", | ||
version_prefix = "v".dimmed(), | ||
current_version = current_version.dimmed(), | ||
latest_version = latest_version.green().bold(), | ||
github_repo = github_repo, | ||
// TODO: make this package manager aware | ||
update_cmd = "npm i -g turbo".cyan().bold(), | ||
); | ||
|
||
if let Some(footer) = footer { | ||
return ui::message(&format!("{}\n{}", msg, footer)); | ||
} | ||
|
||
return ui::message(&msg); | ||
} | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
use terminal_size::{terminal_size, Width}; | ||
|
||
use crate::UpdateNotifierError; | ||
pub mod utils; | ||
|
||
const DEFAULT_PADDING: usize = 8; | ||
|
||
pub fn message(text: &str) -> Result<(), UpdateNotifierError> { | ||
let size = terminal_size(); | ||
let lines: Vec<&str> = text.split('\n').map(|line| line.trim()).collect(); | ||
|
||
// get the display width of each line so we can center it within the box later | ||
let lines_display_width = lines | ||
.iter() | ||
.map(|line| utils::get_display_length(line)) | ||
.collect::<Result<Vec<_>, _>>()?; | ||
|
||
// find the longest line to determine layout | ||
let longest_line = lines_display_width | ||
.iter() | ||
.max() | ||
.copied() | ||
.unwrap_or_default(); | ||
let full_message_width = longest_line + DEFAULT_PADDING; | ||
|
||
// create a curried render function to reduce verbosity when calling | ||
let render_at_layout = |layout: utils::Layout, width: usize| { | ||
utils::render_message( | ||
layout, | ||
width, | ||
lines, | ||
lines_display_width, | ||
full_message_width, | ||
) | ||
}; | ||
|
||
// render differently depending on viewport | ||
if let Some((Width(term_width), _)) = size { | ||
// if possible, pad this value slightly | ||
let term_width = if term_width > 2 { | ||
usize::from(term_width) - 2 | ||
} else { | ||
term_width.into() | ||
}; | ||
|
||
let can_fit_box = term_width >= full_message_width; | ||
let can_center_text = term_width >= longest_line; | ||
|
||
if can_fit_box { | ||
render_at_layout(utils::Layout::Large, term_width); | ||
} else if can_center_text { | ||
render_at_layout(utils::Layout::Medium, term_width); | ||
} else { | ||
render_at_layout(utils::Layout::Small, term_width); | ||
} | ||
} else { | ||
render_at_layout(utils::Layout::Unknown, 0); | ||
} | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
use std::{io::Error as IOError, string::FromUtf8Error}; | ||
|
||
use colored::*; | ||
use strip_ansi_escapes::strip as strip_ansi_escapes; | ||
use thiserror::Error as ThisError; | ||
|
||
pub enum BorderAlignment { | ||
Divider, | ||
Top, | ||
Bottom, | ||
} | ||
|
||
pub enum Layout { | ||
Unknown, | ||
Small, | ||
Medium, | ||
Large, | ||
} | ||
|
||
const TOP_LEFT: &str = "╭"; | ||
const TOP_RIGHT: &str = "╮"; | ||
const BOTTOM_LEFT: &str = "╰"; | ||
const BOTTOM_RIGHT: &str = "╯"; | ||
const HORIZONTAL: &str = "─"; | ||
const VERTICAL: &str = "│"; | ||
const SPACE: &str = " "; | ||
|
||
#[derive(ThisError, Debug)] | ||
pub enum GetDisplayLengthError { | ||
#[error("Could not strip ANSI escape codes from string")] | ||
StripError(#[from] IOError), | ||
#[error("Could not convert to string")] | ||
ConvertError(#[from] FromUtf8Error), | ||
} | ||
|
||
pub fn get_display_length(line: &str) -> Result<usize, GetDisplayLengthError> { | ||
// strip any ansi escape codes (for color) | ||
let stripped = strip_ansi_escapes(line)?; | ||
let stripped = String::from_utf8(stripped)?; | ||
// count the chars instead of the bytes (for unicode) | ||
return Ok(stripped.chars().count()); | ||
} | ||
|
||
pub fn x_border(width: usize, position: BorderAlignment) { | ||
match position { | ||
BorderAlignment::Top => { | ||
println!( | ||
"{}{}{}", | ||
TOP_LEFT.yellow(), | ||
HORIZONTAL.repeat(width).yellow(), | ||
TOP_RIGHT.yellow() | ||
); | ||
} | ||
BorderAlignment::Bottom => { | ||
println!( | ||
"{}{}{}", | ||
BOTTOM_LEFT.yellow(), | ||
HORIZONTAL.repeat(width).yellow(), | ||
BOTTOM_RIGHT.yellow() | ||
); | ||
} | ||
BorderAlignment::Divider => { | ||
println!("{}", HORIZONTAL.repeat(width).yellow(),); | ||
} | ||
} | ||
} | ||
|
||
pub fn render_message( | ||
layout: Layout, | ||
width: usize, | ||
lines: Vec<&str>, | ||
lines_display_width: Vec<usize>, | ||
full_message_width: usize, | ||
) { | ||
match layout { | ||
// Left aligned text with no border. | ||
// Used when term width is unknown. | ||
Layout::Unknown => { | ||
for line in lines.iter() { | ||
println!("{}", line); | ||
} | ||
} | ||
|
||
// Left aligned text with top and bottom border. | ||
// Used when text cannot be centered without wrapping | ||
Layout::Small => { | ||
x_border(width, BorderAlignment::Divider); | ||
for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) { | ||
if *line_display_width == 0 { | ||
println!("{}", SPACE.repeat(width)); | ||
} else { | ||
println!("{}", line); | ||
} | ||
} | ||
x_border(width, BorderAlignment::Divider); | ||
} | ||
|
||
// Centered text with top and bottom border. | ||
// Used when text can be centered without wrapping, but | ||
// there isn't enough room to include the box with padding. | ||
Layout::Medium => { | ||
x_border(width, BorderAlignment::Divider); | ||
for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) { | ||
if *line_display_width == 0 { | ||
println!("{}", SPACE.repeat(width)); | ||
} else { | ||
let line_padding = (width - line_display_width) / 2; | ||
// for lines of odd length, tack the reminder to the end | ||
let line_padding_remainder = width - (line_padding * 2) - line_display_width; | ||
println!( | ||
"{}{}{}", | ||
SPACE.repeat(line_padding), | ||
line, | ||
SPACE.repeat(line_padding + line_padding_remainder), | ||
); | ||
} | ||
} | ||
x_border(width, BorderAlignment::Divider); | ||
} | ||
|
||
// Centered text with border on all sides | ||
Layout::Large => { | ||
x_border(full_message_width, BorderAlignment::Top); | ||
for (line, line_display_width) in lines.iter().zip(lines_display_width.iter()) { | ||
if *line_display_width == 0 { | ||
println!( | ||
"{}{}{}", | ||
VERTICAL.yellow(), | ||
SPACE.repeat(full_message_width), | ||
VERTICAL.yellow() | ||
); | ||
} else { | ||
let line_padding = (full_message_width - line_display_width) / 2; | ||
// for lines of odd length, tack the reminder to the end | ||
let line_padding_remainder = | ||
full_message_width - (line_padding * 2) - line_display_width; | ||
println!( | ||
"{}{}{}{}{}", | ||
VERTICAL.yellow(), | ||
SPACE.repeat(line_padding), | ||
line, | ||
SPACE.repeat(line_padding + line_padding_remainder), | ||
VERTICAL.yellow() | ||
); | ||
} | ||
} | ||
x_border(full_message_width, BorderAlignment::Bottom); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters