From 1bb19d063f7c0b75bf6733ae36ccaca3149908b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 13 Nov 2024 11:54:39 +0100 Subject: [PATCH 01/28] =?UTF-8?q?=E2=9C=A8=20Switch=20no=5Fsubcommand=20er?= =?UTF-8?q?ror=20to=20crossterm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 27 ++++++------------------- src/view/mod.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 src/view/mod.rs diff --git a/src/main.rs b/src/main.rs index fae5e68..f277889 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,22 +7,7 @@ use utils::out::clear_screen; mod config; mod functions; mod modules; - -fn setup_ui() { - use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet}; - - let mut render_config = RenderConfig::default(); - if config::load_config().color != config::defines::COLOR::NORMAL { - render_config.prompt = - StyleSheet::new().with_fg(config::load_config().color.as_inquire_color()); - } - render_config.answer = StyleSheet::new() - .with_fg(Color::Grey) - .with_attr(Attributes::BOLD); - render_config.help_message = StyleSheet::new().with_fg(Color::DarkGrey); - - inquire::set_global_render_config(render_config); -} +mod view; #[derive(Parser)] #[command(name = "tgh", author, version, about)] @@ -56,13 +41,13 @@ enum SubCommand { async fn main() { let args = Cli::parse(); + view::setup_view_controller(); config::check_prerequisites().await; - setup_ui(); let subcmd = match args.subcmd { Some(subcmd) => subcmd, None => { - out::print_error("\nNo subcommand provided\n"); + view::no_subcommand_error(); return; } }; @@ -83,8 +68,8 @@ async fn main() { SubCommand::History(options) => { return modules::history::commit_history(options); } - SubCommand::Login => { - config::login().await - } + SubCommand::Login => config::login().await, } + + view::clean_up(); } diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..79e0088 --- /dev/null +++ b/src/view/mod.rs @@ -0,0 +1,54 @@ +use crossterm::{ + cursor::MoveToNextLine, + execute, + style::{Attribute, Color, Print, SetAttribute, SetForegroundColor}, + terminal::{disable_raw_mode, enable_raw_mode}, +}; +use std::io::stdout; + +pub fn setup_view_controller() { + enable_raw_mode().unwrap(); + + let default_panic = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + disable_raw_mode().unwrap(); + println!(); + default_panic(panic_info); + })); +} + +pub fn clean_up() { + disable_raw_mode().unwrap(); +} + +pub fn no_subcommand_error() { + enable_raw_mode().unwrap(); + + execute!( + stdout(), + SetForegroundColor(Color::Red), + SetAttribute(Attribute::Bold), + Print("error: "), + SetAttribute(Attribute::Reset), + Print("no subcommand provided\n\n"), + MoveToNextLine(1), + SetAttribute(Attribute::Bold), + SetAttribute(Attribute::Underlined), + Print("Usage:"), + SetAttribute(Attribute::Reset), + SetAttribute(Attribute::Bold), + Print(" tgh"), + SetAttribute(Attribute::Reset), + Print(" [COMMAND]\n\n"), + MoveToNextLine(1), + Print("For more information try '"), + SetAttribute(Attribute::Bold), + Print("tgh --help"), + SetAttribute(Attribute::Reset), + Print("'\n"), + MoveToNextLine(1), + ) + .unwrap(); + + disable_raw_mode().unwrap(); +} From d86f377672f00a1db475e2d30c36f2fa60aa6ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Fri, 22 Nov 2024 22:50:18 +0100 Subject: [PATCH 02/28] =?UTF-8?q?=E2=9C=A8=20Create=20a=20layout=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 1 + src/view/mod.rs | 196 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 170 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index f277889..a3db1b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; mod utils; use utils::out; use utils::out::clear_screen; +use view::printer; mod config; mod functions; diff --git a/src/view/mod.rs b/src/view/mod.rs index 79e0088..99d2e72 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,7 +1,7 @@ use crossterm::{ cursor::MoveToNextLine, execute, - style::{Attribute, Color, Print, SetAttribute, SetForegroundColor}, + style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, terminal::{disable_raw_mode, enable_raw_mode}, }; use std::io::stdout; @@ -21,34 +21,176 @@ pub fn clean_up() { disable_raw_mode().unwrap(); } -pub fn no_subcommand_error() { +enum VisualEffect { + SetAttribute(Attribute), + SetForegroundColor(Color), + SetBackgroundColor(Color), +} + +fn match_effect(effect: &str) -> Vec { + let mut effects: Vec = Vec::new(); + + let mut i = 1; + while i < effect.len() { + let c = effect.chars().nth(i).unwrap(); + + let mut special = String::new(); + special.push(c); + + i += 1; + + while i < effect.len() && effect.chars().nth(i).unwrap() != '$' { + special.push(effect.chars().nth(i).unwrap()); + i += 1; + } + + match special.as_str() { + "b" => effects.push(VisualEffect::SetAttribute(Attribute::Bold)), + "i" => effects.push(VisualEffect::SetAttribute(Attribute::Italic)), + "u" => effects.push(VisualEffect::SetAttribute(Attribute::Underlined)), + + "cr" => effects.push(VisualEffect::SetForegroundColor(Color::Red)), + _ => {} + } + + i += 1; + } + + effects +} + +fn set_new_effects(stdout: &mut std::io::Stdout, effects: &Vec>) { + execute!(stdout, SetAttribute(Attribute::Reset)).unwrap(); + for effect in effects { + for e in effect { + match e { + VisualEffect::SetAttribute(a) => { + execute!(stdout, SetAttribute(*a)).unwrap(); + } + VisualEffect::SetForegroundColor(c) => { + execute!(stdout, SetForegroundColor(*c)).unwrap(); + } + VisualEffect::SetBackgroundColor(c) => { + execute!(stdout, SetBackgroundColor(*c)).unwrap(); + } + } + } + } +} + +/** +This function is used to print a string with special effects. The special effects are defined by the following syntax: +- $b: bold +- $i: italic +- $u: underline + +- $cr: red color +- $cg: green color +- $cb: blue color +- $cy: yellow color +- $cm: magenta color +- $cc: cyan color +- $cw: white color + +- $br: background red color +- $bg: background green color +- $bb: background blue color +- $by: background yellow color +- $bm: background magenta color +- $bc: background cyan color +- $bw: background white color + +- />: tab (4 spaces) + */ +pub fn printer(content: &str) { + let chars = content.chars(); + let n = chars.count(); + let mut stdout = stdout(); + + let mut effects: Vec> = Vec::new(); + enable_raw_mode().unwrap(); - execute!( - stdout(), - SetForegroundColor(Color::Red), - SetAttribute(Attribute::Bold), - Print("error: "), - SetAttribute(Attribute::Reset), - Print("no subcommand provided\n\n"), - MoveToNextLine(1), - SetAttribute(Attribute::Bold), - SetAttribute(Attribute::Underlined), - Print("Usage:"), - SetAttribute(Attribute::Reset), - SetAttribute(Attribute::Bold), - Print(" tgh"), - SetAttribute(Attribute::Reset), - Print(" [COMMAND]\n\n"), - MoveToNextLine(1), - Print("For more information try '"), - SetAttribute(Attribute::Bold), - Print("tgh --help"), - SetAttribute(Attribute::Reset), - Print("'\n"), - MoveToNextLine(1), - ) - .unwrap(); + let mut i = 0; + while i < n { + let c = content.chars().nth(i).unwrap(); + + // Add a special effect + if c == '$' { + let mut special = String::new(); + + while content.chars().nth(i).unwrap() != ' ' { + special.push(content.chars().nth(i).unwrap()); + i += 1; + } + + let effect = match_effect(&special); + effects.push(effect); + set_new_effects(&mut stdout, &effects); + + i += 2; + + continue; + } + + // Clear the last effect + if c == '`' { + effects.pop(); + + set_new_effects(&mut stdout, &effects); + + i += 1; + continue; + } + + // Print a new line and clear the spaces + if c == '\n' { + execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); + i += 1; + + while i < n && content.chars().nth(i).unwrap() == ' ' { + i += 1; + } + + continue; + } + + if c == '/' { + i += 1; + + if i >= n { + break; + } + + let c = content.chars().nth(i).unwrap(); + + // Print a 4 wide space (tab) + if c == '>' { + execute!(stdout, Print(" ")).unwrap(); + } + + i += 1; + continue; + } + + execute!(stdout, Print(c)).unwrap(); + + i += 1; + } + + execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); disable_raw_mode().unwrap(); } + +pub fn no_subcommand_error() { + let eror_message = r#" + $b$cr `error`: no subcommand provided + + $b$u `Usage`: $b `tgh` [COMMAND] + + For more information try $b `'tgh --help'` + "#; + + printer(eror_message); +} From dcc20d9563c610885bf15922ffe3e6a8802432db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 24 Nov 2024 21:53:00 +0100 Subject: [PATCH 03/28] =?UTF-8?q?=F0=9F=92=84=20Port=20config=20error=20me?= =?UTF-8?q?ssages=20to=20the=20new=20view=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/mod.rs | 52 ++++++++++++++++++++++++++++-------- src/config/utils.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++ src/view/mod.rs | 4 +-- 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index d3af289..f98e3a4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,4 @@ -use crate::utils::out; +use crate::{utils::out, view}; use serde::{Deserialize, Serialize}; mod config; @@ -20,17 +20,47 @@ pub struct Config { pub fancy: bool, } +// Create a error message for when git is not installed depending on the OS +#[cfg(target_os = "windows")] +static GIT_NOT_INSTALLED: &str = r#" +$b$cr `error`: $b `git` is not installed. + +You can install it using Chocolatey: +$i ` choco install git` + +or using Winget: +$i ` winget install --id Git.Git -e --source winget` + +Or you can download it from the official website: +$b ` `$u `https://git-scm.com/download/win` +"#; + +#[cfg(target_os = "macos")] +static GIT_NOT_INSTALLED: &str = r#" +$b$cr `error`: $b `git` is not installed. + +You can install it using Homebrew: +$i ` brew install git` + +or using MacPorts: +$i ` sudo port install git` + +Xcode also includes git. You can install it from the App Store. +"#; + /// Checks if the prerequisites for tgh are installed. /// If not, it will print an error and exit. -/// -/// ### Arguments -/// * `args` - A vector of the command line arguments. pub async fn check_prerequisites() { // Check if git is installed if !git::check_git() { - out::print_error("Error: Git is not installed.\n"); - println!("Please install using the link below:"); - println!("\x1B[mhttps://git-scm.com/downloads\x1B[m\n"); + #[cfg(target_os = "windows")] + view::printer(GIT_NOT_INSTALLED); + + #[cfg(target_os = "macos")] + view::printer(GIT_NOT_INSTALLED); + + #[cfg(target_os = "linux")] + view::printer(&utils::get_git_installation_instructions()); std::process::exit(1); } @@ -38,17 +68,17 @@ pub async fn check_prerequisites() { git::check_git_config(); // Check for a config file - if !utils::config_exists() { - out::print_error("Config file not found. Creating one...\n"); + if !utils::config_exists() || true { + view::printer("\n$b$cr `error`: Config file not found. Creating a new one...\n"); config::create_config(); } else if !utils::validate_config_file() { - out::print_error("Config file is invalid. Creating a new one...\n"); + view::printer("\n$b$cr `error`: Config file is invalid. Creating a new one...\n"); config::create_config(); } // Check for a GitHub token if !github::check_token() { - out::print_error("Error: GitHub token invalid.\n"); + view::printer("\n$b$cr `error`: GitHub token not found. Logging in...\n"); login().await; std::thread::sleep(std::time::Duration::from_secs(1)); diff --git a/src/config/utils.rs b/src/config/utils.rs index ea77f88..c87b416 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -312,3 +312,68 @@ pub fn get_labels() -> Vec { labels } + +#[cfg(target_os = "linux")] +pub fn get_git_installation_instructions() -> String { + let install_cmd; + + // Get the distribution + let binding = match std::fs::read_to_string("/etc/os-release") { + Ok(binding) => binding, + Err(_) => { + return r#" + $b$cr `error`: $b `git` is not installed. + + You can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "# + .into(); + } + }; + let distro = binding + .lines() + .find(|line| line.starts_with("ID=")) + .unwrap() + .split('=') + .last() + .unwrap(); + + match distro { + "ubuntu" | "debian" => { + install_cmd = "sudo apt install git"; + } + "fedora" | "centos" | "rhel" => { + install_cmd = "sudo dnf install git"; + } + "arch" | "manjaro" => { + install_cmd = "sudo pacman -S git"; + } + "alpine" => { + install_cmd = "apk add git"; + } + _ => { + return r#" + $b$cr `error`: $b `git` is not installed. + + You can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "# + .into(); + } + } + + let instructions = format!( + r#" + $b$cr `error`: $b `git` is not installed. + + You can install it using your package manager: + $i ` {}` + + Or you can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "#, + install_cmd + ); + + instructions +} diff --git a/src/view/mod.rs b/src/view/mod.rs index 99d2e72..0e582dd 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -100,7 +100,7 @@ This function is used to print a string with special effects. The special effect - $bc: background cyan color - $bw: background white color -- />: tab (4 spaces) +- &>: tab (4 spaces) */ pub fn printer(content: &str) { let chars = content.chars(); @@ -155,7 +155,7 @@ pub fn printer(content: &str) { continue; } - if c == '/' { + if c == '&' { i += 1; if i >= n { From f17935b305234299e44942cbe140b422e034f14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 24 Nov 2024 22:13:25 +0100 Subject: [PATCH 04/28] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20check=5Fgit=5Fc?= =?UTF-8?q?onfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the option to set user.name or user.email using tgh and migrate the error message to printer. --- src/config/git.rs | 67 ++++++++++++++--------------------------------- src/config/mod.rs | 37 ++++++++++++++++++++++++-- src/view/mod.rs | 1 + 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/src/config/git.rs b/src/config/git.rs index ad6d1d6..eba7e9b 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -1,4 +1,4 @@ -use crate::out; +use crate::{out, view}; pub fn check_git() -> bool { let mut command = std::process::Command::new("git"); @@ -12,64 +12,35 @@ pub fn check_git() -> bool { false } + +pub enum GitConfigError { + NameNotFound, + EmailNotFound, +} + /// Checks if the user has a git config. (user.name, user.email) -pub fn check_git_config() { +pub fn check_git_config() -> Result<(), GitConfigError> { let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.name"]); + command.args(["config", "user.name"]); let output = command.output().unwrap(); + let binding = String::from_utf8(output.stdout).unwrap(); + let s = binding.trim(); - if !output.status.success() || output.stdout.len() == 0 { - out::print_error("Error: user.name was not found in git config.\n"); - let name = ask_git_name(); - - let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.name", &name]); - - let output = command.output().unwrap(); - - if !output.status.success() { - out::print_error("Error: Failed to set user.name.\n"); - println!("Try setting it manually using `git config --global user.name \"Your Name\"`"); - std::process::exit(1); - } + if !output.status.success() || s.len() == 0 { + return Err(GitConfigError::NameNotFound); } let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.email"]); + command.args(["config", "user.email"]); let output = command.output().unwrap(); + let binding = String::from_utf8(output.stdout).unwrap(); + let s = binding.trim(); - if !output.status.success() || output.stdout.len() == 0 { - out::print_error("Error: user.email was not found in git config.\n"); - let email = ask_git_email(); - - let mut command = std::process::Command::new("git"); - command.args(["config", "--global", "user.email", &email]); - - let output = command.output().unwrap(); - - if !output.status.success() { - out::print_error("Error: Failed to set user.email.\n"); - println!( - "Try setting it manually using `git config --global user.email \"Your Email\"`" - ); - std::process::exit(1); - } + if !output.status.success() || s.len() == 0 { + return Err(GitConfigError::EmailNotFound); } -} - -fn ask_git_name() -> String { - let name = inquire::Text::new("Enter name used for git:") - .with_validator(inquire::required!("Name is required.")) - .prompt(); - - name.unwrap() -} -fn ask_git_email() -> String { - let email = inquire::Text::new("Enter email used for git:") - .with_validator(super::utils::validate_email) - .prompt(); - email.unwrap() + Ok(()) } diff --git a/src/config/mod.rs b/src/config/mod.rs index f98e3a4..6e2dabb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,5 @@ use crate::{utils::out, view}; +use git::check_git_config; use serde::{Deserialize, Serialize}; mod config; @@ -65,10 +66,42 @@ pub async fn check_prerequisites() { } // Check for git config - git::check_git_config(); + match check_git_config() { + Ok(_) => {} + Err(err) => match err { + git::GitConfigError::NameNotFound => { + let msg = r#" + $b$cr `error`: Git user.name not found. + + You can set it using the following command: + $i ` git config user.name "Your Name"` + + or globally: + $i ` git config --global user.name "Your Name"` + $i$s `this will not work if you set it locally` + "#; + view::printer(msg); + std::process::exit(1); + } + git::GitConfigError::EmailNotFound => { + let msg = r#" + $b$cr `error`: Git user.email not found. + + You can set it using the following command: + $i ` git config user.email "` + + or globally: + $i ` git config --global user.email "` + $i$s `this will not work if you set it locally` + "#; + view::printer(msg); + std::process::exit(1); + } + }, + } // Check for a config file - if !utils::config_exists() || true { + if !utils::config_exists() { view::printer("\n$b$cr `error`: Config file not found. Creating a new one...\n"); config::create_config(); } else if !utils::validate_config_file() { diff --git a/src/view/mod.rs b/src/view/mod.rs index 0e582dd..2574f7e 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -48,6 +48,7 @@ fn match_effect(effect: &str) -> Vec { "b" => effects.push(VisualEffect::SetAttribute(Attribute::Bold)), "i" => effects.push(VisualEffect::SetAttribute(Attribute::Italic)), "u" => effects.push(VisualEffect::SetAttribute(Attribute::Underlined)), + "s" => effects.push(VisualEffect::SetAttribute(Attribute::Dim)), "cr" => effects.push(VisualEffect::SetForegroundColor(Color::Red)), _ => {} From eaa39c24d94d5fa3f7c42f82c5d30169e94f87ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Tue, 18 Mar 2025 16:34:54 +0100 Subject: [PATCH 05/28] =?UTF-8?q?=E2=9C=A8=20Add=20aditional=20color=20opt?= =?UTF-8?q?ions=20to=20the=20printer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/view/mod.rs b/src/view/mod.rs index 2574f7e..76338a4 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -51,6 +51,20 @@ fn match_effect(effect: &str) -> Vec { "s" => effects.push(VisualEffect::SetAttribute(Attribute::Dim)), "cr" => effects.push(VisualEffect::SetForegroundColor(Color::Red)), + "cg" => effects.push(VisualEffect::SetForegroundColor(Color::Green)), + "cb" => effects.push(VisualEffect::SetForegroundColor(Color::Blue)), + "cy" => effects.push(VisualEffect::SetForegroundColor(Color::Yellow)), + "cm" => effects.push(VisualEffect::SetForegroundColor(Color::Magenta)), + "cc" => effects.push(VisualEffect::SetForegroundColor(Color::Cyan)), + "cw" => effects.push(VisualEffect::SetForegroundColor(Color::White)), + + "br" => effects.push(VisualEffect::SetBackgroundColor(Color::Red)), + "bg" => effects.push(VisualEffect::SetBackgroundColor(Color::Green)), + "bb" => effects.push(VisualEffect::SetBackgroundColor(Color::Blue)), + "by" => effects.push(VisualEffect::SetBackgroundColor(Color::Yellow)), + "bm" => effects.push(VisualEffect::SetBackgroundColor(Color::Magenta)), + "bc" => effects.push(VisualEffect::SetBackgroundColor(Color::Cyan)), + "bw" => effects.push(VisualEffect::SetBackgroundColor(Color::White)), _ => {} } From a82aa91026d4a3370924b90e768ae3b70380e236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Tue, 18 Mar 2025 16:53:00 +0100 Subject: [PATCH 06/28] =?UTF-8?q?=F0=9F=94=A8=20Switch=20to=20printer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/github.rs | 9 +++++---- src/config/mod.rs | 19 +++++++++++++++++++ src/main.rs | 1 - 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/config/github.rs b/src/config/github.rs index a73bb42..1e46bb7 100644 --- a/src/config/github.rs +++ b/src/config/github.rs @@ -1,6 +1,7 @@ use super::utils; use super::Config; use crate::out; +use crate::view; pub fn check_token() -> bool { if !utils::config_exists() || !utils::validate_config_file() { @@ -47,10 +48,10 @@ pub async fn authenticate() -> Result { let login_url = text_split[4].replace("%3A", ":").replace("%2F", "/"); let grant_type = "urn:ietf:params:oauth:grant-type:device_code"; - println!( - "Please visit this URL to authenticate: \x1B[4m{}\x1B[m", + view::printer(&format!( + "\nPlease visit this URL to authenticate: $u `{}`\n", login_url - ); + )); let clipboard = Clipboard::new(); match clipboard { @@ -63,7 +64,7 @@ pub async fn authenticate() -> Result { } Err(_) => { println!( - "Error copying to clipboard, copy the code manually: {}", + "Could not copy the code to the clipboard, copy the code manually: {}", user_code ); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6e2dabb..34e52c0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -49,6 +49,25 @@ $i ` sudo port install git` Xcode also includes git. You can install it from the App Store. "#; +#[cfg(target_os = "linux")] +static GIT_NOT_INSTALLED: &str = r#" +$b$cr `error`: $b `git` is not installed. + +You can install it using your package manager. + +For example, on Ubuntu: +$i ` sudo apt install git` + +On Fedora: +$i ` sudo dnf install git` + +On Arch Linux: +$i ` sudo pacman -S git` + +You can also download it from the official website: +$b ` `$u `https://git-scm.com/download/linux` +"#; + /// Checks if the prerequisites for tgh are installed. /// If not, it will print an error and exit. pub async fn check_prerequisites() { diff --git a/src/main.rs b/src/main.rs index a3db1b1..f277889 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use clap::{Parser, Subcommand}; mod utils; use utils::out; use utils::out::clear_screen; -use view::printer; mod config; mod functions; From 3ae4129937123535fa811c71177104f19c6c7ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Tue, 18 Mar 2025 16:55:08 +0100 Subject: [PATCH 07/28] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/mod.rs | 19 ------------------- src/config/utils.rs | 22 ---------------------- 2 files changed, 41 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 34e52c0..6e2dabb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -49,25 +49,6 @@ $i ` sudo port install git` Xcode also includes git. You can install it from the App Store. "#; -#[cfg(target_os = "linux")] -static GIT_NOT_INSTALLED: &str = r#" -$b$cr `error`: $b `git` is not installed. - -You can install it using your package manager. - -For example, on Ubuntu: -$i ` sudo apt install git` - -On Fedora: -$i ` sudo dnf install git` - -On Arch Linux: -$i ` sudo pacman -S git` - -You can also download it from the official website: -$b ` `$u `https://git-scm.com/download/linux` -"#; - /// Checks if the prerequisites for tgh are installed. /// If not, it will print an error and exit. pub async fn check_prerequisites() { diff --git a/src/config/utils.rs b/src/config/utils.rs index c87b416..b0f5623 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -100,28 +100,6 @@ pub fn save_config_file(config: crate::config::Config) { config_file.write_all(config_contents.as_bytes()).unwrap(); } -pub fn validate_email( - email: &str, -) -> Result { - use regex::Regex; - - let re = Regex::new(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$").unwrap(); - - if email.len() == 0 { - return Ok(inquire::validator::Validation::Invalid( - "Email cannot be empty".into(), - )); - } - - if !re.is_match(email) { - return Ok(inquire::validator::Validation::Invalid( - "Invalid email".into(), - )); - } - - Ok(inquire::validator::Validation::Valid) -} - #[derive(Clone, Debug)] pub struct CommitLabel { pub label: String, From 8f309c8531777b773202bf0dca992f2c7b46e96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 2 Apr 2025 20:37:10 +0200 Subject: [PATCH 08/28] =?UTF-8?q?=E2=9C=A8=20Refactor=20git=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.rs | 74 +----------------------- src/config/defines.rs | 15 +---- src/config/git.rs | 128 ++++++++++++++++++++++++++++++++++++++++-- src/config/mod.rs | 45 ++------------- 4 files changed, 131 insertions(+), 131 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index e4aab7d..fdf8ba5 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -2,8 +2,7 @@ use super::{defines, utils, Config}; use crate::out; /// Loads the config file. -/// If the config file doesn't exist, it will create one. -/// If the config file is invalid, it will create a new one. +/// If the config file doesn't exist or is invalid, it will create a new one. /// /// ### Returns /// A Config struct. @@ -125,74 +124,3 @@ fn ask_fancy() -> bool { } } } - -fn update_username(username: String) { - let config = utils::read_config(); - - let new_config = Config { - username, - token: config.token, - sort: config.sort, - protocol: config.protocol, - color: config.color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_sort(sort: defines::SORTING) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort, - protocol: config.protocol, - color: config.color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_protocol(protocol: defines::PROTOCOL) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort: config.sort, - protocol, - color: config.color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_color(color: defines::COLOR) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort: config.sort, - protocol: config.protocol, - color, - fancy: config.fancy, - }; - - utils::save_config_file(new_config); -} -fn update_fancy(fancy: bool) { - let config = utils::read_config(); - - let new_config = Config { - username: config.username, - token: config.token, - sort: config.sort, - protocol: config.protocol, - color: config.color, - fancy, - }; - - utils::save_config_file(new_config); -} diff --git a/src/config/defines.rs b/src/config/defines.rs index 45eed2a..d9c83af 100644 --- a/src/config/defines.rs +++ b/src/config/defines.rs @@ -40,21 +40,8 @@ impl COLOR { } .to_string() } - - pub fn as_inquire_color(&self) -> inquire::ui::Color { - match self { - COLOR::RED => inquire::ui::Color::LightRed, - COLOR::GREEN => inquire::ui::Color::LightGreen, - COLOR::YELLOW => inquire::ui::Color::LightYellow, - COLOR::BLUE => inquire::ui::Color::LightBlue, - COLOR::MAGENTA => inquire::ui::Color::LightMagenta, - COLOR::CYAN => inquire::ui::Color::LightCyan, - COLOR::WHITE => inquire::ui::Color::White, - COLOR::GRAY => inquire::ui::Color::Grey, - _ => inquire::ui::Color::White, - } - } } + impl std::fmt::Display for COLOR { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let color = match self { diff --git a/src/config/git.rs b/src/config/git.rs index eba7e9b..45450ce 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -1,16 +1,134 @@ -use crate::{out, view}; +const MIN_GIT_VERSION: &str = "2.20.0"; -pub fn check_git() -> bool { +// Create an error message for when git is not installed depending on the OS +#[cfg(target_os = "windows")] +static GIT_INSTALL_INSTRUCTIONS: &str = r#" +You can install it using Chocolatey: +$i ` choco install git` + +or using Winget: +$i ` winget install --id Git.Git -e --source winget` + +Or you can download it from the official website: +$b ` `$u `https://git-scm.com/download/win` +"#; + +#[cfg(target_os = "macos")] +static GIT_INSTALL_INSTRUCTIONS: &str = r#" +You can install it using Homebrew: +$i ` brew install git` + +or using MacPorts: +$i ` sudo port install git` + +Xcode also includes git. You can install it from the App Store. +"#; + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +static GIT_INSTALL_INSTRUCTIONS: &str = r#" +You can install it using your package manager. + +Or you can download it from the official website: +$b ` `$u `https://git-scm.com/` +"#; + +pub enum GitError { + NotInstalled, + VersionNotSupported { current: String, min: String }, +} + +impl GitError { + pub fn to_string(&self) -> String { + match self { + GitError::NotInstalled => { + #[cfg(target_os = "linux")] // For Linux, use the dynamic message (based on distro) + let message = utils::get_git_installation_instructions(); + + #[cfg(not(target_os = "linux"))] // For other OSes, use the static message + let message = GIT_INSTALL_INSTRUCTIONS; + + return format!( + r#" + $b$cr `error`: $b `git` is not installed. + + {}"#, + message, + ); + } + + GitError::VersionNotSupported { current, min } => { + #[cfg(target_os = "linux")] + let install_cmd = utils::get_git_installation_instructions(); + + #[cfg(not(target_os = "linux"))] + let install_cmd = GIT_INSTALL_INSTRUCTIONS; + + let msg = format!( + r#" + $b$cr `error`: $b `git` version not supported. + You need at least version {}, you are currently using version {} + + {} + "#, + min, current, install_cmd + ); + + return msg; + } + } + } +} + +pub fn validate_git_install() -> Result<(), GitError> { let mut command = std::process::Command::new("git"); command.arg("--version"); let output = command.output().unwrap(); - if output.status.success() { - return true; + if !output.status.success() { + return Err(GitError::NotInstalled); + } + + let binding = String::from_utf8(output.stdout).unwrap(); + let s = binding.trim(); + if s.len() == 0 { + return Err(GitError::NotInstalled); } - false + let version = s.split(" ").last().unwrap(); + let version_u32 = version + .split(".") + .collect::>() + .iter() + .map(|s| s.parse::().unwrap()) + .collect::>(); + + let min_version = MIN_GIT_VERSION + .split(".") + .collect::>() + .iter() + .map(|s| s.parse::().unwrap()) + .collect::>(); + + if version_u32.len() != 3 || min_version.len() != 3 { + return Err(GitError::VersionNotSupported { + current: version.to_string(), + min: MIN_GIT_VERSION.to_string(), + }); + } + + for i in 0..3 { + if version_u32[i] < min_version[i] { + return Err(GitError::VersionNotSupported { + current: version.to_string(), + min: MIN_GIT_VERSION.to_string(), + }); + } else if version_u32[i] > min_version[i] { + return Ok(()); + } + } + + Ok(()) } pub enum GitConfigError { diff --git a/src/config/mod.rs b/src/config/mod.rs index 6e2dabb..170f5a6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,48 +21,15 @@ pub struct Config { pub fancy: bool, } -// Create a error message for when git is not installed depending on the OS -#[cfg(target_os = "windows")] -static GIT_NOT_INSTALLED: &str = r#" -$b$cr `error`: $b `git` is not installed. - -You can install it using Chocolatey: -$i ` choco install git` - -or using Winget: -$i ` winget install --id Git.Git -e --source winget` - -Or you can download it from the official website: -$b ` `$u `https://git-scm.com/download/win` -"#; - -#[cfg(target_os = "macos")] -static GIT_NOT_INSTALLED: &str = r#" -$b$cr `error`: $b `git` is not installed. - -You can install it using Homebrew: -$i ` brew install git` - -or using MacPorts: -$i ` sudo port install git` - -Xcode also includes git. You can install it from the App Store. -"#; - /// Checks if the prerequisites for tgh are installed. /// If not, it will print an error and exit. pub async fn check_prerequisites() { - // Check if git is installed - if !git::check_git() { - #[cfg(target_os = "windows")] - view::printer(GIT_NOT_INSTALLED); - - #[cfg(target_os = "macos")] - view::printer(GIT_NOT_INSTALLED); - - #[cfg(target_os = "linux")] - view::printer(&utils::get_git_installation_instructions()); - std::process::exit(1); + match git::validate_git_install() { + Ok(_) => {} + Err(err) => { + view::printer(&err.to_string()); + std::process::exit(1); + } } // Check for git config From ab991381bc6d2d9782a955b16e6ea80320fed1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 2 Apr 2025 20:38:56 +0200 Subject: [PATCH 09/28] =?UTF-8?q?=F0=9F=9A=9A=20Move=20git=20install=20ins?= =?UTF-8?q?tructions=20for=20unix=20to=20git.rs=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/git.rs | 69 +++++++++++++++++++++++++++++++++++++++++++-- src/config/utils.rs | 64 ----------------------------------------- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/src/config/git.rs b/src/config/git.rs index 45450ce..2359dda 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -42,7 +42,7 @@ impl GitError { match self { GitError::NotInstalled => { #[cfg(target_os = "linux")] // For Linux, use the dynamic message (based on distro) - let message = utils::get_git_installation_instructions(); + let message = get_git_installation_instructions(); #[cfg(not(target_os = "linux"))] // For other OSes, use the static message let message = GIT_INSTALL_INSTRUCTIONS; @@ -58,7 +58,7 @@ impl GitError { GitError::VersionNotSupported { current, min } => { #[cfg(target_os = "linux")] - let install_cmd = utils::get_git_installation_instructions(); + let install_cmd = get_git_installation_instructions(); #[cfg(not(target_os = "linux"))] let install_cmd = GIT_INSTALL_INSTRUCTIONS; @@ -162,3 +162,68 @@ pub fn check_git_config() -> Result<(), GitConfigError> { Ok(()) } + +#[cfg(target_os = "linux")] +fn get_git_installation_instructions() -> String { + let install_cmd; + + // Get the distribution + let binding = match std::fs::read_to_string("/etc/os-release") { + Ok(binding) => binding, + Err(_) => { + return r#" + $b$cr `error`: $b `git` is not installed. + + You can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "# + .into(); + } + }; + let distro = binding + .lines() + .find(|line| line.starts_with("ID=")) + .unwrap() + .split('=') + .last() + .unwrap(); + + match distro { + "ubuntu" | "debian" => { + install_cmd = "sudo apt install git"; + } + "fedora" | "centos" | "rhel" => { + install_cmd = "sudo dnf install git"; + } + "arch" | "manjaro" => { + install_cmd = "sudo pacman -S git"; + } + "alpine" => { + install_cmd = "apk add git"; + } + _ => { + return r#" + $b$cr `error`: $b `git` is not installed. + + You can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "# + .into(); + } + } + + let instructions = format!( + r#" + $b$cr `error`: $b `git` is not installed. + + You can install it using your package manager: + $i ` {}` + + Or you can download it from the official website: + $b ` `$u `https://git-scm.com/download/linux` + "#, + install_cmd + ); + + instructions +} diff --git a/src/config/utils.rs b/src/config/utils.rs index b0f5623..292b2f1 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -291,67 +291,3 @@ pub fn get_labels() -> Vec { labels } -#[cfg(target_os = "linux")] -pub fn get_git_installation_instructions() -> String { - let install_cmd; - - // Get the distribution - let binding = match std::fs::read_to_string("/etc/os-release") { - Ok(binding) => binding, - Err(_) => { - return r#" - $b$cr `error`: $b `git` is not installed. - - You can download it from the official website: - $b ` `$u `https://git-scm.com/download/linux` - "# - .into(); - } - }; - let distro = binding - .lines() - .find(|line| line.starts_with("ID=")) - .unwrap() - .split('=') - .last() - .unwrap(); - - match distro { - "ubuntu" | "debian" => { - install_cmd = "sudo apt install git"; - } - "fedora" | "centos" | "rhel" => { - install_cmd = "sudo dnf install git"; - } - "arch" | "manjaro" => { - install_cmd = "sudo pacman -S git"; - } - "alpine" => { - install_cmd = "apk add git"; - } - _ => { - return r#" - $b$cr `error`: $b `git` is not installed. - - You can download it from the official website: - $b ` `$u `https://git-scm.com/download/linux` - "# - .into(); - } - } - - let instructions = format!( - r#" - $b$cr `error`: $b `git` is not installed. - - You can install it using your package manager: - $i ` {}` - - Or you can download it from the official website: - $b ` `$u `https://git-scm.com/download/linux` - "#, - install_cmd - ); - - instructions -} From d308c213ccd0b5daad119fb4b71ea8e65b259e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 2 Apr 2025 20:44:35 +0200 Subject: [PATCH 10/28] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20git=20config=20?= =?UTF-8?q?error=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/git.rs | 37 ++++++++++++++++++++++++++++++++++--- src/config/mod.rs | 34 ++++------------------------------ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/config/git.rs b/src/config/git.rs index 2359dda..d57562d 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -2,7 +2,7 @@ const MIN_GIT_VERSION: &str = "2.20.0"; // Create an error message for when git is not installed depending on the OS #[cfg(target_os = "windows")] -static GIT_INSTALL_INSTRUCTIONS: &str = r#" +const GIT_INSTALL_INSTRUCTIONS: &str = r#" You can install it using Chocolatey: $i ` choco install git` @@ -14,7 +14,7 @@ $b ` `$u `https://git-scm.com/download/win` "#; #[cfg(target_os = "macos")] -static GIT_INSTALL_INSTRUCTIONS: &str = r#" +const GIT_INSTALL_INSTRUCTIONS: &str = r#" You can install it using Homebrew: $i ` brew install git` @@ -25,13 +25,35 @@ Xcode also includes git. You can install it from the App Store. "#; #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -static GIT_INSTALL_INSTRUCTIONS: &str = r#" +const GIT_INSTALL_INSTRUCTIONS: &str = r#" You can install it using your package manager. Or you can download it from the official website: $b ` `$u `https://git-scm.com/` "#; +const GIT_NAME_NOT_FOUND: &str = r#" + $b$cr `error`: Git user.name not found. + + You can set it using the following command: + $i ` git config user.name "Your Name"` + + or globally: + $i ` git config --global user.name "Your Name"` + $i$s `this will not work if you set it locally` +"#; + +const GIT_EMAIL_NOT_FOUND: &str = r#" + $b$cr `error`: Git user.email not found. + + You can set it using the following command: + $i ` git config user.email "` + + or globally: + $i ` git config --global user.email "` + $i$s `this will not work if you set it locally` +"#; + pub enum GitError { NotInstalled, VersionNotSupported { current: String, min: String }, @@ -136,6 +158,15 @@ pub enum GitConfigError { EmailNotFound, } +impl GitConfigError { + pub fn to_string(&self) -> String { + match self { + GitConfigError::NameNotFound => GIT_NAME_NOT_FOUND.to_string(), + GitConfigError::EmailNotFound => GIT_EMAIL_NOT_FOUND.to_string(), + } + } +} + /// Checks if the user has a git config. (user.name, user.email) pub fn check_git_config() -> Result<(), GitConfigError> { let mut command = std::process::Command::new("git"); diff --git a/src/config/mod.rs b/src/config/mod.rs index 170f5a6..d7b6fe9 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -35,36 +35,10 @@ pub async fn check_prerequisites() { // Check for git config match check_git_config() { Ok(_) => {} - Err(err) => match err { - git::GitConfigError::NameNotFound => { - let msg = r#" - $b$cr `error`: Git user.name not found. - - You can set it using the following command: - $i ` git config user.name "Your Name"` - - or globally: - $i ` git config --global user.name "Your Name"` - $i$s `this will not work if you set it locally` - "#; - view::printer(msg); - std::process::exit(1); - } - git::GitConfigError::EmailNotFound => { - let msg = r#" - $b$cr `error`: Git user.email not found. - - You can set it using the following command: - $i ` git config user.email "` - - or globally: - $i ` git config --global user.email "` - $i$s `this will not work if you set it locally` - "#; - view::printer(msg); - std::process::exit(1); - } - }, + Err(err) => { + view::printer(&err.to_string()); + std::process::exit(1); + } } // Check for a config file From c0759b2e64180fce3cd87306548d7092abdb45be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 2 Apr 2025 20:45:12 +0200 Subject: [PATCH 11/28] =?UTF-8?q?=F0=9F=92=A1=20Change=20git=20config=20ch?= =?UTF-8?q?eck=20function=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/git.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/git.rs b/src/config/git.rs index d57562d..f844f89 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -167,7 +167,7 @@ impl GitConfigError { } } -/// Checks if the user has a git config. (user.name, user.email) +/// Checks if the user has a valid git config. (user.name, user.email) pub fn check_git_config() -> Result<(), GitConfigError> { let mut command = std::process::Command::new("git"); command.args(["config", "user.name"]); From bcc2f334819f07dc5c580addfd6c7474246fec16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 2 Apr 2025 22:40:12 +0200 Subject: [PATCH 12/28] =?UTF-8?q?=E2=9C=A8=20Create=20input=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 1 + src/view/input.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/view/mod.rs | 7 +++++++ 3 files changed, 48 insertions(+) create mode 100644 src/view/input.rs diff --git a/src/main.rs b/src/main.rs index f277889..dad9357 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; mod utils; use utils::out; use utils::out::clear_screen; +use view::input; mod config; mod functions; diff --git a/src/view/input.rs b/src/view/input.rs new file mode 100644 index 0000000..d1e0829 --- /dev/null +++ b/src/view/input.rs @@ -0,0 +1,40 @@ +use crossterm::{ + event::{self, KeyCode}, + execute, + terminal::{self, ClearType}, + ExecutableCommand, +}; +use std::io::{self, Write}; + +pub fn read_password_input(prompt: &str) -> String { + let mut password = String::new(); + print!("{}: ", prompt); + io::stdout().flush().unwrap(); + + loop { + if let Ok(event) = event::read() { + match event { + event::Event::Key(event) => match event.code { + KeyCode::Enter => break, + KeyCode::Esc => return String::new(), + KeyCode::Backspace => { + password.pop(); + execute!(io::stdout(), terminal::Clear(ClearType::CurrentLine)).unwrap(); + print!("\r{}: {}", prompt, "*".repeat(password.len())); + io::stdout().flush().unwrap(); + } + _ => { + if let KeyCode::Char(c) = event.code { + password.push(c); + print!("\r{}: {}", prompt, "*".repeat(password.len())); + io::stdout().flush().unwrap(); + } + } + }, + _ => {} + } + } + } + write!(io::stdout(), "\n\r").unwrap(); + password +} diff --git a/src/view/mod.rs b/src/view/mod.rs index 76338a4..41294ac 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -6,6 +6,8 @@ use crossterm::{ }; use std::io::stdout; +pub mod input; + pub fn setup_view_controller() { enable_raw_mode().unwrap(); @@ -198,6 +200,11 @@ pub fn printer(content: &str) { disable_raw_mode().unwrap(); } +pub fn print(content: &str) { + let mut stdout = stdout(); + execute!(stdout, Print(content), Print("\n\r")).unwrap(); // Print the content and move to the next line +} + pub fn no_subcommand_error() { let eror_message = r#" $b$cr `error`: no subcommand provided From 134707b719f8612769c1c8fd762daf3e6f1e78ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Thu, 3 Apr 2025 13:42:53 +0200 Subject: [PATCH 13/28] =?UTF-8?q?=E2=9C=A8=20Create=20initial=20text=20inp?= =?UTF-8?q?ut=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 19 +++- src/view/input.rs | 257 ++++++++++++++++++++++++++++++++++++++++++---- src/view/mod.rs | 7 +- 3 files changed, 258 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index dad9357..e55f4b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,24 @@ enum SubCommand { async fn main() { let args = Cli::parse(); - view::setup_view_controller(); + let password = match input::password("Enter your GitHub password") { + Ok(password) => password, + Err(err) => { + match err { + input::ReturnType::Cancel => { + println!("Cancelled"); + return; + } + input::ReturnType::Exit => { + println!("Exiting"); + return; + } + } + } + }; + println!("Password: {}", password); + + return; config::check_prerequisites().await; let subcmd = match args.subcmd { diff --git a/src/view/input.rs b/src/view/input.rs index d1e0829..fa50df6 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -1,40 +1,257 @@ use crossterm::{ - event::{self, KeyCode}, + cursor::MoveToColumn, + event::{self, KeyCode, KeyModifiers}, execute, - terminal::{self, ClearType}, - ExecutableCommand, + terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, }; use std::io::{self, Write}; -pub fn read_password_input(prompt: &str) -> String { - let mut password = String::new(); +use crate::view::{print, printer}; + +pub enum ReturnType { + Cancel, + Exit, +} + +enum TextInputType { + Text, + Password, +} + +pub fn text(prompt: &str) -> Result { + get_user_text_input(prompt, TextInputType::Text) +} + +pub fn password(prompt: &str) -> Result { + get_user_text_input(prompt, TextInputType::Password) +} + +fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { + enable_raw_mode().unwrap(); + + let mut input = String::new(); + let mut cursor_position = 0; + print!("{}: ", prompt); io::stdout().flush().unwrap(); loop { if let Ok(event) = event::read() { match event { - event::Event::Key(event) => match event.code { - KeyCode::Enter => break, - KeyCode::Esc => return String::new(), - KeyCode::Backspace => { - password.pop(); - execute!(io::stdout(), terminal::Clear(ClearType::CurrentLine)).unwrap(); - print!("\r{}: {}", prompt, "*".repeat(password.len())); - io::stdout().flush().unwrap(); - } - _ => { - if let KeyCode::Char(c) = event.code { - password.push(c); - print!("\r{}: {}", prompt, "*".repeat(password.len())); + event::Event::Key(event) => match event.modifiers { + KeyModifiers::ALT => match event.code { + KeyCode::Char('b') => { + // Move to the beginning of the word + // If input type is password, go to the beginning of the line + match input_type { + TextInputType::Text => { + // Check if the previous character is a whitespace + if cursor_position > 0 + && input[cursor_position - 1..cursor_position] + .chars() + .next() + .unwrap() + .is_whitespace() + { + cursor_position -= 1; + } + + if let Some(pos) = + input[..cursor_position].rfind(char::is_whitespace) + { + cursor_position = pos + 1; + } else { + cursor_position = 0; + } + } + TextInputType::Password => { + cursor_position = 0; + } + } + + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('f') => { + match input_type { + TextInputType::Text => { + // Move to the end of the word + // Check if the cursor is on a whitespace + if cursor_position < input.len() + && input[cursor_position..] + .chars() + .next() + .unwrap() + .is_whitespace() + { + cursor_position += 1; + } + + if let Some(pos) = + input[cursor_position..].find(char::is_whitespace) + { + cursor_position += pos; + } else { + cursor_position = input.len(); + } + } + TextInputType::Password => { + cursor_position = input.len(); + } + } + + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + _ => {} + }, + KeyModifiers::CONTROL => match event.code { + KeyCode::Char('c') => { + write!(io::stdout(), "\n\r").unwrap(); + disable_raw_mode().unwrap(); + return Err(ReturnType::Exit); + } + KeyCode::Char('a') => { + // Move to the beginning of the line + cursor_position = 0; + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('e') => { + // Move to the end of the line + cursor_position = input.len(); + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('u') => { + // Remove all text before the cursor + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::CurrentLine) + ) + .unwrap(); + input.drain(..cursor_position); + cursor_position = 0; + + print!("{}: {}", prompt, input); io::stdout().flush().unwrap(); + execute!( + io::stdout(), + MoveToColumn((prompt.len() + 2 + cursor_position) as u16) + ) + .unwrap(); + } + KeyCode::Char('h') | KeyCode::Char('w') => { + match input_type { + TextInputType::Text => { + // Remove the word before the cursor + if let Some(pos) = + input[..cursor_position].rfind(char::is_whitespace) + { + input.drain(pos..cursor_position); + cursor_position = pos; + } else { + input.drain(..cursor_position); + cursor_position = 0; + } + } + TextInputType::Password => { + // Move to the beginning of the line + cursor_position = 0; + input.clear(); + } + } + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::CurrentLine) + ) + .unwrap(); + print!("{}: {}", prompt, input); + io::stdout().flush().unwrap(); + execute!( + io::stdout(), + MoveToColumn((prompt.len() + 2 + cursor_position) as u16) + ) + .unwrap(); + } + _ => {} + }, + KeyModifiers::NONE => match event.code { + KeyCode::Esc => { + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::CurrentLine) + ) + .unwrap(); + printer(&format!("{}: $cr `canceled`\n", prompt)); + + disable_raw_mode().unwrap(); + return Err(ReturnType::Cancel); + } + KeyCode::Enter => { + break; + } + KeyCode::Backspace => { + if cursor_position > 0 { + cursor_position -= 1; + input.remove(cursor_position); + + execute!(io::stdout(), terminal::Clear(ClearType::CurrentLine)) + .unwrap(); + print!("\r{}: {}", prompt, input); + io::stdout().flush().unwrap(); + + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } } - } + KeyCode::Left => { + if cursor_position > 0 { + cursor_position -= 1; + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Right => { + if cursor_position < input.len() { + cursor_position += 1; + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Char(c) => { + input.insert(cursor_position, c); + cursor_position += 1; + match input_type { + TextInputType::Text => { + print!("\r{}: {}", prompt, input); + } + TextInputType::Password => { + print!("\r{}: {}", prompt, "*".repeat(input.len())); + } + } + + io::stdout().flush().unwrap(); + + let column = prompt.len() + 2 + cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + _ => {} + }, + _ => {} }, _ => {} } + + io::stdout().flush().unwrap(); } } + write!(io::stdout(), "\n\r").unwrap(); - password + + disable_raw_mode().unwrap(); + Ok(input) } diff --git a/src/view/mod.rs b/src/view/mod.rs index 41294ac..790efa6 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -120,14 +120,14 @@ This function is used to print a string with special effects. The special effect - &>: tab (4 spaces) */ pub fn printer(content: &str) { + enable_raw_mode().unwrap(); + let chars = content.chars(); let n = chars.count(); let mut stdout = stdout(); let mut effects: Vec> = Vec::new(); - enable_raw_mode().unwrap(); - let mut i = 0; while i < n { let c = content.chars().nth(i).unwrap(); @@ -195,8 +195,7 @@ pub fn printer(content: &str) { i += 1; } - execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); - + // execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); disable_raw_mode().unwrap(); } From 828f411430ac2639e08ff26a03b991b93de5721b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Thu, 3 Apr 2025 19:16:44 +0200 Subject: [PATCH 14/28] =?UTF-8?q?=E2=9C=A8=20Input=20can=20use=20printer?= =?UTF-8?q?=20based=20formating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 22 +++++++-------- src/view/input.rs | 46 +++++++++++++++++++------------ src/view/mod.rs | 70 +++++++++++++++++++++++++++++++++++------------ 3 files changed, 90 insertions(+), 48 deletions(-) diff --git a/src/main.rs b/src/main.rs index e55f4b9..2f9cf39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,20 +42,18 @@ enum SubCommand { async fn main() { let args = Cli::parse(); - let password = match input::password("Enter your GitHub password") { + let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { Ok(password) => password, - Err(err) => { - match err { - input::ReturnType::Cancel => { - println!("Cancelled"); - return; - } - input::ReturnType::Exit => { - println!("Exiting"); - return; - } + Err(err) => match err { + input::ReturnType::Cancel => { + println!("Cancelled"); + return; } - } + input::ReturnType::Exit => { + println!("Exiting"); + return; + } + }, }; println!("Password: {}", password); diff --git a/src/view/input.rs b/src/view/input.rs index fa50df6..70b2b0f 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -6,7 +6,9 @@ use crossterm::{ }; use std::io::{self, Write}; -use crate::view::{print, printer}; +use crate::view::print; + +use super::PrintSize; pub enum ReturnType { Cancel, @@ -32,7 +34,7 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result Result { @@ -100,7 +102,7 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result {} @@ -114,13 +116,13 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { // Move to the beginning of the line cursor_position = 0; - let column = prompt.len() + 2 + cursor_position; + let column = prompt_length + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } KeyCode::Char('e') => { // Move to the end of the line cursor_position = input.len(); - let column = prompt.len() + 2 + cursor_position; + let column = prompt_length + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } KeyCode::Char('u') => { @@ -134,11 +136,11 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result Result Result Result { + print(format!("\r{}{}", prompt, input)); + } + TextInputType::Password => { + print(format!("\r{}{}", prompt, "*".repeat(input.len()))); + } + } io::stdout().flush().unwrap(); - let column = prompt.len() + 2 + cursor_position; + let column = prompt_length + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } } KeyCode::Left => { if cursor_position > 0 { cursor_position -= 1; - let column = prompt.len() + 2 + cursor_position; + let column = prompt_length + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } } KeyCode::Right => { if cursor_position < input.len() { cursor_position += 1; - let column = prompt.len() + 2 + cursor_position; + let column = prompt_length + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } } @@ -227,16 +237,16 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { - print!("\r{}: {}", prompt, input); + print(format!("\r{}{}", prompt, input)); } TextInputType::Password => { - print!("\r{}: {}", prompt, "*".repeat(input.len())); + print(format!("\r{}{}", prompt, "*".repeat(input.len()))); } } io::stdout().flush().unwrap(); - let column = prompt.len() + 2 + cursor_position; + let column = prompt_length + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } _ => {} diff --git a/src/view/mod.rs b/src/view/mod.rs index 790efa6..55ee401 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -8,17 +8,6 @@ use std::io::stdout; pub mod input; -pub fn setup_view_controller() { - enable_raw_mode().unwrap(); - - let default_panic = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |panic_info| { - disable_raw_mode().unwrap(); - println!(); - default_panic(panic_info); - })); -} - pub fn clean_up() { disable_raw_mode().unwrap(); } @@ -95,6 +84,11 @@ fn set_new_effects(stdout: &mut std::io::Stdout, effects: &Vec } } +pub struct PrintSize { + pub cols: usize, + pub rows: usize, +} + /** This function is used to print a string with special effects. The special effects are defined by the following syntax: - $b: bold @@ -119,9 +113,43 @@ This function is used to print a string with special effects. The special effect - &>: tab (4 spaces) */ -pub fn printer(content: &str) { +pub fn printer(content: impl AsRef) -> PrintSize { enable_raw_mode().unwrap(); + let size = print(content); + disable_raw_mode().unwrap(); + + size +} + +/** Print the content with correct formatting, but without going into raw mode +This function is used to print a string with special effects. The special effects are defined by the following syntax: +- $b: bold +- $i: italic +- $u: underline + +- $cr: red color +- $cg: green color +- $cb: blue color +- $cy: yellow color +- $cm: magenta color +- $cc: cyan color +- $cw: white color +- $br: background red color +- $bg: background green color +- $bb: background blue color +- $by: background yellow color +- $bm: background magenta color +- $bc: background cyan color +- $bw: background white color + +- &>: tab (4 spaces) +*/ +pub fn print(content: impl AsRef) -> PrintSize { + let mut size = PrintSize { cols: 0, rows: 0 }; + let mut current_width: usize = 0; + + let content = content.as_ref(); let chars = content.chars(); let n = chars.count(); let mut stdout = stdout(); @@ -162,11 +190,17 @@ pub fn printer(content: &str) { // Print a new line and clear the spaces if c == '\n' { + if current_width > size.cols { + size.cols = current_width; + size.rows += 1; + } + execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); i += 1; while i < n && content.chars().nth(i).unwrap() == ' ' { i += 1; + size.rows += 1; } continue; @@ -190,18 +224,18 @@ pub fn printer(content: &str) { continue; } + current_width += 1; execute!(stdout, Print(c)).unwrap(); i += 1; } - // execute!(stdout, Print('\n'), MoveToNextLine(1)).unwrap(); - disable_raw_mode().unwrap(); -} + if current_width > size.cols { + size.cols = current_width; + size.rows += 1; + } -pub fn print(content: &str) { - let mut stdout = stdout(); - execute!(stdout, Print(content), Print("\n\r")).unwrap(); // Print the content and move to the next line + size } pub fn no_subcommand_error() { From 7a43cc1929fc329041edc78b4d4b42414bfbbfab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Mon, 7 Apr 2025 20:32:55 +0200 Subject: [PATCH 15/28] =?UTF-8?q?=E2=9C=A8=20Initial=20list=20input=20impl?= =?UTF-8?q?ementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 55 ++++++++++++----- src/view/input.rs | 154 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 190 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2f9cf39..19cb22b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,20 +42,47 @@ enum SubCommand { async fn main() { let args = Cli::parse(); - let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { - Ok(password) => password, - Err(err) => match err { - input::ReturnType::Cancel => { - println!("Cancelled"); - return; - } - input::ReturnType::Exit => { - println!("Exiting"); - return; - } - }, - }; - println!("Password: {}", password); + // let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { + // Ok(password) => password, + // Err(err) => match err { + // input::ReturnType::Cancel => { + // println!("Cancelled"); + // return; + // } + // input::ReturnType::Exit => { + // println!("Exiting"); + // return; + // } + // }, + // }; + // println!("Password: {}", password); + + let choice = input::list( + "$cg `>` $cw `Enter your GitHub password: `", + vec![ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + "Option 7", + "Option 8", + "Option 9", + "Option 10", + "Option 11", + "Option 12", + "Option 13", + "Option 14", + "Option 15", + "Option 16", + "Option 17", + "Option 18", + "Option 19", + "Option 20", + ], + ).unwrap_or(""); + println!("Choice: {:?}", choice); return; config::check_prerequisites().await; diff --git a/src/view/input.rs b/src/view/input.rs index 70b2b0f..67fd053 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -1,14 +1,17 @@ use crossterm::{ - cursor::MoveToColumn, + cursor::{MoveDown, MoveToColumn, MoveUp}, event::{self, KeyCode, KeyModifiers}, execute, terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, }; -use std::io::{self, Write}; +use std::{ + fmt::Display, + io::{self, Write}, +}; -use crate::view::print; +use super::{print, PrintSize}; -use super::PrintSize; +const MAX_ROWS: usize = 12; pub enum ReturnType { Cancel, @@ -28,13 +31,154 @@ pub fn password(prompt: &str) -> Result { get_user_text_input(prompt, TextInputType::Password) } +pub fn list(prompt: &str, items: Vec) -> Result +where + T: Display + Clone, +{ + enable_raw_mode().unwrap(); + let mut selected = 0; + let mut offset = 0; + + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}\n", prompt)); + + let total_rows = items.len(); + let available_rows = terminal::size().unwrap().1 as usize - 1; // 1 for the prompt + + let mut usable_rows = if total_rows > available_rows { + available_rows + } else { + total_rows + }; + if usable_rows > MAX_ROWS { + usable_rows = MAX_ROWS; + } + + for i in offset..usable_rows + offset { + if i == selected { + print(format!("$cc `>` {}\n", items[i])); + } else { + if i == usable_rows + offset - 1 && total_rows > usable_rows { + print(format!("▼ {}\n", items[i])); + } else { + print(format!(" {}\n", items[i])); + } + } + } + + execute!( + io::stdout(), + MoveUp((usable_rows + 1) as u16), + MoveToColumn((prompt_length) as u16) + ) + .unwrap(); + + loop { + if let Ok(event) = event::read() { + match event { + event::Event::Key(event) => match event.code { + KeyCode::Esc => { + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::CurrentLine) + ) + .unwrap(); + print(format!("{}$cr `canceled`\n", prompt)); + + disable_raw_mode().unwrap(); + return Err(ReturnType::Cancel); + } + KeyCode::Enter => { + break; + } + KeyCode::Up => { + if selected > 0 { + selected -= 1; + } + } + KeyCode::Down => { + if selected < total_rows - 1 { + selected += 1; + } + } + KeyCode::Char('c') => { + if event.modifiers == KeyModifiers::CONTROL { + write!(io::stdout(), "\n\r").unwrap(); + disable_raw_mode().unwrap(); + return Err(ReturnType::Exit); + } + } + _ => {} + }, + _ => {} + } + + let diff = (selected + 1) as isize - (usable_rows / 2) as isize; + if diff <= 0 { + offset = 0; + } else if diff > 0 { + offset = diff as usize; + } + if offset > total_rows - usable_rows { + offset = total_rows - usable_rows; + } + + // Render the prompt + execute!( + io::stdout(), + MoveToColumn(0), + terminal::Clear(ClearType::FromCursorDown) + ) + .unwrap(); + print(format!("{} {}", prompt, diff)); + execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); + + // Render the list + for i in offset..usable_rows + offset { + if i == selected { + print(format!("$cc `>` {}", items[i])); + } else { + if i == usable_rows + offset - 1 && total_rows > usable_rows + offset { + print(format!("▼ {}", items[i])); + } else if offset > 0 && i == offset { + print(format!("▲ {}", items[i])); + } else { + print(format!(" {}", items[i])); + } + } + execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); + } + + // Go back to the prompt + execute!( + io::stdout(), + MoveUp((usable_rows + 1) as u16), + MoveToColumn((prompt_length) as u16) + ) + .unwrap(); + + io::stdout().flush().unwrap(); + } + } + + disable_raw_mode().unwrap(); + + Ok(items[selected].clone()) +} + fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { enable_raw_mode().unwrap(); let mut input = String::new(); let mut cursor_position = 0; - let PrintSize{cols: prompt_length, rows: _} = print(format!("{}", prompt)); + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}", prompt)); io::stdout().flush().unwrap(); loop { From 084b8a436bb56d1d841c479b411f38c388a4ad5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Mon, 7 Apr 2025 21:43:56 +0200 Subject: [PATCH 16/28] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20text=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 80 ++++++++++++++++++++++---------------------- src/view/input.rs | 84 ++++++++++++++++++++++++++++------------------- 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/src/main.rs b/src/main.rs index 19cb22b..a985728 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,47 +42,47 @@ enum SubCommand { async fn main() { let args = Cli::parse(); - // let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { - // Ok(password) => password, - // Err(err) => match err { - // input::ReturnType::Cancel => { - // println!("Cancelled"); - // return; - // } - // input::ReturnType::Exit => { - // println!("Exiting"); - // return; - // } - // }, - // }; - // println!("Password: {}", password); + let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { + Ok(password) => password, + Err(err) => match err { + input::ReturnType::Cancel => { + println!("Cancelled"); + return; + } + input::ReturnType::Exit => { + println!("Exiting"); + return; + } + }, + }; + println!("Password: {}", password); - let choice = input::list( - "$cg `>` $cw `Enter your GitHub password: `", - vec![ - "Option 1", - "Option 2", - "Option 3", - "Option 4", - "Option 5", - "Option 6", - "Option 7", - "Option 8", - "Option 9", - "Option 10", - "Option 11", - "Option 12", - "Option 13", - "Option 14", - "Option 15", - "Option 16", - "Option 17", - "Option 18", - "Option 19", - "Option 20", - ], - ).unwrap_or(""); - println!("Choice: {:?}", choice); + // let choice = input::list( + // "$cg `>` $cw `Enter your GitHub password: `", + // vec![ + // "Option 1", + // "Option 2", + // "Option 3", + // "Option 4", + // "Option 5", + // "Option 6", + // "Option 7", + // "Option 8", + // "Option 9", + // "Option 10", + // "Option 11", + // "Option 12", + // "Option 13", + // "Option 14", + // "Option 15", + // "Option 16", + // "Option 17", + // "Option 18", + // "Option 19", + // "Option 20", + // ], + // ).unwrap_or(""); + // println!("Choice: {:?}", choice); return; config::check_prerequisites().await; diff --git a/src/view/input.rs b/src/view/input.rs index 67fd053..e4f9fc7 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -24,11 +24,23 @@ enum TextInputType { } pub fn text(prompt: &str) -> Result { - get_user_text_input(prompt, TextInputType::Text) + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}", prompt)); + io::stdout().flush().unwrap(); + + get_user_text_input(prompt_length, TextInputType::Text) } pub fn password(prompt: &str) -> Result { - get_user_text_input(prompt, TextInputType::Password) + let PrintSize { + cols: prompt_length, + rows: _, + } = print(format!("{}", prompt)); + io::stdout().flush().unwrap(); + + get_user_text_input(prompt_length, TextInputType::Password) } pub fn list(prompt: &str, items: Vec) -> Result @@ -169,18 +181,12 @@ where Ok(items[selected].clone()) } -fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { +fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { enable_raw_mode().unwrap(); let mut input = String::new(); let mut cursor_position = 0; - let PrintSize { - cols: prompt_length, - rows: _, - } = print(format!("{}", prompt)); - io::stdout().flush().unwrap(); - loop { if let Ok(event) = event::read() { match event { @@ -215,7 +221,7 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { @@ -246,7 +252,7 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result {} @@ -260,31 +266,31 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { // Move to the beginning of the line cursor_position = 0; - let column = prompt_length + cursor_position; + let column = position + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } KeyCode::Char('e') => { // Move to the end of the line cursor_position = input.len(); - let column = prompt_length + cursor_position; + let column = position + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } KeyCode::Char('u') => { // Remove all text before the cursor execute!( io::stdout(), - MoveToColumn(0), - terminal::Clear(ClearType::CurrentLine) + MoveToColumn(position as u16), + terminal::Clear(ClearType::UntilNewLine) ) .unwrap(); input.drain(..cursor_position); cursor_position = 0; - print(format!("{}{}", prompt, input)); + print(format!("{}", input)); io::stdout().flush().unwrap(); execute!( io::stdout(), - MoveToColumn((prompt_length + cursor_position) as u16) + MoveToColumn((position + cursor_position) as u16) ) .unwrap(); } @@ -310,15 +316,15 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result Result { execute!( io::stdout(), - MoveToColumn(0), - terminal::Clear(ClearType::CurrentLine) + MoveToColumn(position as u16), + terminal::Clear(ClearType::UntilNewLine) ) .unwrap(); - print(format!("{}$cr `canceled`\n", prompt)); + print("$cr `canceled`\n"); disable_raw_mode().unwrap(); return Err(ReturnType::Cancel); @@ -345,52 +351,62 @@ fn get_user_text_input(prompt: &str, input_type: TextInputType) -> Result { - print(format!("\r{}{}", prompt, input)); + print(format!("{}", input)); } TextInputType::Password => { - print(format!("\r{}{}", prompt, "*".repeat(input.len()))); + print(format!("{}", "*".repeat(input.len()))); } } io::stdout().flush().unwrap(); - let column = prompt_length + cursor_position; + let column = position + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } } KeyCode::Left => { if cursor_position > 0 { cursor_position -= 1; - let column = prompt_length + cursor_position; + let column = position + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } } KeyCode::Right => { if cursor_position < input.len() { cursor_position += 1; - let column = prompt_length + cursor_position; + let column = position + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } } KeyCode::Char(c) => { input.insert(cursor_position, c); cursor_position += 1; + execute!( + io::stdout(), + MoveToColumn(position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); match input_type { TextInputType::Text => { - print(format!("\r{}{}", prompt, input)); + print(format!("{}", input)); } TextInputType::Password => { - print(format!("\r{}{}", prompt, "*".repeat(input.len()))); + print(format!("{}", "*".repeat(input.len()))); } } io::stdout().flush().unwrap(); - let column = prompt_length + cursor_position; + let column = position + cursor_position; execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); } _ => {} From a1d8e2a66571a08da3c1ae76e8f52b2f183f29da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Tue, 8 Apr 2025 15:31:43 +0200 Subject: [PATCH 17/28] =?UTF-8?q?=F0=9F=94=A8=20Move=20input=20into=20a=20?= =?UTF-8?q?separate,=20self=20contained=20struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/input.rs | 435 +++++++++++++++++++++++++--------------------- 1 file changed, 234 insertions(+), 201 deletions(-) diff --git a/src/view/input.rs b/src/view/input.rs index e4f9fc7..599d2ac 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -1,6 +1,6 @@ use crossterm::{ cursor::{MoveDown, MoveToColumn, MoveUp}, - event::{self, KeyCode, KeyModifiers}, + event::{self, KeyCode, KeyEvent, KeyModifiers}, execute, terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, }; @@ -23,6 +23,234 @@ enum TextInputType { Password, } +/** +## A struct to handle text input +#### It handles the input, cursor position, and input type (text or password), it only handles input (special actions (like Ctrl+C) are handled in the main function) +*/ +struct TextInput { + input: String, + position: usize, + input_type: TextInputType, + cursor_position: usize, +} + +impl TextInput { + fn new(position: usize, input_type: TextInputType) -> Self { + Self { + input: String::new(), + position, + input_type, + cursor_position: 0, + } + } + + fn handle_event(&mut self, event: KeyEvent) { + match event.modifiers { + KeyModifiers::ALT => match event.code { + KeyCode::Char('b') => { + // Move to the beginning of the word + // If input type is password, go to the beginning of the line + match self.input_type { + TextInputType::Text => { + // Check if the previous character is a whitespace + if self.cursor_position > 0 + && self.input[self.cursor_position - 1..self.cursor_position] + .chars() + .next() + .unwrap() + .is_whitespace() + { + self.cursor_position -= 1; + } + + if let Some(pos) = + self.input[..self.cursor_position].rfind(char::is_whitespace) + { + self.cursor_position = pos + 1; + } else { + self.cursor_position = 0; + } + } + TextInputType::Password => { + self.cursor_position = 0; + } + } + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('f') => { + match self.input_type { + TextInputType::Text => { + // Move to the end of the word + // Check if the cursor is on a whitespace + if self.cursor_position < self.input.len() + && self.input[self.cursor_position..] + .chars() + .next() + .unwrap() + .is_whitespace() + { + self.cursor_position += 1; + } + + if let Some(pos) = + self.input[self.cursor_position..].find(char::is_whitespace) + { + self.cursor_position += pos; + } else { + self.cursor_position = self.input.len(); + } + } + TextInputType::Password => { + self.cursor_position = self.input.len(); + } + } + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + _ => {} + }, + KeyModifiers::CONTROL => match event.code { + KeyCode::Char('a') => { + // Move to the beginning of the line + self.cursor_position = 0; + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('e') => { + // Move to the end of the line + self.cursor_position = self.input.len(); + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + KeyCode::Char('u') => { + // Remove all text before the cursor + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + self.input.drain(..self.cursor_position); + self.cursor_position = 0; + + print(format!("{}", self.input)); + io::stdout().flush().unwrap(); + execute!( + io::stdout(), + MoveToColumn((self.position + self.cursor_position) as u16) + ) + .unwrap(); + } + KeyCode::Char('h') | KeyCode::Char('w') => { + match self.input_type { + TextInputType::Text => { + // Remove the word before the cursor + if let Some(pos) = + self.input[..self.cursor_position].rfind(char::is_whitespace) + { + self.input.drain(pos..self.cursor_position); + self.cursor_position = pos; + } else { + self.input.drain(..self.cursor_position); + self.cursor_position = 0; + } + } + TextInputType::Password => { + // Move to the beginning of the line + self.cursor_position = 0; + self.input.clear(); + } + } + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + print(format!("{}", self.input)); + io::stdout().flush().unwrap(); + execute!( + io::stdout(), + MoveToColumn((self.position + self.cursor_position) as u16) + ) + .unwrap(); + } + _ => {} + }, + KeyModifiers::NONE => match event.code { + KeyCode::Backspace => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + self.input.remove(self.cursor_position); + + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + + match self.input_type { + TextInputType::Text => { + print(format!("{}", self.input)); + } + TextInputType::Password => { + print(format!("{}", "*".repeat(self.input.len()))); + } + } + io::stdout().flush().unwrap(); + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Right => { + if self.cursor_position < self.input.len() { + self.cursor_position += 1; + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + } + KeyCode::Char(c) => { + self.input.insert(self.cursor_position, c); + self.cursor_position += 1; + execute!( + io::stdout(), + MoveToColumn(self.position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + match self.input_type { + TextInputType::Text => { + print(format!("{}", self.input)); + } + TextInputType::Password => { + print(format!("{}", "*".repeat(self.input.len()))); + } + } + + io::stdout().flush().unwrap(); + + let column = self.position + self.cursor_position; + execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); + } + _ => {} + }, + _ => {} + } + } +} + pub fn text(prompt: &str) -> Result { let PrintSize { cols: prompt_length, @@ -184,151 +412,19 @@ where fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { enable_raw_mode().unwrap(); - let mut input = String::new(); - let mut cursor_position = 0; + let mut text_input = TextInput::new(position, input_type); loop { if let Ok(event) = event::read() { match event { event::Event::Key(event) => match event.modifiers { - KeyModifiers::ALT => match event.code { - KeyCode::Char('b') => { - // Move to the beginning of the word - // If input type is password, go to the beginning of the line - match input_type { - TextInputType::Text => { - // Check if the previous character is a whitespace - if cursor_position > 0 - && input[cursor_position - 1..cursor_position] - .chars() - .next() - .unwrap() - .is_whitespace() - { - cursor_position -= 1; - } - - if let Some(pos) = - input[..cursor_position].rfind(char::is_whitespace) - { - cursor_position = pos + 1; - } else { - cursor_position = 0; - } - } - TextInputType::Password => { - cursor_position = 0; - } - } - - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - KeyCode::Char('f') => { - match input_type { - TextInputType::Text => { - // Move to the end of the word - // Check if the cursor is on a whitespace - if cursor_position < input.len() - && input[cursor_position..] - .chars() - .next() - .unwrap() - .is_whitespace() - { - cursor_position += 1; - } - - if let Some(pos) = - input[cursor_position..].find(char::is_whitespace) - { - cursor_position += pos; - } else { - cursor_position = input.len(); - } - } - TextInputType::Password => { - cursor_position = input.len(); - } - } - - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - _ => {} - }, KeyModifiers::CONTROL => match event.code { KeyCode::Char('c') => { write!(io::stdout(), "\n\r").unwrap(); disable_raw_mode().unwrap(); return Err(ReturnType::Exit); } - KeyCode::Char('a') => { - // Move to the beginning of the line - cursor_position = 0; - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - KeyCode::Char('e') => { - // Move to the end of the line - cursor_position = input.len(); - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - KeyCode::Char('u') => { - // Remove all text before the cursor - execute!( - io::stdout(), - MoveToColumn(position as u16), - terminal::Clear(ClearType::UntilNewLine) - ) - .unwrap(); - input.drain(..cursor_position); - cursor_position = 0; - - print(format!("{}", input)); - io::stdout().flush().unwrap(); - execute!( - io::stdout(), - MoveToColumn((position + cursor_position) as u16) - ) - .unwrap(); - } - KeyCode::Char('h') | KeyCode::Char('w') => { - match input_type { - TextInputType::Text => { - // Remove the word before the cursor - if let Some(pos) = - input[..cursor_position].rfind(char::is_whitespace) - { - input.drain(pos..cursor_position); - cursor_position = pos; - } else { - input.drain(..cursor_position); - cursor_position = 0; - } - } - TextInputType::Password => { - // Move to the beginning of the line - cursor_position = 0; - input.clear(); - } - } - execute!( - io::stdout(), - MoveToColumn(position as u16), - terminal::Clear(ClearType::UntilNewLine) - ) - .unwrap(); - print(format!("{}", input)); - io::stdout().flush().unwrap(); - execute!( - io::stdout(), - MoveToColumn((position + cursor_position) as u16) - ) - .unwrap(); - } - _ => {} + _ => text_input.handle_event(event), }, KeyModifiers::NONE => match event.code { KeyCode::Esc => { @@ -346,72 +442,9 @@ fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { break; } - KeyCode::Backspace => { - if cursor_position > 0 { - cursor_position -= 1; - input.remove(cursor_position); - - execute!( - io::stdout(), - MoveToColumn(position as u16), - terminal::Clear(ClearType::UntilNewLine) - ) - .unwrap(); - - match input_type { - TextInputType::Text => { - print(format!("{}", input)); - } - TextInputType::Password => { - print(format!("{}", "*".repeat(input.len()))); - } - } - io::stdout().flush().unwrap(); - - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - } - KeyCode::Left => { - if cursor_position > 0 { - cursor_position -= 1; - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - } - KeyCode::Right => { - if cursor_position < input.len() { - cursor_position += 1; - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - } - KeyCode::Char(c) => { - input.insert(cursor_position, c); - cursor_position += 1; - execute!( - io::stdout(), - MoveToColumn(position as u16), - terminal::Clear(ClearType::UntilNewLine) - ) - .unwrap(); - match input_type { - TextInputType::Text => { - print(format!("{}", input)); - } - TextInputType::Password => { - print(format!("{}", "*".repeat(input.len()))); - } - } - - io::stdout().flush().unwrap(); - - let column = position + cursor_position; - execute!(io::stdout(), MoveToColumn(column as u16)).unwrap(); - } - _ => {} + _ => text_input.handle_event(event), }, - _ => {} + _ => text_input.handle_event(event), }, _ => {} } @@ -423,5 +456,5 @@ fn get_user_text_input(position: usize, input_type: TextInputType) -> Result Date: Tue, 8 Apr 2025 15:38:28 +0200 Subject: [PATCH 18/28] =?UTF-8?q?=E2=9C=A8=20Add=20text=20box=20to=20list?= =?UTF-8?q?=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 80 +++++++++++++++++++++++------------------------ src/view/input.rs | 13 +++++--- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/main.rs b/src/main.rs index a985728..19cb22b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,47 +42,47 @@ enum SubCommand { async fn main() { let args = Cli::parse(); - let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { - Ok(password) => password, - Err(err) => match err { - input::ReturnType::Cancel => { - println!("Cancelled"); - return; - } - input::ReturnType::Exit => { - println!("Exiting"); - return; - } - }, - }; - println!("Password: {}", password); + // let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { + // Ok(password) => password, + // Err(err) => match err { + // input::ReturnType::Cancel => { + // println!("Cancelled"); + // return; + // } + // input::ReturnType::Exit => { + // println!("Exiting"); + // return; + // } + // }, + // }; + // println!("Password: {}", password); - // let choice = input::list( - // "$cg `>` $cw `Enter your GitHub password: `", - // vec![ - // "Option 1", - // "Option 2", - // "Option 3", - // "Option 4", - // "Option 5", - // "Option 6", - // "Option 7", - // "Option 8", - // "Option 9", - // "Option 10", - // "Option 11", - // "Option 12", - // "Option 13", - // "Option 14", - // "Option 15", - // "Option 16", - // "Option 17", - // "Option 18", - // "Option 19", - // "Option 20", - // ], - // ).unwrap_or(""); - // println!("Choice: {:?}", choice); + let choice = input::list( + "$cg `>` $cw `Enter your GitHub password: `", + vec![ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + "Option 7", + "Option 8", + "Option 9", + "Option 10", + "Option 11", + "Option 12", + "Option 13", + "Option 14", + "Option 15", + "Option 16", + "Option 17", + "Option 18", + "Option 19", + "Option 20", + ], + ).unwrap_or(""); + println!("Choice: {:?}", choice); return; config::check_prerequisites().await; diff --git a/src/view/input.rs b/src/view/input.rs index 599d2ac..27b632d 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -284,6 +284,7 @@ where rows: _, } = print(format!("{}\n", prompt)); + let mut text_input = TextInput::new(prompt_length, TextInputType::Text); let total_rows = items.len(); let available_rows = terminal::size().unwrap().1 as usize - 1; // 1 for the prompt @@ -344,14 +345,16 @@ where selected += 1; } } - KeyCode::Char('c') => { - if event.modifiers == KeyModifiers::CONTROL { + KeyCode::Char(c) => { + if event.modifiers == KeyModifiers::CONTROL && c == 'c' { write!(io::stdout(), "\n\r").unwrap(); disable_raw_mode().unwrap(); return Err(ReturnType::Exit); } + + text_input.handle_event(event); } - _ => {} + _ => text_input.handle_event(event), }, _ => {} } @@ -373,7 +376,7 @@ where terminal::Clear(ClearType::FromCursorDown) ) .unwrap(); - print(format!("{} {}", prompt, diff)); + print(format!("{}{}", prompt, text_input.input)); execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); // Render the list @@ -396,7 +399,7 @@ where execute!( io::stdout(), MoveUp((usable_rows + 1) as u16), - MoveToColumn((prompt_length) as u16) + MoveToColumn((prompt_length + text_input.cursor_position) as u16) ) .unwrap(); From 9326dac66566f905fea3ce82ff4de136f8acaf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Thu, 8 May 2025 10:51:13 +0200 Subject: [PATCH 19/28] =?UTF-8?q?=F0=9F=94=A8=20Display=20input=20at=20the?= =?UTF-8?q?=20end=20of=20the=20prompt=20after=20completion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 2 +- src/view/input.rs | 127 +++++++++++++++++++++++++++++----------------- 2 files changed, 82 insertions(+), 47 deletions(-) diff --git a/src/main.rs b/src/main.rs index 19cb22b..c75c6c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ enum SubCommand { async fn main() { let args = Cli::parse(); - // let password = match input::text("$cg `>` $cw `Enter your GitHub password: `") { + // let password = match input::password("$cg `>` $cw `Enter your GitHub password: `") { // Ok(password) => password, // Err(err) => match err { // input::ReturnType::Cancel => { diff --git a/src/view/input.rs b/src/view/input.rs index 27b632d..67bfe6c 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -4,6 +4,7 @@ use crossterm::{ execute, terminal::{self, disable_raw_mode, enable_raw_mode, ClearType}, }; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use std::{ fmt::Display, io::{self, Write}, @@ -18,6 +19,7 @@ pub enum ReturnType { Exit, } +#[derive(PartialEq, Clone, Copy)] enum TextInputType { Text, Password, @@ -275,9 +277,17 @@ pub fn list(prompt: &str, items: Vec) -> Result where T: Display + Clone, { + use fuzzy_matcher::skim::SkimMatcherV2; + + if items.is_empty() { + return Err(ReturnType::Cancel); + } + enable_raw_mode().unwrap(); + let mut selected = 0; - let mut offset = 0; + let matcher = SkimMatcherV2::default(); + let mut render_buffer = items.clone(); let PrintSize { cols: prompt_length, @@ -285,36 +295,32 @@ where } = print(format!("{}\n", prompt)); let mut text_input = TextInput::new(prompt_length, TextInputType::Text); - let total_rows = items.len(); - let available_rows = terminal::size().unwrap().1 as usize - 1; // 1 for the prompt - - let mut usable_rows = if total_rows > available_rows { - available_rows - } else { - total_rows - }; - if usable_rows > MAX_ROWS { - usable_rows = MAX_ROWS; + let mut available_rows = terminal::size().unwrap().1 as usize - 1; // 1 for the prompt + if available_rows > MAX_ROWS { + available_rows = MAX_ROWS; } - for i in offset..usable_rows + offset { - if i == selected { - print(format!("$cc `>` {}\n", items[i])); - } else { - if i == usable_rows + offset - 1 && total_rows > usable_rows { - print(format!("▼ {}\n", items[i])); - } else { - print(format!(" {}\n", items[i])); - } - } + render_buffer.truncate(available_rows); + let usable_rows = render_buffer.len(); + + for _ in 0..render_buffer.len() { + print(format!("\n\r")); } + execute!( + io::stdout(), + MoveUp(render_buffer.len() as u16), + MoveToColumn(0) + ) + .unwrap(); + let rendered = render_list(&render_buffer, 0, &matcher, text_input.input.clone()); execute!( io::stdout(), - MoveUp((usable_rows + 1) as u16), + MoveUp((rendered + 1) as u16), MoveToColumn((prompt_length) as u16) ) .unwrap(); + io::stdout().flush().unwrap(); loop { if let Ok(event) = event::read() { @@ -341,7 +347,7 @@ where } } KeyCode::Down => { - if selected < total_rows - 1 { + if selected < render_buffer.len() - 1 { selected += 1; } } @@ -359,16 +365,6 @@ where _ => {} } - let diff = (selected + 1) as isize - (usable_rows / 2) as isize; - if diff <= 0 { - offset = 0; - } else if diff > 0 { - offset = diff as usize; - } - if offset > total_rows - usable_rows { - offset = total_rows - usable_rows; - } - // Render the prompt execute!( io::stdout(), @@ -380,25 +376,36 @@ where execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); // Render the list - for i in offset..usable_rows + offset { - if i == selected { - print(format!("$cc `>` {}", items[i])); - } else { - if i == usable_rows + offset - 1 && total_rows > usable_rows + offset { - print(format!("▼ {}", items[i])); - } else if offset > 0 && i == offset { - print(format!("▲ {}", items[i])); - } else { - print(format!(" {}", items[i])); + let mut new_selected = 0; + render_buffer.clear(); + for i in 0..items.len() { + if let Some(matched) = matcher.fuzzy_match(&items[i].to_string(), &text_input.input) + { + render_buffer.push(items[i].clone()); + if i == selected { + new_selected = render_buffer.len() - 1; } } - execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); } + // let diff = (selected + 1) as isize - (usable_rows / 2) as isize; + // let mut offset = 0; + // if diff <= 0 { + // offset = 0; + // } else if diff > 0 { + // offset = diff as usize; + // } + // if offset > total_rows - usable_rows { + // offset = total_rows - usable_rows; + // } + + let rendered = + render_list(&render_buffer, selected, &matcher, text_input.input.clone()); + // Go back to the prompt execute!( io::stdout(), - MoveUp((usable_rows + 1) as u16), + MoveUp((rendered + 1) as u16), MoveToColumn((prompt_length + text_input.cursor_position) as u16) ) .unwrap(); @@ -409,7 +416,24 @@ where disable_raw_mode().unwrap(); - Ok(items[selected].clone()) + Ok(render_buffer[selected].clone()) +} + +/// Render the list of items +/// This function assumes that the items are already filtered, and correctly offset, and uses the matcher to color the items +fn render_list(items: &Vec, selected: usize, matcher: &SkimMatcherV2, input: String) -> usize +where + T: Display + Clone, +{ + for i in 0..items.len() { + if i == selected { + print("> "); + } + + execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); + } + + items.len() } fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { @@ -443,6 +467,17 @@ fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { + execute!( + io::stdout(), + MoveToColumn(position as u16), + terminal::Clear(ClearType::UntilNewLine) + ) + .unwrap(); + if input_type == TextInputType::Password { + print(format!("$cw$b `{}`", "*".repeat(text_input.input.len()))); + } else { + print(format!("$cw$b `{}`", text_input.input)); + } break; } _ => text_input.handle_event(event), From e27f8b05af07e3f59550d812c6888102cc2d71e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Wed, 17 Sep 2025 21:06:43 +0200 Subject: [PATCH 20/28] =?UTF-8?q?=E2=9C=A8=20Finalize=20initial=20list=20i?= =?UTF-8?q?nput=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 3 +- src/view/input.rs | 170 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 132 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index c75c6c6..4006ff8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,7 +81,8 @@ async fn main() { "Option 19", "Option 20", ], - ).unwrap_or(""); + ) + .unwrap_or(""); println!("Choice: {:?}", choice); return; diff --git a/src/view/input.rs b/src/view/input.rs index 67bfe6c..0c6a352 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use std::{ - fmt::Display, + fmt::{write, Debug, Display}, io::{self, Write}, }; @@ -253,6 +253,18 @@ impl TextInput { } } +struct ListValue { + key: usize, + value: T, + matched: bool, +} + +impl Display for ListValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value) + } +} + pub fn text(prompt: &str) -> Result { let PrintSize { cols: prompt_length, @@ -283,11 +295,20 @@ where return Err(ReturnType::Cancel); } + let mut kv_items: Vec> = items + .iter() + .enumerate() + .map(|(i, x)| ListValue { + key: i, + value: x.clone(), + matched: true, + }) + .collect(); + enable_raw_mode().unwrap(); let mut selected = 0; let matcher = SkimMatcherV2::default(); - let mut render_buffer = items.clone(); let PrintSize { cols: prompt_length, @@ -300,19 +321,26 @@ where available_rows = MAX_ROWS; } - render_buffer.truncate(available_rows); - let usable_rows = render_buffer.len(); + let usable_rows; + if kv_items.len() < available_rows { + usable_rows = kv_items.len() + } else { + usable_rows = available_rows; + } - for _ in 0..render_buffer.len() { + for _ in 0..usable_rows { print(format!("\n\r")); } - execute!( - io::stdout(), - MoveUp(render_buffer.len() as u16), - MoveToColumn(0) - ) - .unwrap(); - let rendered = render_list(&render_buffer, 0, &matcher, text_input.input.clone()); + execute!(io::stdout(), MoveUp(usable_rows as u16), MoveToColumn(0)).unwrap(); + + let rendered = render_list( + &kv_items, + 0, + 0, + usable_rows, + &matcher, + text_input.input.clone(), + ); execute!( io::stdout(), @@ -345,10 +373,18 @@ where if selected > 0 { selected -= 1; } + + while selected > 0 && !kv_items[selected].matched { + selected -= 1; + } } KeyCode::Down => { - if selected < render_buffer.len() - 1 { + if selected < kv_items.len() - 1 { selected += 1; + + while selected < kv_items.len() - 1 && !kv_items[selected].matched { + selected += 1; + } } } KeyCode::Char(c) => { @@ -376,31 +412,42 @@ where execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); // Render the list - let mut new_selected = 0; - render_buffer.clear(); - for i in 0..items.len() { - if let Some(matched) = matcher.fuzzy_match(&items[i].to_string(), &text_input.input) - { - render_buffer.push(items[i].clone()); - if i == selected { - new_selected = render_buffer.len() - 1; - } + kv_items.iter_mut().for_each(|x| { + x.matched = matcher + .fuzzy_match(&x.value.to_string(), &text_input.input) + .is_some() + }); + let visible = kv_items.iter().filter(|x| x.matched).count(); + + if !kv_items[selected].matched { + if let Some(found) = kv_items.iter().find(|&x| x.matched) { + selected = found.key; + } else { + selected = 0; } } - // let diff = (selected + 1) as isize - (usable_rows / 2) as isize; - // let mut offset = 0; - // if diff <= 0 { - // offset = 0; - // } else if diff > 0 { - // offset = diff as usize; - // } - // if offset > total_rows - usable_rows { - // offset = total_rows - usable_rows; - // } + let diff = (selected + 1) as isize - (usable_rows / 2) as isize; + let mut offset = 0; + if diff <= 0 { + offset = 0; + } else if diff > 0 { + offset = diff as usize; + } + if visible < usable_rows { + offset = 0; + } else if offset > visible - usable_rows { + offset = visible - usable_rows; + } - let rendered = - render_list(&render_buffer, selected, &matcher, text_input.input.clone()); + let rendered = render_list( + &kv_items, + selected, + offset, + usable_rows, + &matcher, + text_input.input.clone(), + ); // Go back to the prompt execute!( @@ -416,24 +463,67 @@ where disable_raw_mode().unwrap(); - Ok(render_buffer[selected].clone()) + Ok(items[selected].clone()) } /// Render the list of items /// This function assumes that the items are already filtered, and correctly offset, and uses the matcher to color the items -fn render_list(items: &Vec, selected: usize, matcher: &SkimMatcherV2, input: String) -> usize +fn render_list( + items: &Vec>, + selected: usize, + offset: usize, + usable_rows: usize, + matcher: &SkimMatcherV2, + input: String, +) -> usize where T: Display + Clone, { - for i in 0..items.len() { - if i == selected { - print("> "); + let mut rendered = 0; + let mut i = offset; + while rendered < usable_rows { + if i >= items.len() { + break; + } + + if !items[i].matched { + i += 1; + continue; } + if items[i].key == selected { + print("$cc `>` "); + } else if rendered == 0 && offset > 0 { + print("⌃ "); + } else if rendered == usable_rows - 1 && i < items.len() - 1 { + print("⌄ "); + } else { + print(" "); + } + + let matched_letters = matcher + .fuzzy_indices(&items[i].value.to_string(), &input) + .unwrap_or((0, vec![])) + .1; + + let word = items[i].value.to_string(); + + for i in 0..word.len() { + let letter = word.get(i..i + 1).unwrap_or(""); + if matched_letters.iter().find(|&&x| x == i).is_some() { + print(format!("$cc `{}`", letter)); + } else { + print(format!("{}", letter)); + } + } + + rendered += 1; + i += 1; + execute!(io::stdout(), MoveDown(1), MoveToColumn(0)).unwrap(); } - items.len() + rendered } fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { From 1e5e29d5b5777495ffd837a78c5f96432004d99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Fri, 26 Sep 2025 22:44:03 +0200 Subject: [PATCH 21/28] =?UTF-8?q?=E2=9C=A8=20Basic=20spinner=20implementat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/git.rs | 20 +++++++++-- src/config/mod.rs | 4 ++- src/main.rs | 82 +++++++++++++-------------------------------- src/view/input.rs | 4 +-- src/view/mod.rs | 31 ++++++++++++++--- src/view/spinner.rs | 58 ++++++++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 69 deletions(-) create mode 100644 src/view/spinner.rs diff --git a/src/config/git.rs b/src/config/git.rs index f844f89..1be3114 100644 --- a/src/config/git.rs +++ b/src/config/git.rs @@ -1,3 +1,5 @@ +use regex::Regex; + const MIN_GIT_VERSION: &str = "2.20.0"; // Create an error message for when git is not installed depending on the OS @@ -117,19 +119,31 @@ pub fn validate_git_install() -> Result<(), GitError> { return Err(GitError::NotInstalled); } - let version = s.split(" ").last().unwrap(); + let re = Regex::new(r"(\d+\.\d+\.\d+)").unwrap(); + let version; + + if let Some(captures) = re.captures(s) { + if let Some(v) = captures.get(1) { + version = v.as_str(); + } else { + return Err(GitError::NotInstalled); + } + } else { + return Err(GitError::NotInstalled); + } + let version_u32 = version .split(".") .collect::>() .iter() - .map(|s| s.parse::().unwrap()) + .map(|s| s.parse::().unwrap_or(0)) .collect::>(); let min_version = MIN_GIT_VERSION .split(".") .collect::>() .iter() - .map(|s| s.parse::().unwrap()) + .map(|s| s.parse::().unwrap_or(0)) .collect::>(); if version_u32.len() != 3 || min_version.len() != 3 { diff --git a/src/config/mod.rs b/src/config/mod.rs index d7b6fe9..ef2ce75 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,4 @@ -use crate::{utils::out, view}; +use crate::view; use git::check_git_config; use serde::{Deserialize, Serialize}; @@ -32,6 +32,8 @@ pub async fn check_prerequisites() { } } + return; + // Check for git config match check_git_config() { Ok(_) => {} diff --git a/src/main.rs b/src/main.rs index 4006ff8..6756da4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,66 +26,31 @@ enum SubCommand { CommitAll(modules::commit::CommitOptions), #[clap(name = "cf", about = "Commit specific files")] CommitFiles(modules::commit::CommitOptions), + // #[clap(name = "clone", about = "Clone a repository")] + // Clone(modules::clone::CloneOptions), - #[clap(name = "clone", about = "Clone a repository")] - Clone(modules::clone::CloneOptions), + // #[clap(name = "history", about = "Show commit history")] + // #[clap(visible_alias = "log")] + // History(modules::history::CommitHistoryOptions), - #[clap(name = "history", about = "Show commit history")] - #[clap(visible_alias = "log")] - History(modules::history::CommitHistoryOptions), - - #[clap(name = "login", about = "Login to GitHub")] - Login, + // #[clap(name = "login", about = "Login to GitHub")] + // Login, } #[tokio::main] async fn main() { - let args = Cli::parse(); + let mut spinner = view::spinner::Spinner::new("Loading..."); + + tokio::time::sleep(std::time::Duration::from_secs(2)).await; - // let password = match input::password("$cg `>` $cw `Enter your GitHub password: `") { - // Ok(password) => password, - // Err(err) => match err { - // input::ReturnType::Cancel => { - // println!("Cancelled"); - // return; - // } - // input::ReturnType::Exit => { - // println!("Exiting"); - // return; - // } - // }, - // }; - // println!("Password: {}", password); - - let choice = input::list( - "$cg `>` $cw `Enter your GitHub password: `", - vec![ - "Option 1", - "Option 2", - "Option 3", - "Option 4", - "Option 5", - "Option 6", - "Option 7", - "Option 8", - "Option 9", - "Option 10", - "Option 11", - "Option 12", - "Option 13", - "Option 14", - "Option 15", - "Option 16", - "Option 17", - "Option 18", - "Option 19", - "Option 20", - ], - ) - .unwrap_or(""); - println!("Choice: {:?}", choice); + spinner.stop_with_message("Done!"); + + tokio::time::sleep(std::time::Duration::from_secs(2)).await; return; + + let args = Cli::parse(); + config::check_prerequisites().await; let subcmd = match args.subcmd { @@ -105,14 +70,13 @@ async fn main() { } SubCommand::CommitFiles(options) => { return modules::commit::commit_specific_files(options); - } - SubCommand::Clone(options) => { - return modules::clone::clone_menu(options).await; - } - SubCommand::History(options) => { - return modules::history::commit_history(options); - } - SubCommand::Login => config::login().await, + } // SubCommand::Clone(options) => { + // return modules::clone::clone_menu(options).await; + // } + // SubCommand::History(options) => { + // return modules::history::commit_history(options); + // } + // SubCommand::Login => config::login().await, } view::clean_up(); diff --git a/src/view/input.rs b/src/view/input.rs index 0c6a352..92fb5d5 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -305,7 +305,7 @@ where }) .collect(); - enable_raw_mode().unwrap(); + super::init(); let mut selected = 0; let matcher = SkimMatcherV2::default(); @@ -527,7 +527,7 @@ where } fn get_user_text_input(position: usize, input_type: TextInputType) -> Result { - enable_raw_mode().unwrap(); + super::init(); let mut text_input = TextInput::new(position, input_type); diff --git a/src/view/mod.rs b/src/view/mod.rs index 55ee401..fc99c92 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,12 +1,17 @@ use crossterm::{ - cursor::MoveToNextLine, + cursor::{MoveToColumn, MoveToNextLine}, execute, style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, - terminal::{disable_raw_mode, enable_raw_mode}, + terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, }; use std::io::stdout; pub mod input; +pub mod spinner; + +pub fn init() { + enable_raw_mode().unwrap(); +} pub fn clean_up() { disable_raw_mode().unwrap(); @@ -90,7 +95,9 @@ pub struct PrintSize { } /** -This function is used to print a string with special effects. The special effects are defined by the following syntax: +This function is used to print a string with special effects. + +The special effects are defined by the following syntax: $effect `content` - $b: bold - $i: italic - $u: underline @@ -112,6 +119,8 @@ This function is used to print a string with special effects. The special effect - $bw: background white color - &>: tab (4 spaces) + +Multiple effects can be combined, ex. $b$u - bold underline, or $b `Bold and $u `underline`` */ pub fn printer(content: impl AsRef) -> PrintSize { enable_raw_mode().unwrap(); @@ -122,7 +131,9 @@ pub fn printer(content: impl AsRef) -> PrintSize { } /** Print the content with correct formatting, but without going into raw mode -This function is used to print a string with special effects. The special effects are defined by the following syntax: +This function is used to print a string with special effects. + +The special effects are defined by the following syntax: $effect `content` - $b: bold - $i: italic - $u: underline @@ -144,6 +155,8 @@ This function is used to print a string with special effects. The special effect - $bw: background white color - &>: tab (4 spaces) + +Multiple effects can be combined, ex. $b$u - bold underline, or $b `Bold and $u `underline`` */ pub fn print(content: impl AsRef) -> PrintSize { let mut size = PrintSize { cols: 0, rows: 0 }; @@ -238,6 +251,16 @@ pub fn print(content: impl AsRef) -> PrintSize { size } +pub fn clear_line() { + execute!( + stdout(), + MoveToColumn(0), + Clear(ClearType::CurrentLine), + MoveToColumn(0) + ) + .unwrap(); +} + pub fn no_subcommand_error() { let eror_message = r#" $b$cr `error`: no subcommand provided diff --git a/src/view/spinner.rs b/src/view/spinner.rs new file mode 100644 index 0000000..4e35ddd --- /dev/null +++ b/src/view/spinner.rs @@ -0,0 +1,58 @@ +use super::*; +use std::sync::{Arc, Mutex}; + +const SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +pub struct Spinner<'a> { + message: &'a str, + + thread_handle: Option>, + should_stop: Arc>, +} + +impl<'a> Spinner<'a> { + pub fn new(message: &'a str) -> Self { + let mut spinner = Spinner { + message, + thread_handle: None, + should_stop: Arc::new(Mutex::new(false)), + }; + spinner.start(); + spinner + } + + fn start(&mut self) { + let message = self.message.to_string(); + let should_stop = Arc::clone(&self.should_stop); + self.thread_handle = Some(std::thread::spawn(move || { + let mut i = 0; + loop { + if *(should_stop.lock().unwrap()) { + break; + } + + let frame = SPINNER_FRAMES[i % SPINNER_FRAMES.len()]; + printer(format!("{} {}", frame, message)); + std::io::Write::flush(&mut stdout()).unwrap(); + print!("\r"); + std::thread::sleep(std::time::Duration::from_millis(75)); + i += 1; + } + })); + } + + pub fn stop(&mut self) { + if let Some(handle) = &self.thread_handle { + *(self.should_stop.lock().unwrap()) = true; + } + } + + pub fn stop_with_message(&mut self, message: &str) { + self.stop(); + clear_line(); + printer(message.to_string()); + if (message.ends_with('\n')) == false { + print!("\n"); + } + } +} From cd3cb2b2216e8d99b51b385fe24e32a31625b84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Mon, 29 Sep 2025 18:07:46 +0200 Subject: [PATCH 22/28] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config/mod.rs | 1 - src/view/input.rs | 11 +++++------ src/view/mod.rs | 3 +-- src/view/spinner.rs | 5 +++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index ef2ce75..79fb26c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -32,7 +32,6 @@ pub async fn check_prerequisites() { } } - return; // Check for git config match check_git_config() { diff --git a/src/view/input.rs b/src/view/input.rs index 92fb5d5..5450410 100644 --- a/src/view/input.rs +++ b/src/view/input.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use std::{ - fmt::{write, Debug, Display}, + fmt::{Debug, Display}, io::{self, Write}, }; @@ -508,12 +508,11 @@ where let word = items[i].value.to_string(); - for i in 0..word.len() { - let letter = word.get(i..i + 1).unwrap_or(""); - if matched_letters.iter().find(|&&x| x == i).is_some() { - print(format!("$cc `{}`", letter)); + for (byte_idx, ch) in word.char_indices() { + if matched_letters.iter().any(|&x| x == byte_idx) { + print(format!("$cc `{}`", ch)); } else { - print(format!("{}", letter)); + print(format!("{}", ch)); } } diff --git a/src/view/mod.rs b/src/view/mod.rs index fc99c92..97d60fe 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -177,7 +177,7 @@ pub fn print(content: impl AsRef) -> PrintSize { if c == '$' { let mut special = String::new(); - while content.chars().nth(i).unwrap() != ' ' { + while i < n && content.chars().nth(i).unwrap() != ' ' { special.push(content.chars().nth(i).unwrap()); i += 1; } @@ -213,7 +213,6 @@ pub fn print(content: impl AsRef) -> PrintSize { while i < n && content.chars().nth(i).unwrap() == ' ' { i += 1; - size.rows += 1; } continue; diff --git a/src/view/spinner.rs b/src/view/spinner.rs index 4e35ddd..f8a7433 100644 --- a/src/view/spinner.rs +++ b/src/view/spinner.rs @@ -42,8 +42,9 @@ impl<'a> Spinner<'a> { } pub fn stop(&mut self) { - if let Some(handle) = &self.thread_handle { + if let Some(handle) = self.thread_handle.take() { *(self.should_stop.lock().unwrap()) = true; + let _ = handle.join(); } } @@ -51,7 +52,7 @@ impl<'a> Spinner<'a> { self.stop(); clear_line(); printer(message.to_string()); - if (message.ends_with('\n')) == false { + if !message.ends_with('\n') { print!("\n"); } } From 535ca9b15943442144ec2ca4ef501756f68313cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Mon, 29 Sep 2025 18:09:17 +0200 Subject: [PATCH 23/28] =?UTF-8?q?=F0=9F=92=84=20Fix=20typo=20in=20variable?= =?UTF-8?q?=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/mod.rs b/src/view/mod.rs index 97d60fe..77b512f 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -261,7 +261,7 @@ pub fn clear_line() { } pub fn no_subcommand_error() { - let eror_message = r#" + let error_message = r#" $b$cr `error`: no subcommand provided $b$u `Usage`: $b `tgh` [COMMAND] @@ -269,5 +269,5 @@ pub fn no_subcommand_error() { For more information try $b `'tgh --help'` "#; - printer(eror_message); + printer(error_message); } From e6b4c538b5e22ecc1846df74663a2d4261989f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 11 Jan 2026 22:17:39 +0100 Subject: [PATCH 24/28] =?UTF-8?q?=F0=9F=94=A5=20Clean=20up=20development?= =?UTF-8?q?=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 44 +++++---------------------------- src/modules/commit.rs | 27 +++++++++++++++++--- src/modules/commit/views.rs | 49 ------------------------------------- src/view/spinner.rs | 41 +++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 91 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6756da4..bcec099 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,6 @@ use clap::{Parser, Subcommand}; mod utils; use utils::out; -use utils::out::clear_screen; -use view::input; mod config; mod functions; @@ -19,36 +17,15 @@ struct Cli { #[derive(Subcommand)] enum SubCommand { - #[clap(name = "commit", about = "Open the commit menu")] - #[clap(visible_alias = "c")] - Commit(modules::commit::CommitOptions), + #[clap(name = "commit", about = "Commit changes to the repository")] + #[clap(visible_alias = "cf")] + CommitFiles(modules::commit::CommitOptions), #[clap(name = "ca", about = "Commit all files")] CommitAll(modules::commit::CommitOptions), - #[clap(name = "cf", about = "Commit specific files")] - CommitFiles(modules::commit::CommitOptions), - // #[clap(name = "clone", about = "Clone a repository")] - // Clone(modules::clone::CloneOptions), - - // #[clap(name = "history", about = "Show commit history")] - // #[clap(visible_alias = "log")] - // History(modules::history::CommitHistoryOptions), - - // #[clap(name = "login", about = "Login to GitHub")] - // Login, } #[tokio::main] async fn main() { - let mut spinner = view::spinner::Spinner::new("Loading..."); - - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - - spinner.stop_with_message("Done!"); - - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - - return; - let args = Cli::parse(); config::check_prerequisites().await; @@ -62,21 +39,12 @@ async fn main() { }; match subcmd { - SubCommand::Commit(options) => { - return modules::commit::commit_menu(options); - } SubCommand::CommitAll(options) => { - return modules::commit::commit_all_files(options); + modules::commit::commit_all_files(options); } SubCommand::CommitFiles(options) => { - return modules::commit::commit_specific_files(options); - } // SubCommand::Clone(options) => { - // return modules::clone::clone_menu(options).await; - // } - // SubCommand::History(options) => { - // return modules::history::commit_history(options); - // } - // SubCommand::Login => config::login().await, + modules::commit::commit_specific_files(options); + } } view::clean_up(); diff --git a/src/modules/commit.rs b/src/modules/commit.rs index 317f909..359ef22 100644 --- a/src/modules/commit.rs +++ b/src/modules/commit.rs @@ -3,19 +3,19 @@ use clap::Parser; mod functions; mod views; -pub use views::{commit_all_files, commit_menu, commit_specific_files}; +pub use views::commit_specific_files; #[derive(Parser)] pub struct CommitOptions { - /// Don't push changes to remote + /// Don't push changes to the remote #[clap(short, long)] pub no_push: bool, - /// Don't use fancy commit messages + /// Don't use fancy commit message #[clap(long, conflicts_with = "force_fancy")] pub skip_fancy: bool, - /// Force fancy commit messages + /// Force fancy commit message #[clap(long, conflicts_with = "skip_fancy")] pub force_fancy: bool, @@ -33,3 +33,22 @@ impl Default for CommitOptions { } } } + +pub fn commit_all_files(options: CommitOptions) { + functions::is_valid_commit(); + + let message = ask_commit_message(&options); + + println!("Committing all files with message: {}", message); + + // commit_all_files(message, options.no_push); +} + +fn ask_commit_message(options: &CommitOptions) -> String { + if let Some(message) = &options.commit_message { + return message.clone(); + } + + let config = crate::config::load_config(); + String::from("dummy message") // Temporary fix to allow compilation +} diff --git a/src/modules/commit/views.rs b/src/modules/commit/views.rs index 526bf2a..d2f37bd 100644 --- a/src/modules/commit/views.rs +++ b/src/modules/commit/views.rs @@ -1,54 +1,6 @@ use super::CommitOptions; use inquire::{list_option::ListOption, validator::Validation}; -pub fn commit_menu(options: CommitOptions) { - use crate::clear_screen; - use inquire::Select; - use std::process; - - clear_screen(); - - super::functions::is_valid_commit(); - - let choice; - - let menu = Select::new( - "What do you want to commit?", - vec!["Commit specific files", "Commit all files"], - ) - .prompt(); - - match menu { - Ok(option) => { - choice = option; - } - Err(_) => { - process::exit(0); - } - } - - match choice { - "Commit specific files" => { - commit_specific_files(options); - } - "Commit all files" => { - commit_all_files(options); - } - _ => { - println!("Invalid option"); - } - } -} - -pub fn commit_all_files(options: CommitOptions) { - use super::functions::{commit_all_files, is_valid_commit}; - - is_valid_commit(); - - let message = ask_commit_message(&options); - - commit_all_files(message, options.no_push); -} pub fn commit_specific_files(options: CommitOptions) { use super::functions::{commit_specific_files, is_valid_commit}; @@ -60,7 +12,6 @@ pub fn commit_specific_files(options: CommitOptions) { commit_specific_files(files, message, options.no_push); } -pub fn commit_history() {} fn ask_commit_message(options: &CommitOptions) -> String { use inquire::{Select, Text}; diff --git a/src/view/spinner.rs b/src/view/spinner.rs index f8a7433..e53d80b 100644 --- a/src/view/spinner.rs +++ b/src/view/spinner.rs @@ -57,3 +57,44 @@ impl<'a> Spinner<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn test_spinner_lifecycle() { + let mut spinner = Spinner::new("Testing spinner..."); + std::thread::sleep(Duration::from_millis(200)); + + spinner.stop(); + + // Verify state after stop + assert!( + *spinner.should_stop.lock().unwrap(), + "Spinner should be marked as stopped after stop()" + ); + assert!( + spinner.thread_handle.is_none(), + "Thread handle should be None after stop()" + ); + } + + #[test] + fn test_spinner_stop_with_message() { + let mut spinner = Spinner::new("Testing spinner with message..."); + + std::thread::sleep(Duration::from_millis(200)); + spinner.stop_with_message("Done!"); + + assert!( + *spinner.should_stop.lock().unwrap(), + "Spinner should be marked as stopped after stop_with_message()" + ); + assert!( + spinner.thread_handle.is_none(), + "Thread handle should be None after stop_with_message()" + ); + } +} From 24ff7b55d510e2a9205e0634083e3881974f6881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 11 Jan 2026 23:13:15 +0100 Subject: [PATCH 25/28] =?UTF-8?q?=E2=9C=A8=20Create=20the=20update=20servi?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 720 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 5 +- src/config/update.rs | 65 ++++ 3 files changed, 764 insertions(+), 26 deletions(-) create mode 100644 src/config/update.rs diff --git a/Cargo.lock b/Cargo.lock index eaa9015..248577d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -26,6 +26,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -88,7 +97,7 @@ dependencies = [ "objc-foundation", "objc_id", "parking_lot", - "thiserror", + "thiserror 1.0.57", "windows-sys 0.48.0", "x11rb", ] @@ -120,6 +129,18 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" + [[package]] name = "bitflags" version = "1.3.2" @@ -138,6 +159,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.15.3" @@ -174,6 +204,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.2" @@ -195,7 +238,7 @@ dependencies = [ "clap_lex", "strsim", "unicase", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -207,7 +250,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -237,6 +280,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.11", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -277,6 +339,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.0" @@ -286,6 +357,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.25.0" @@ -327,12 +404,109 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dyn-clone" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -366,9 +540,9 @@ checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" @@ -379,6 +553,24 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "flate2" version = "1.0.28" @@ -422,7 +614,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -461,6 +653,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-sink" version = "0.3.30" @@ -480,9 +678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -503,6 +704,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -513,6 +724,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -636,6 +858,30 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -670,6 +916,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + [[package]] name = "inquire" version = "0.7.0" @@ -684,7 +943,7 @@ dependencies = [ "newline-converter", "once_cell", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.11", ] [[package]] @@ -722,9 +981,20 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall 0.7.0", +] [[package]] name = "linux-raw-sys" @@ -824,6 +1094,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.18" @@ -843,6 +1119,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "objc" version = "0.2.7" @@ -910,7 +1192,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -959,7 +1241,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -982,6 +1264,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1001,15 +1293,36 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.35" @@ -1019,6 +1332,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1028,6 +1350,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "regex" version = "1.10.3" @@ -1063,7 +1394,7 @@ version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1103,6 +1434,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.31" @@ -1122,7 +1462,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -1175,6 +1515,45 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", + "tempfile", + "windows-sys 0.52.0", +] + +[[package]] +name = "self_update" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a34ad8e4a86884ab42e9b8690e9343abdcfe5fa38a0318cfe1565ba9ad437b4" +dependencies = [ + "hyper", + "indicatif", + "log", + "quick-xml", + "regex", + "reqwest", + "self-replace", + "semver", + "serde_json", + "tar", + "tempfile", + "urlencoding", + "zip", + "zipsign-api", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.197" @@ -1192,7 +1571,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -1218,6 +1597,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -1248,6 +1638,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1290,6 +1690,16 @@ dependencies = [ "strum", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.11.0" @@ -1318,6 +1728,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1331,9 +1747,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1367,6 +1783,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -1385,7 +1812,16 @@ version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.57", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1396,7 +1832,18 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -1420,11 +1867,31 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + [[package]] name = "tiny-git-helper" -version = "0.1.5" +version = "0.0.5" dependencies = [ "arboard", + "chrono", "clap", "crossterm 0.27.0", "fuzzy-matcher", @@ -1433,6 +1900,8 @@ dependencies = [ "openssl", "regex", "reqwest", + "self_update", + "semver", "serde", "serde_json", "spinners", @@ -1481,7 +1950,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", ] [[package]] @@ -1539,6 +2008,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicase" version = "2.7.0" @@ -1581,6 +2056,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "url" version = "2.5.0" @@ -1592,6 +2073,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1646,7 +2133,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -1680,7 +2167,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.114", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1701,6 +2188,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.8" @@ -1729,6 +2226,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1747,6 +2303,15 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1777,6 +2342,23 @@ dependencies = [ "windows_x86_64_msvc 0.52.4", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1789,6 +2371,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1801,6 +2389,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1813,6 +2407,18 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1825,6 +2431,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1837,6 +2449,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1849,6 +2467,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1861,6 +2485,12 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winreg" version = "0.50.0" @@ -1887,3 +2517,43 @@ name = "x11rb-protocol" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "time", +] + +[[package]] +name = "zipsign-api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "thiserror 2.0.17", +] diff --git a/Cargo.toml b/Cargo.toml index bd766a2..2c16939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tiny-git-helper" description = "tgh - A GitHub CLI written in Rust" -version = "0.1.5" +version = "0.0.5" edition = "2021" [[bin]] @@ -24,3 +24,6 @@ spinners = "4.1.1" tokio = { version = "1.34.0", features = ["full"] } clap = { version = "4.4.11", features = ["derive", "unicode"] } fuzzy-matcher = "0.3.7" +chrono = "0.4.42" +self_update = { version = "0.39", features = ["archive-zip", "archive-tar"] } +semver = "1.0.27" diff --git a/src/config/update.rs b/src/config/update.rs new file mode 100644 index 0000000..130fdff --- /dev/null +++ b/src/config/update.rs @@ -0,0 +1,65 @@ +use self_update::backends::github::ReleaseList; + +pub async fn check_for_updates() -> Result> { + let update_check = tokio::task::spawn_blocking(|| tokio_check_for_updates()) + .await + .expect("Blocking task panicked"); + + update_check +} + +fn tokio_check_for_updates() -> Result> { + let current_version = env!("CARGO_PKG_VERSION"); + + let releases = ReleaseList::configure() + .repo_owner("dkomeza") + .repo_name("tiny-git-helper") + .build() + .map_err(|e| Box::new(e) as Box)? + .fetch() + .map_err(|e| Box::new(e) as Box)?; + + let latest_release = releases + .iter() + .filter(|r| !r.version.contains("alpha") && !r.version.contains("beta")) + .max_by(|a, b| { + let version_a = + semver::Version::parse(&a.version).unwrap_or(semver::Version::new(0, 0, 0)); + let version_b = + semver::Version::parse(&b.version).unwrap_or(semver::Version::new(0, 0, 0)); + version_a.cmp(&version_b) + }); + + if let Some(release) = latest_release { + let current = semver::Version::parse(current_version) + .map_err(|e| Box::new(e) as Box)?; + let latest = semver::Version::parse(&release.version) + .map_err(|e| Box::new(e) as Box)?; + + if latest > current { + let update_msg = format!( + "\n$cg$b `📦 New Update Available`\n\ + &> $cy `{}` $cw `➜` $cg$b `{}`\n\ + &> Run $cc$i `tgh update` $cw `to upgrade`\n", + current_version, release.version + ); + return Ok(update_msg); + } + } + + Ok("".into()) +} + +pub fn perform_self_update() -> Result<(), Box> { + let status = self_update::backends::github::Update::configure() + .repo_owner("dkomeza") + .repo_name("tiny-git-helper") + .bin_name("tgh") + .show_download_progress(true) + .current_version(env!("CARGO_PKG_VERSION")) + .build()? + .update()?; + + println!("Updated to version: {}", status.version()); + Ok(()) +} From fba1cd5f3aebf94f90ec2bd6f7cfd9c6117e5ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 11 Jan 2026 23:13:25 +0100 Subject: [PATCH 26/28] =?UTF-8?q?=E2=9C=A8=20Check=20for=20update=20on=20s?= =?UTF-8?q?tartup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/mod.rs | 34 ++++++++++++++++- src/config/utils.rs | 91 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 79fb26c..cc4d8c1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + use crate::view; use git::check_git_config; use serde::{Deserialize, Serialize}; @@ -6,6 +8,7 @@ mod config; pub mod defines; mod git; mod github; +mod update; pub mod utils; pub use config::load_config; @@ -21,6 +24,20 @@ pub struct Config { pub fancy: bool, } +#[derive(Serialize, Deserialize, Clone)] +pub struct Metadata { + pub last_checked: String, +} + +impl Default for Metadata { + fn default() -> Self { + Metadata { + last_checked: chrono::DateTime::::from(SystemTime::UNIX_EPOCH) + .to_rfc3339(), + } + } +} + /// Checks if the prerequisites for tgh are installed. /// If not, it will print an error and exit. pub async fn check_prerequisites() { @@ -32,7 +49,6 @@ pub async fn check_prerequisites() { } } - // Check for git config match check_git_config() { Ok(_) => {} @@ -58,4 +74,20 @@ pub async fn check_prerequisites() { std::thread::sleep(std::time::Duration::from_secs(1)); } + + if utils::should_check_for_updates() { + match update::check_for_updates().await { + Ok(msg) => { + if msg.len() > 0 { + view::printer(msg); + } + } + Err(err) => { + view::printer(&format!( + "\n$b$cr `error`: Failed to check for updates: {}\n", + err.to_string() + )); + } + } + } } diff --git a/src/config/utils.rs b/src/config/utils.rs index 292b2f1..29d4f7b 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -10,7 +10,7 @@ pub fn handle_config_folder() { create_dir_all(config_path).unwrap(); } -pub fn get_config_path() -> String { +fn get_config_path() -> String { use home::home_dir; let home = home_dir().unwrap(); @@ -19,6 +19,15 @@ pub fn get_config_path() -> String { config_path } +fn get_metadata_path() -> String { + use home::home_dir; + + let home = home_dir().unwrap(); + let metadata_path = format!("{}/.config/tgh/metadata.json", home.display()); + + metadata_path +} + pub fn config_exists() -> bool { use std::path::Path; @@ -28,30 +37,59 @@ pub fn config_exists() -> bool { config_path.exists() } -pub fn read_config_content() -> String { +fn read_file_content(path: String) -> Result { use std::fs::File; use std::io::prelude::*; - let config_path = get_config_path(); - let mut config_file = File::open(config_path).unwrap(); + let mut config_file = File::open(path)?; let mut config_contents = String::new(); - config_file.read_to_string(&mut config_contents).unwrap(); + config_file.read_to_string(&mut config_contents)?; - config_contents + Ok(config_contents) } pub fn read_config() -> crate::config::Config { - let config_contents = read_config_content(); - let config: crate::config::Config = serde_json::from_str(&config_contents).unwrap(); + let config_contents = read_file_content(get_config_path()); + + let conf = match config_contents { + Err(_) => { + panic!("Failed to read config file."); + } + Ok(contents) => contents, + }; + + let config: crate::config::Config = serde_json::from_str(&conf).unwrap(); config } +pub fn read_metadata() -> crate::config::Metadata { + let metadata_path = get_metadata_path(); + + let metadata_contents = read_file_content(metadata_path); + + let metadata = match metadata_contents { + Err(_) => { + let metadata = crate::config::Metadata::default(); + save_metadata_file(metadata.clone()); + metadata + } + Ok(contents) => serde_json::from_str(&contents).unwrap_or_default(), + }; + + metadata +} + pub fn validate_config_file() -> bool { use crate::config::Config; - let config_contents = read_config_content(); + let config_contents = match read_file_content(get_config_path()) { + Ok(contents) => contents, + Err(_) => { + return false; + } + }; if config_contents.len() == 0 { return false; @@ -291,3 +329,38 @@ pub fn get_labels() -> Vec { labels } +pub fn should_check_for_updates() -> bool { + let metadata = read_metadata(); + + let now = chrono::Utc::now(); + let last_checked = chrono::DateTime::parse_from_rfc3339(&metadata.last_checked) + .unwrap() + .to_utc(); + + let time_diff = now.signed_duration_since(last_checked).num_days(); + + if time_diff >= 1 { + return true; + } + + false +} + +pub fn save_metadata_file(metadata: crate::config::Metadata) { + use std::{fs::File, io::prelude::*}; + + let metadata_path = get_metadata_path(); + + let mut metadata_file = match (File::create(metadata_path)) { + Ok(file) => file, + Err(err) => { + panic!("Failed to create metadata file: {}", err.to_string()); + } + }; + + let metadata_contents = serde_json::to_string_pretty(&metadata).unwrap(); + + metadata_file + .write_all(metadata_contents.as_bytes()) + .unwrap(); +} From 094a938094df2a97743fa150674c087e26b702eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 11 Jan 2026 23:22:55 +0100 Subject: [PATCH 27/28] =?UTF-8?q?=F0=9F=91=B7=20Change=20how=20the=20binar?= =?UTF-8?q?ies=20are=20released?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release_binaries.yml | 50 ++++++++++---------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/.github/workflows/release_binaries.yml b/.github/workflows/release_binaries.yml index 861e614..a0bd6c5 100644 --- a/.github/workflows/release_binaries.yml +++ b/.github/workflows/release_binaries.yml @@ -10,19 +10,15 @@ env: jobs: release-linux: + runs-on: ubuntu-latest strategy: matrix: platform: - - release_for: ARM64 Linux - target: aarch64-unknown-linux-gnu + - target: aarch64-unknown-linux-gnu binary: "tgh-linux-arm64" - host: "ubuntu-latest" - - release_for: X86_64 Linux - target: x86_64-unknown-linux-gnu + - target: x86_64-unknown-linux-gnu binary: "tgh-linux-x64" - host: "ubuntu-latest" - runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 @@ -41,19 +37,18 @@ jobs: run: cross build --release --target ${{ matrix.platform.target }} - name: Compress binary - run: tar -czf target/${{ matrix.platform.binary }}.tar.gz -C target/${{ matrix.platform.target }}/release tgh + run: tar -czf target/${{ matrix.platform.target }}.tar.gz -C target/${{ matrix.platform.target }}/release tgh - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/${{ matrix.platform.binary }}.tar.gz - asset_name: ${{ matrix.platform.binary }}.tar.gz + file: target/${{ matrix.platform.target }}.tar.gz + asset_name: tgh-${{ matrix.platform.target }}.tar.gz tag: ${{ github.ref }} release-windows: runs-on: windows-latest - steps: - name: Checkout uses: actions/checkout@v2 @@ -67,28 +62,22 @@ jobs: - name: Build run: cargo build --release + - name: Compress binary + run: Compress-Archive -Path target/release/tgh.exe -DestinationPath target/tgh-pc-windows-msvc.zip + - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/release/tgh.exe - asset_name: tgh-windows-x64.exe + file: target/tgh-pc-windows-msvc.zip + asset_name: tgh-x86_64-pc-windows-msvc.zip tag: ${{ github.ref }} release-macos: + runs-on: macos-latest strategy: matrix: - platform: - - release_for: X86_64 macOS - target: x86_64-apple-darwin - binary: "tgh-darwin-x64" - host: "macos-latest" - - release_for: ARM64 macOS - target: aarch64-apple-darwin - binary: "tgh-darwin-arm64" - host: "macos-latest" - - runs-on: macos-latest + target: [x86_64-apple-darwin, aarch64-apple-darwin] steps: - name: Checkout uses: actions/checkout@v2 @@ -98,21 +87,18 @@ jobs: with: toolchain: stable override: true - target: ${{ matrix.platform.target }} - - - name: Setup cross-compilation - run: cargo install -f cross + target: ${{ matrix.target }} - name: Build - run: cross build --release --target ${{ matrix.platform.target }} + run: cargo build --release --target ${{ matrix.target }} - name: Compress binary - run: tar -czf target/${{ matrix.platform.binary }}.tar.gz -C target/${{ matrix.platform.target }}/release tgh + run: tar -czf target/${{ matrix.target }}.tar.gz -C target/${{ matrix.target }}/release tgh - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/${{ matrix.platform.binary }}.tar.gz - asset_name: ${{ matrix.platform.binary }}.tar.gz + file: target/${{ matrix.target }}.tar.gz + asset_name: tgh-${{ matrix.target }}.tar.gz tag: ${{ github.ref }} From f59c0313ace6e38ebe3d8acb41dcb9532ec51787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Kom=C4=99za?= Date: Sun, 11 Jan 2026 23:30:06 +0100 Subject: [PATCH 28/28] =?UTF-8?q?=E2=9C=A8=20Finish=20the=20update=20servi?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/mod.rs | 2 +- src/config/update.rs | 75 ++++++++++++++++++++++++++++++++++++++------ src/main.rs | 6 ++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index cc4d8c1..780c3b7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,7 +8,7 @@ mod config; pub mod defines; mod git; mod github; -mod update; +pub mod update; pub mod utils; pub use config::load_config; diff --git a/src/config/update.rs b/src/config/update.rs index 130fdff..709b16c 100644 --- a/src/config/update.rs +++ b/src/config/update.rs @@ -1,4 +1,4 @@ -use self_update::backends::github::ReleaseList; +use self_update::backends::github::{ReleaseList, Update}; pub async fn check_for_updates() -> Result> { let update_check = tokio::task::spawn_blocking(|| tokio_check_for_updates()) @@ -38,8 +38,8 @@ fn tokio_check_for_updates() -> Result current { let update_msg = format!( - "\n$cg$b `📦 New Update Available`\n\ - &> $cy `{}` $cw `➜` $cg$b `{}`\n\ + "\n$cg$b `📦 New Update Available` + &> $cy `{}` $cw `➜` $cg$b `{}` &> Run $cc$i `tgh update` $cw `to upgrade`\n", current_version, release.version ); @@ -50,16 +50,73 @@ fn tokio_check_for_updates() -> Result Result<(), Box> { - let status = self_update::backends::github::Update::configure() +pub async fn perform_self_update() { + use crate::view::printer; + let current_ver = env!("CARGO_PKG_VERSION"); + + printer(format!( + "\n$cb$b `🔍 Checking for updates...`\n&> $cw `Current version:` $cy `{}`\n", + current_ver + )); + + let update_available = check_for_updates().await; + + match update_available { + Ok(msg) => { + if msg.is_empty() { + printer(format!( + "$cg$b `✔ You are already up to date.`\n&> $cw `Version:` $cg `{}`\n", + current_ver + )); + return; + } else { + printer("\n$cy$b `⬇ Update found! Starting download...`\n"); + } + } + Err(e) => { + printer(format!( + "\n$cr$b `✖ Failed to check for updates`\n&> $cr `Error:` $cw `{}`\n", + e + )); + return; + } + } + + let update_result = tokio::task::spawn_blocking(|| execute_update_logic()) + .await + .expect("Blocking task panicked"); + + // 4. Report Final Status + match update_result { + Ok(new_version) => { + printer(format!( + "\n$cg$b `✨ Update Successful!`\n&> $cw `New version:` $cg `{}`\n&> $cw `Please restart the terminal to use the new version.`\n", + new_version + )); + } + Err(err) => { + printer(format!( + "\n$cr$b `✖ Update Failed`\n&> $cr `Reason:` $cw `{}`\n", + err + )); + } + } +} + +// The internal blocking logic +fn execute_update_logic() -> Result> { + let status = Update::configure() .repo_owner("dkomeza") .repo_name("tiny-git-helper") .bin_name("tgh") .show_download_progress(true) .current_version(env!("CARGO_PKG_VERSION")) - .build()? - .update()?; + .no_confirm(true) + .build() + .map_err(|e| Box::new(e) as Box)? + .update() + .map_err(|e| Box::new(e) as Box)?; - println!("Updated to version: {}", status.version()); - Ok(()) + // Return the new version string + Ok(status.version().to_string()) } diff --git a/src/main.rs b/src/main.rs index bcec099..f002294 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,9 @@ enum SubCommand { CommitFiles(modules::commit::CommitOptions), #[clap(name = "ca", about = "Commit all files")] CommitAll(modules::commit::CommitOptions), + + #[clap(name = "update", about = "Update tgh to the latest version")] + Update, } #[tokio::main] @@ -45,6 +48,9 @@ async fn main() { SubCommand::CommitFiles(options) => { modules::commit::commit_specific_files(options); } + SubCommand::Update => { + config::update::perform_self_update().await; + } } view::clean_up();