Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/banner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use color_print::{cprintln, cstr};

use crate::Config;

pub const BANNER: &str = color_print::cstr! {
r#"
<#FFFFFF>████████╗</#FFFFFF><#999999>██╗ ██╗</#999999><#FF007F>██████╗ </#FF007F>
<#FFFFFF>╚══██╔══╝</#FFFFFF><#999999>╚██╗██╔╝</#999999><#FF007F>╚════██╗</#FF007F>
<#FFFFFF> ██║ </#FFFFFF><#999999> ╚███╔╝ </#999999><#FF007F> █████╔╝</#FF007F>
<#FFFFFF> ██║ </#FFFFFF><#999999> ██╔██╗ </#999999><#FF007F> ╚═══██╗</#FF007F>
<#FFFFFF> ██║ </#FFFFFF><#999999>██╔╝ ██╗</#999999><#FF007F>██████╔╝</#FF007F>
<#FFFFFF> ╚═╝ </#FFFFFF><#999999>╚═╝ ╚═╝</#999999><#FF007F>╚═════╝ </#FF007F>"#
};

pub fn print_banner(config: &Config) {
println!("\n{}\n", BANNER.trim_start());

cprintln!(
"root dir: <#FFFFFF>{}</#FFFFFF>",
config.root_dir().display()
);
cprintln!("channel: <#FFFFFF>{}</#FFFFFF>", config.ensure_channel());
println!();
}
60 changes: 50 additions & 10 deletions src/cmds/check.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
use clap::Parser;
use clap::ValueEnum;

use crate::ArgsCommon;
use crate::{Config, manifest, updates};

#[derive(Clone, Debug, ValueEnum)]
pub enum OutputFormat {
Json,
Text,
}

#[derive(Parser, Default)]
pub struct Args {
#[arg(short, long)]
pub silent: bool,

/// Force
#[arg(short, long)]
pub force: bool,

/// Print details of each update
#[arg(short, long)]
pub verbose: bool,

#[arg(short, long)]
pub output: Option<OutputFormat>,
}

impl ArgsCommon for Args {
fn skip_banner(&self) -> bool {
self.silent || matches!(self.output, Some(OutputFormat::Json))
}
}

fn print_update(update: &updates::Update, manifest: &manifest::Manifest) -> anyhow::Result<()> {
Expand All @@ -28,21 +47,17 @@ fn print_update(update: &updates::Update, manifest: &manifest::Manifest) -> anyh
Ok(())
}

pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> {
let manifest = manifest::load_latest_manifest(config, args.force).await?;

let updates = updates::load_updates(&manifest, config, args.force).await?;

if args.silent {
return Ok(());
}

fn text_output(
updates: &[updates::Update],
manifest: &manifest::Manifest,
verbose: bool,
) -> anyhow::Result<()> {
if updates.is_empty() {
println!("You are up to date 🎉");
return Ok(());
}

if !args.verbose {
if !verbose {
println!("You have {} update/s to install 📦", updates.len());
} else {
for update in updates {
Expand All @@ -52,3 +67,28 @@ pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> {

Ok(())
}

fn json_output(updates: &[updates::Update]) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(&updates)?;
println!("{}", json);
Ok(())
}

pub async fn run(args: &Args, config: &Config) -> anyhow::Result<()> {
let manifest = manifest::load_latest_manifest(config, args.force).await?;

let updates = updates::load_updates(&manifest, config, args.force).await?;

if args.silent {
return Ok(());
}

let output = args.output.as_ref().unwrap_or(&OutputFormat::Text);

match output {
OutputFormat::Json => json_output(&updates)?,
OutputFormat::Text => text_output(&updates, &manifest, args.verbose)?,
};

Ok(())
}
8 changes: 8 additions & 0 deletions src/cmds/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@ use std::path::PathBuf;
use tar::Archive;
use xz2::read::XzDecoder;

use crate::ArgsCommon;
use crate::manifest;
use crate::updates;
use crate::{Config, manifest::*};

#[derive(Parser, Default)]
pub struct Args {
#[arg(short, long)]
release: Option<String>,
}

impl ArgsCommon for Args {
fn skip_banner(&self) -> bool {
false
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct VersionsFile {
tools: Vec<ToolVersion>,
Expand Down
8 changes: 7 additions & 1 deletion src/cmds/show.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use std::process::Command;

use crate::{Config, manifest};
use crate::{ArgsCommon, Config, manifest};

#[derive(Debug, clap::Parser)]
pub struct Args {
pub tool: Option<String>,
}

impl ArgsCommon for Args {
fn skip_banner(&self) -> bool {
false
}
}

fn print_tool(tool: &crate::manifest::Tool, config: &Config) -> anyhow::Result<()> {
println!("bin path: {}", tool.bin_path(config).display());

Expand Down
11 changes: 7 additions & 4 deletions src/cmds/use.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use anyhow::Result;
use clap::Parser;
use std::os::unix::fs::symlink;
use std::{fs, path::Path};

use crate::{Config, perm_path};
use crate::{ArgsCommon, Config, perm_path};

#[derive(Parser)]
pub struct Args {
#[arg(default_value = "stable")]
pub new_channel: String,
}

impl ArgsCommon for Args {
fn skip_banner(&self) -> bool {
false
}
}

impl Default for Args {
fn default() -> Self {
Self {
Expand Down
44 changes: 25 additions & 19 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,15 @@ use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};

mod banner;
mod bin;
mod cmds;
mod manifest;
mod perm_path;
mod updates;

pub const BANNER: &str = color_print::cstr! {
r#"
<#FFFFFF>████████╗</#FFFFFF><#999999>██╗ ██╗</#999999><#FF007F>██████╗ </#FF007F>
<#FFFFFF>╚══██╔══╝</#FFFFFF><#999999>╚██╗██╔╝</#999999><#FF007F>╚════██╗</#FF007F>
<#FFFFFF> ██║ </#FFFFFF><#999999> ╚███╔╝ </#999999><#FF007F> █████╔╝</#FF007F>
<#FFFFFF> ██║ </#FFFFFF><#999999> ██╔██╗ </#999999><#FF007F> ╚═══██╗</#FF007F>
<#FFFFFF> ██║ </#FFFFFF><#999999>██╔╝ ██╗</#999999><#FF007F>██████╔╝</#FF007F>
<#FFFFFF> ╚═╝ </#FFFFFF><#999999>╚═╝ ╚═╝</#999999><#FF007F>╚═════╝ </#FF007F>"#
};

#[derive(Parser)]
#[command(author, version, about, long_about = Some(BANNER))]
#[command(author, version, about, long_about = Some(banner::BANNER))]
struct Cli {
#[arg(global = true, short, long, env = "TX3_ROOT")]
root_dir: Option<PathBuf>,
Expand All @@ -47,6 +38,22 @@ enum Commands {
Show(cmds::show::Args),
}

pub trait ArgsCommon {
fn skip_banner(&self) -> bool;
}

impl Commands {
fn skip_banner(&self) -> bool {
match self {
Commands::Install(x) => x.skip_banner(),
Commands::Check(x) => x.skip_banner(),
Commands::Use(x) => x.skip_banner(),
Commands::Show(x) => x.skip_banner(),
Commands::Uninstall => true,
}
}
}

pub struct Config {
root_dir: Option<PathBuf>,
channel: Option<String>,
Expand Down Expand Up @@ -101,7 +108,8 @@ impl Config {
std::fs::remove_file(&fixed_channel_dir)?;
}

// Create new symlink
std::fs::create_dir_all(&channel_dir)?;

std::os::unix::fs::symlink(&channel_dir, &fixed_channel_dir)?;

Ok(())
Expand All @@ -124,8 +132,7 @@ impl Config {
pub fn ensure_channel(&self) -> String {
match self.channel() {
Ok(channel) => channel,
Err(e) => {
eprintln!("Error getting channel: {}", e);
Err(_) => {
self.set_fixed_channel("stable").unwrap();
"stable".to_string()
}
Expand Down Expand Up @@ -159,11 +166,11 @@ async fn main() -> anyhow::Result<()> {
channel: cli.channel,
};

println!("\n{}\n", BANNER.trim_start());
let skip_banner = cli.command.as_ref().map_or(false, |c| c.skip_banner());

println!("root dir: {}", config.root_dir().display());
println!("current channel: {}", config.ensure_channel());
println!();
if !skip_banner {
banner::print_banner(&config);
}

if let Some(command) = cli.command {
match command {
Expand All @@ -175,7 +182,6 @@ async fn main() -> anyhow::Result<()> {
}
} else {
cmds::install::run(&cmds::install::Args::default(), &config).await?;
cmds::r#use::run(&cmds::r#use::Args::default(), &config).await?;
}

Ok(())
Expand Down
50 changes: 46 additions & 4 deletions src/updates.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::{Duration, SystemTime};

use anyhow::Context as _;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -81,7 +83,13 @@ async fn save_updates(updates: &[Update], config: &Config) -> anyhow::Result<()>
}

pub async fn clear_updates(config: &Config) -> anyhow::Result<()> {
fs::remove_file(config.updates_file())
let updates_file = config.updates_file();

if !updates_file.exists() {
return Ok(());
}

fs::remove_file(updates_file)
.await
.context("removing updates file")?;

Expand All @@ -97,22 +105,56 @@ pub async fn check_updates(manifest: &Manifest, config: &Config) -> anyhow::Resu
}
}

save_updates(&updates, config).await?;
if updates.is_empty() {
clear_updates(config).await?;
} else {
save_updates(&updates, config).await?;
}

Ok(updates)
}

async fn check_updates_timestamp(config: &Config) -> anyhow::Result<Option<SystemTime>> {
let updates_file = config.updates_file();

if !updates_file.exists() {
return Ok(None);
}

let metadata = fs::metadata(updates_file)
.await
.context("getting updates file metadata")?;

let modified = metadata
.modified()
.context("getting updates file modified time")?;

Ok(Some(modified))
}

const UPDATES_STALE_THRESHOLD: Duration = Duration::from_secs(60 * 60 * 24);

fn updates_are_stale(timestamp: Option<SystemTime>) -> bool {
timestamp.is_none() || timestamp.unwrap() < SystemTime::now() - UPDATES_STALE_THRESHOLD
}

pub async fn load_updates(
manifest: &Manifest,
config: &Config,
force_check: bool,
) -> anyhow::Result<Vec<Update>> {
let updates_file = config.updates_file();
let timestamp = check_updates_timestamp(config).await?;

if !updates_file.exists() || force_check {
if force_check || updates_are_stale(timestamp) {
check_updates(manifest, config).await?;
}

let updates_file = config.updates_file();

if !updates_file.exists() {
return Ok(vec![]);
}

let updates = fs::read_to_string(updates_file)
.await
.context("reading updates file")?;
Expand Down