diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/PrettyPrompt.iml b/.idea/PrettyPrompt.iml deleted file mode 100644 index cf84ae4..0000000 --- a/.idea/PrettyPrompt.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 3792141..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 932f2f8..39cac17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,92 +1,41 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "colored" -version = "2.1.0" +name = "ansi_term" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "lazy_static", - "windows-sys", + "winapi", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "prettyprompt" -version = "0.1.0" +version = "0.2.0" dependencies = [ - "colored", + "ansi_term", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "windows-targets", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 5c2432d..56acdc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "prettyprompt" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] -colored = "2.1.0" +ansi_term = "0.12" diff --git a/README.md b/README.md index f974528..4f5c7d0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,129 @@ -# PrettyPrompt +# PrettyPrompt -A pretty shell prompt, written in rust +A pretty shell prompt, written in rust. + +## Current Features + +- **User indicator** - Symbol with different colors for root user and normal users +- **Error indicator** - Symbol with different colors to indicate if the last comment was successful +- **Git repo indicator** + - Indicates if the current directory is a repo or a regular directory + - Branches indicated by different colors +- **SSH indicator** - Symbol to indicate if the current shell is in an SSH session +- **Current directory** + - Abbreviated if the path is too long + - Replaces the user's home directory with a `~` symbol + - Show the repo's name if currently in a git repo ## Screenshot ![screenshot](https://git.candifloss.cc/candifloss/PrettyPrompt/raw/branch/main/screenshot/BashPromptExampleScreenshoot.png "Screenshot") -Ignore the zsh prompt in the screenshot. + +## Planned Features + +- **Right-hand side prompt**: Challenging to implement on non-zsh shells. +- **Configuration file** + - Choose only the components you need + - Change appearance + - Symbols and text + - Colors + - Order and position + - Custom components + - Static: Shell icon, Host name, etc. + - Dynamic: Time & date, system stats, or any custom commands + +## Current Limitations + +- **Hard-Coded Configuration**: User customization is not available yet. +- **Exit Code Requirement**: Must pass the last command’s exit code as a command-line argument. + +## Tested on + +Ubuntu 24.04 + - `bash` 5.2 + - `zsh` 5.9 + - `ion` 1.0.0-alpha + +## Installation + +**Step 1. Build binary from source** +This requires [rust](https://www.rust-lang.org/tools/install) installed on your system. + +```bash +git clone https://git.candifloss.cc/candifloss/PrettyPrompt.git +cd PrettyPrompt/ +cargo build --release +# Binary location: `target/release/prettyprompt` +``` + +**Step 2. Add to `$PATH`** + - Option 1. Move the binary to a directory in your `$PATH`. Eg: +```bash +sudo mv target/release/prettyprompt /usr/bin/ +``` + - Option 2. Add the directory containing the binary to `$PATH` + System-wide: `/etc/profile` + User-specific: `~/.profile` + Shell-specific: `bashrc`, `zshrc`, etc. +```bash +export PATH="$PATH:/path/where/the/binary/is/" +``` + +## Usage + +Configuration varies by shell, and the file location varies by distro. Refer your shell's docs or community resources for details. Note that the exit code of the last command(usually `$?` variable) must be passed as a command-line argument. + +### `bash` + + - The `PS1` variable sets a fixed prompt string. + - This `PROMPT_COMMAND` variable sets a dynamic prompt. + +System-wide: `/etc/bash.bashrc` +User-specific: `~/.bashrc`: +```bash +PS1="" # Set it to an empty string +PROMPT_COMMAND='prettyprompt $?' # Single quotes, not double quotes +``` + +### `ion` + +The `PROMPT` function is currently the only way to customize the prompt according to the `ion` shell docs. +User-specific config: `~/.config/ion/initrc`: +```ion +fn PROMPT + prettyprompt $? +end +``` + +### `zsh` +Export the `PS1` variable with the output of `prettyprompt $?` as its value. +User-specific: `~/.zshrc` +System-wide: `/etc/zsh/zshrc` +```sh +export PS1='$(prettyprompt $?)' # Notice the single quotes +``` + +### Other shells + +For other shells, refer their docs to set a dynamic prompt. Ensure the last command's exit code (`$?` or equivalent) is passed to `prettyprompt`. + +## Changes since the last version + - **Updated Output String Type:** Ansi strings improved compatibility with other shells. + - **Revamped Indicator Symbols:** Enhanced the visual aspect of the prompt. + - **Removed Shell Icon:** Determining the shell is practically not possible. + - **Conditional Component Inclusion:** A first step towards user-configuration expected in future versions. + - **Code Improvements:** readability and performance + - **Refactoring:** Modular structure for better readability and maintenance. + - **Modularization:** Separate modules for cleaner organization. + - **Error Handling:** Improved logic to exclude error messages from the prompt. + - **Enhanced Documentation:** Comments for better comprehension. + +## Acknowledgement + +The current default (and only) theme draws inspiration from [s1ck94](https://github.com/zimfw/s1ck94) theme of [zimfw](https://zimfw.sh/). + +## Why this project? + +- **Efficiency**: Avoids repeated invocation of multiple binaries like `tr`, `grep`, `echo`, `git`, `sed`, etc., which would otherwise be used dozens of times in shell scripts just to generate a colored string. +- **Portability**: Eliminates the need to write separate scripts in different shell languages for various shells. +- **Learning Rust**: Serves as a fun and practical project to learn and apply Rust programming skills. \ No newline at end of file diff --git a/src/indicators/error.rs b/src/indicators/error.rs new file mode 100644 index 0000000..5ce1992 --- /dev/null +++ b/src/indicators/error.rs @@ -0,0 +1,16 @@ +use ansi_term::ANSIGenericString; +use ansi_term::Colour::RGB; + +pub const ERR_SYMBOL: &str = "\u{276F}"; // Error indicator symbol: "❯" +pub const ERR_COL: ansi_term::Colour = RGB(255, 53, 94); // Error +pub const NORMIE_COL: ansi_term::Colour = RGB(0, 255, 180); // Success + +// Error indicator +pub fn indicator(args: &[String]) -> ANSIGenericString<'static, str> { + let exit_code = args.get(1).map_or("0", String::as_str); // Default to "0" (success, no error) if arg missing + if exit_code == "0" { + NORMIE_COL.paint(ERR_SYMBOL) // Success + } else { + ERR_COL.paint(ERR_SYMBOL) // Error + } +} diff --git a/src/indicators/git.rs b/src/indicators/git.rs new file mode 100644 index 0000000..e459fe9 --- /dev/null +++ b/src/indicators/git.rs @@ -0,0 +1,48 @@ +use ansi_term::ANSIGenericString; +use ansi_term::Colour::RGB; +use std::path::Path; +use std::process::Command; + +pub const GIT_SYMBOL: &str = "\u{276F}"; // Git indicator symbol: "❯" +pub const MAIN_COL: ansi_term::Colour = RGB(178, 98, 44); +pub const DEV_COL: ansi_term::Colour = RGB(54, 159, 150); +pub const DEFAULT_BRANCH_COL: ansi_term::Colour = RGB(255, 255, 255); +pub const NORMIE_COL: ansi_term::Colour = RGB(82, 82, 82); + +/// Returns the repo's root and branch, if present +pub fn info() -> Option<(String, String)> { + let output = Command::new("git") // Program/Command to execute + .args(["rev-parse", "--show-toplevel", "--abbrev-ref", "HEAD"]) // Arguments + .output() + .ok()?; + + if output.status.success() { + let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let parts: Vec<&str> = output_str.split('\n').collect(); + if parts.len() == 2 { + return Some((parts[0].to_string(), parts[1].to_string())); // If the `git`` command returns a repo and branch + } + } + None // If the current directory is not in a git repo +} + +/// The name of the repo +pub fn repo_name(path: &str) -> String { + Path::new(path) + .file_name() // Extracts the last component of the path. + .and_then(|name| name.to_str()) // Converts the `OsStr` to `&str`. + .unwrap_or("") // Default value(empty string) if None(no valid name) + .to_string() // Converts &str to String +} + +/// Git branch indicator +pub fn indicator(branch: Option) -> ANSIGenericString<'static, str> { + match branch { + Some(b) => match b.as_str() { + "main" => MAIN_COL.paint(GIT_SYMBOL), + "dev" => DEV_COL.paint(GIT_SYMBOL), + _ => DEFAULT_BRANCH_COL.paint(GIT_SYMBOL), + }, + None => NORMIE_COL.paint(GIT_SYMBOL), + } +} diff --git a/src/indicators/pwd.rs b/src/indicators/pwd.rs new file mode 100644 index 0000000..21fb3a3 --- /dev/null +++ b/src/indicators/pwd.rs @@ -0,0 +1,97 @@ +use crate::indicators::git::{repo_name, MAIN_COL}; +use ansi_term::ANSIGenericString; +use ansi_term::Colour::RGB; +use std::env::current_dir; + +pub const UNKNOWN_PATH: &str = "\u{2248}"; // "≈" +pub const PATH_COL: ansi_term::Colour = RGB(82, 82, 82); +pub const REPO_COL: ansi_term::Colour = RGB(55, 120, 130); + +/// Find the current user's home directory. +fn home_dir() -> String { + std::env::var("HOME").map_or_else(|_| String::new(), |path| path.to_string()) +} + +/// Returns the full path of the current directory as a string. +fn full_path() -> String { + current_dir().map_or_else( + |_| UNKNOWN_PATH.to_string(), + |path| path.display().to_string(), + ) +} + +/// Remove the path of the repo's root from the path, to replace it with the repo's name. +fn remove_repo(pwd_path: &str, repo_path: &str) -> String { + pwd_path.replacen(repo_path, "", 1) +} + +/// Replace the 'home directory' part of the path with a the '~' symbol +fn replace_home(path: &str) -> String { + path.replacen(&home_dir(), "~", 1) +} + +fn short(path: &str, slash_limit: u8) -> String { + let slashes = path.matches('/').count(); // Get the number of slashes + if slashes <= slash_limit.into() { + return path.to_string(); // Short path, return without changes + } + + // Else: Long path, shorten it + let parts: Vec<&str> = path.split('/').collect(); // Split the path into parts + let first = if path.starts_with('~') { "~" } else { "" }; // Determine the first part correctly + let last = parts[parts.len() - 1]; // The last part + + // Abbreviate middle parts (take the first character of each) + let abbreviated_middle = parts[1..parts.len() - 1] // Skip the first and last part + .iter() + .filter(|&&part| !part.is_empty()) // Avoid empty parts (like "//") + .map(|&part| part.chars().next().unwrap().to_string()) // Take the first letter + .collect::>() // Collect the parts as a Vec + .join("/"); // Join them with a "/" separator + + format!("{first}/{abbreviated_middle}/{last}") // Final +} + +pub fn pwd( + abbrev_home: bool, + shorten_path: bool, + replace_repo: bool, + git_repo: Option, +) -> ANSIGenericString<'static, str> { + let mut slash_limit: u8 = 3; // Max number of slashes + let mut path = full_path(); // Get the full path of he current directory + + // Replace a git repo root path with the repo's name + if replace_repo && git_repo.is_some() { + if let Some(repo_path) = git_repo { + path = remove_repo(&path, &repo_path); + let repo_name = repo_name(&repo_path); + if shorten_path { + slash_limit = 2; // Max number of slashes + path = short(&path, slash_limit); + } + return if path.is_empty() { + REPO_COL.paint(repo_name) // In the root dir of the repo + } else { + // In a subdir inside the repo + format!( + "{}{}{}", + REPO_COL.paint(repo_name), // Repo name + MAIN_COL.paint(" \u{F02A2} "), // Seperator + PATH_COL.italic().paint(path) // Sub-directory + ) + .into() + }; + } + } + + // Replace the home directory path with the 'home symbol': ~ + if abbrev_home { + path = replace_home(&path); + } + if shorten_path { + path = short(&path, slash_limit); + } + + PATH_COL.italic().paint(path) +} diff --git a/src/indicators/ssh.rs b/src/indicators/ssh.rs new file mode 100644 index 0000000..500b5eb --- /dev/null +++ b/src/indicators/ssh.rs @@ -0,0 +1,23 @@ +use ansi_term::ANSIGenericString; +use ansi_term::Colour::RGB; +use std::env::var; // For environment variables + +pub const SSH_SYMBOL: &str = "\u{276F}"; // SSH indicator symbol: "❯" +pub const SSH_COL: ansi_term::Colour = RGB(255, 149, 0); // In SSH session +pub const NORMIE_COL: ansi_term::Colour = RGB(82, 82, 82); // Non-SSH session +const SSH_ENV_VARS: [&str; 2] = ["SSH_TTY", "SSH_CONNECTION"]; // Environment variables normally present in SSH sessions + +/// Checks if current session is an SSH session +fn is_ssh_session() -> bool { + SSH_ENV_VARS.iter().any(|&var_name| var(var_name).is_ok()) // any() iterates through the iter and stops at first `true`. Equal to `||`(`or` condition) +} + +/// SSH shell indicator +pub fn indicator() -> ANSIGenericString<'static, str> { + let is_ssh: bool = is_ssh_session(); + if is_ssh { + SSH_COL.paint(SSH_SYMBOL) + } else { + NORMIE_COL.paint(SSH_SYMBOL) + } +} diff --git a/src/indicators/user.rs b/src/indicators/user.rs new file mode 100644 index 0000000..451303f --- /dev/null +++ b/src/indicators/user.rs @@ -0,0 +1,23 @@ +use ansi_term::ANSIGenericString; +use ansi_term::Colour::RGB; +use std::env::var; // For environment variables + +pub const USER_SYMBOL: &str = "\u{276F}"; // User indicator symbol: "❯" +pub const ROOT_COL: ansi_term::Colour = RGB(255, 53, 94); // If the user is root +pub const NORMIE_COL: ansi_term::Colour = RGB(0, 255, 180); // Regular user +const ROOT_USER: &str = "root"; // Root username constant + +/// Username of current user +pub fn username() -> String { + var("USER").unwrap_or_else(|_| "UnknownUser".to_string()) +} + +/// Root user indicator +pub fn indicator() -> ANSIGenericString<'static, str> { + let user = username(); + if user == ROOT_USER { + ROOT_COL.paint(USER_SYMBOL) + } else { + NORMIE_COL.paint(USER_SYMBOL) + } +} diff --git a/src/main.rs b/src/main.rs index 7bd6c8c..3d6ed5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,161 +1,69 @@ -use colored::{ColoredString, Colorize}; -use std::env::{args, current_dir, var_os}; -use std::process::Command; - -fn get_shell_char(shell: &str) -> String { - let shell_char = match shell { - "bash" | "/bin/bash" | "/usr/bin/bash" | "-bash" => " ", - "zsh" | "/bin/zsh" | "/usr/bin/zsh" | "-zsh" => "󰰶 ", - "fish" => "󰈺 ", - "nushell" => " ", - "ion" => " ", - "oursh" => "󱢇 ", - _ => "󱆃 ", - }; - shell_char.to_string() +mod indicators { + pub mod error; + pub mod git; + pub mod pwd; + pub mod ssh; + pub mod user; } -fn get_git_branch() -> String { - let git_status_cmd = Command::new("git") - .arg("status") - .output() - .expect("git_status_cmd_fail"); - let git_status_output = String::from_utf8_lossy(&git_status_cmd.stdout); - let git_err = String::from_utf8_lossy(&git_status_cmd.stderr); +use crate::indicators::{error, git, pwd, ssh, user}; +use ansi_term::{ANSIGenericString, ANSIGenericStrings}; - if git_err == "" { - git_status_output.split('\n').collect::>()[0] - .split(' ') - .collect::>()[2] - .to_string() +// Add a component to the prompt if the condition is true. +fn add_component( + components: &mut Vec>, // Vector to hold components of the prompt. + condition: bool, // Condition to add the component + component_fn: impl FnOnce() -> ANSIGenericString<'static, str>, // Function to create the component(takes no arguments, returns `ANSIGenericString`) is passed here. +) { + if condition { + components.push(component_fn()); // Push the generated component to the vector. + } +} + +fn main() { + let cmd_args: Vec = std::env::args().collect(); // Command-line args + + // Vector to hold the different parts of the prompt. + let mut components: Vec> = Vec::new(); // The components will be concatenated into a single string in the end. + + // Hard-coded configuration. This will be replaced by a configuration file in a future version + let indicate_user: bool = true; + let indicate_ssh: bool = true; + let indicate_err: bool = true; + let indicate_git_branch: bool = true; + let show_pwd: bool = true; + let abbrev_home: bool = true; + let shorten_path: bool = true; + let replace_repo: bool = true; + + // Conditionally fetch Git-related info if required, or set to None + let git_info: Option<(String, String)> = if indicate_git_branch || replace_repo { + git::info() } else { - String::new() - } -} - -fn get_git_root() -> String { - let git_repo_root_cmd = Command::new("git") - .arg("rev-parse") - .arg("--show-toplevel") - .output() - .expect("git_repo_root_cmd_fail"); - let mut git_repo_path = String::from_utf8_lossy(&git_repo_root_cmd.stdout).to_string(); - let git_repo_err = String::from_utf8_lossy(&git_repo_root_cmd.stderr); - - if git_repo_err == "" { - let len = git_repo_path.trim_end_matches(&['\r', '\n'][..]).len(); - git_repo_path.truncate(len); - } else { - git_repo_path = String::new(); - } - git_repo_path -} - -fn get_git_repo_name(git_repo_root: &str) -> String { - let repo_path_split: Vec<&str> = git_repo_root.split('/').collect(); - let last_index = repo_path_split.len() - 1; - let git_repo_name = repo_path_split[last_index]; - - git_repo_name.to_string() -} - -fn get_git_char(git_branch: &str) -> ColoredString { - match git_branch { - "" => "".clear(), - "main" => " 󰊢 ".truecolor(178, 98, 44), - "master" => " 󰊢 ".truecolor(196, 132, 29), - _ => " 󰊢 ".truecolor(82, 82, 82), - } -} - -fn abrev_path(path: &str) -> String { - let mut short_dir = path.to_string(); - - let slashes = path.matches('/').count(); - - if slashes > 3 { - let parts: Vec<&str> = path.split('/').collect(); - let len = parts.len() - 1; - let mut ch1: String; - - for part in &parts[0..len] { - if part.to_string() != "" { - // to avoid the 1st "/" - ch1 = part.chars().next().expect(part).to_string(); // 1st char of each part - short_dir = short_dir.replace(part, &ch1); - } - } - } - short_dir -} - -fn main() -> std::io::Result<()> { - let angle = "❯"; - - //Root user indicator - let user = var_os("USER") - .expect("UnknownUser") - .to_str() - .expect("UnknownUser") - .to_string(); - - let mut err: String = String::new(); - - let args: Vec = args().collect(); - let shell: String; - if args.len() > 1 { - shell = args[1].clone(); // Shell symbol - if args.len() > 2 { - err.clone_from(&args[2]); // Error status - } - } else { - shell = "none".to_string(); - } - - let root_indicator = match user.as_str() { - "root" => angle.truecolor(255, 53, 94), - _ => angle.truecolor(0, 255, 180), + None }; - let err_indicator = match err.as_str() { - "0" => angle.truecolor(0, 255, 180), - _ => angle.truecolor(255, 53, 94), - }; + // Conditionally add the parts of the prompt + add_component(&mut components, indicate_ssh, ssh::indicator); + add_component(&mut components, indicate_git_branch, || { + git::indicator(git_info.as_ref().map(|info| info.1.clone())) + }); + add_component(&mut components, indicate_user, user::indicator); + add_component(&mut components, indicate_err, || { + error::indicator(&cmd_args) + }); - //SSH shell indicator - let ssh_char: ColoredString = match var_os("SSH_TTY") { - Some(_val) => " ".truecolor(0, 150, 180), - None => "".clear(), - }; + // Insert `pwd` at the beginning of the prompt + if show_pwd { + let repo_path = git_info.map(|info| info.0); + components.insert( + 0, + pwd::pwd(abbrev_home, shorten_path, replace_repo, repo_path), + ); + } - //Git status - let git_branch = get_git_branch(); - let git_repo_root = get_git_root(); - let git_repo_name = get_git_repo_name(&git_repo_root.clone()).truecolor(122, 68, 24); - let git_char = get_git_char(&git_branch); - - //pwd - let homedir = var_os("HOME") - .expect("UnknownDir") - .to_str() - .expect("UnknownDir") - .to_string(); - let pwd = current_dir()?; - let mut cur_dir = pwd.display().to_string(); - cur_dir = cur_dir.replace(&git_repo_root, ""); // Remove git repo root - cur_dir = cur_dir.replace(&homedir, "~"); // Abreviate homedir with "~" - cur_dir = abrev_path(&cur_dir); - - print!( - "{}{}{}{}{}{}{} ", - ssh_char, - get_shell_char(&shell).truecolor(75, 75, 75), - git_repo_name, - git_char, - cur_dir.italic().truecolor(82, 82, 82), - root_indicator, - err_indicator, - ); - - Ok(()) + // Finally, combine the parts into a single prompts string + let prompt: ANSIGenericStrings<'_, str> = ANSIGenericStrings(&components[..]); + // `print!()` prevents an extra newline, unlike `println!()` + print!("{prompt} "); // A trailing space for aesthetic formatting. }