diff --git a/src/cli.rs b/src/cli.rs index e7eed99..b80e731 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,18 +4,34 @@ pub use structopt::StructOpt; #[structopt( about = "a cross-shell customizable powerline-like prompt with icons", name = "silver", - after_help = "https://github.com/reujab/silver/wiki" + after_help = r#"To use silver, put `eval $(silver init)` or equivalent in your shell's +interactive startup file. The `lprint` and `rprint` subcommands are +used by the code emitted by `silver init`; you should not need to use +them directly. + +silver expects a configuration file in $XDG_CONFIG_HOME/silver/config.toml +on Unix, or {FOLDERID_RoamingAppData}/silver/config/config.toml on Windows. +(This path can be overridden with the --config option.) +See for documentation of this file. +"# )] pub struct Silver { + /// Path of the configuration file to use. #[structopt(short, long)] pub config: Option, + /// Name of your shell (e.g. bash). + #[structopt(short, long)] + pub shell: Option, #[structopt(subcommand)] pub cmd: Command, } #[derive(StructOpt, Debug)] pub enum Command { + /// Emit shell code to set up silver. Init, + /// Print the left side of a prompt. Lprint, + /// Print the right side of a prompt. Rprint, } diff --git a/src/init.bash b/src/init.bash index f356215..2cd580f 100644 --- a/src/init.bash +++ b/src/init.bash @@ -2,4 +2,5 @@ PROMPT_COMMAND=silver_prompt silver_prompt() { PS1="$(code=$? jobs=$(jobs -p | wc -l) silver lprint)" } +export SILVER_SHELL="@SILVER_SHELL@" export VIRTUAL_ENV_DISABLE_PROMPT=1 diff --git a/src/init.ion b/src/init.ion index 1b2ce24..c157c09 100644 --- a/src/init.ion +++ b/src/init.ion @@ -1,4 +1,5 @@ fn PROMPT env code=$? jobs=$(jobs -p | wc -l) silver lprint end -export VIRTUAL_ENV_DISABLE_PROMPT = 1 # Doesn't make any sense yet +export SILVER_SHELL = "@SILVER_SHELL@" +export VIRTUAL_ENV_DISABLE_PROMPT = 1 diff --git a/src/init.ps1 b/src/init.ps1 index 59ed790..16595ae 100644 --- a/src/init.ps1 +++ b/src/init.ps1 @@ -6,4 +6,5 @@ function prompt { Start-Process -Wait -NoNewWindow silver lprint "$([char]0x1b)[0m" } -$Env:VIRTUAL_ENV_DISABLE_PROMPT = 1 +$env:SILVER_SHELL = "@SILVER_SHELL@" +$env:VIRTUAL_ENV_DISABLE_PROMPT = 1 diff --git a/src/main.rs b/src/main.rs index bd535df..37564fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,11 @@ mod sh; use cli::*; use once_cell::sync::{Lazy, OnceCell}; -use std::path::{Path, PathBuf}; -use sysinfo::{get_current_pid, ProcessExt, System, SystemExt}; +use std::{ + env, + path::{Path, PathBuf}, +}; +use sysinfo::{get_current_pid, ProcessExt, RefreshKind, System, SystemExt}; static CONFIG_PATH: OnceCell = OnceCell::new(); @@ -21,6 +24,10 @@ static CONFIG: Lazy = Lazy::new(|| { .expect("Failed to read config") }); +const INIT_BASH: &str = include_str!("init.bash"); +const INIT_PS1: &str = include_str!("init.ps1"); +const INIT_ION: &str = include_str!("init.ion"); + #[derive(Clone, Debug)] pub struct Segment { background: String, @@ -38,47 +45,111 @@ impl Default for Segment { } } -fn main() { - let sys = System::new_all(); - let process = sys.get_process(get_current_pid().unwrap()).unwrap(); - let parent = sys.get_process(process.parent().unwrap()).unwrap(); - let shell = parent.name().trim(); +/// Helper function for trimming a String in place. Derived from +/// https://users.rust-lang.org/t/trim-string-in-place/15809/9 +fn replace_with_subslice(this: &mut String, f: F) +where + F: FnOnce(&str) -> &str, +{ + let original_len = this.len(); + let new_slice: &str = f(this); + let start = (new_slice.as_ptr() as usize).wrapping_sub(this.as_ptr() as usize); + if original_len < start { + this.clear(); + return; + } + + let len = new_slice.len(); + if start != 0 { + this.drain(..start); + } + this.truncate(len); +} + +/// Identify the type of shell in use. +fn get_shell(opt: &cli::Silver) -> String { + let mut shell: String = if let Some(ref s) = opt.shell { + // If --shell was given on the command line, use that. + s.clone() + } else if let Ok(s) = env::var("SILVER_SHELL") { + // For backward compatibility with 1.1 and earlier, + // use the value of the SILVER_SHELL environment variable. + s + } else { + // Use the name of the parent process, if we can. + // Minimize the amount of information loaded by sysinfo. + // FIXME: Proper error handling, not all these unwraps. + let mut sys = System::new_with_specifics(RefreshKind::new()); + // It'd be nice if either the stdlib or sysinfo exposed + // getppid() directly, but they don't. + let mypid = get_current_pid().unwrap(); + sys.refresh_process(mypid); + let parentpid = sys.get_process(mypid).unwrap().parent().unwrap(); + sys.refresh_process(parentpid); + sys.get_process(parentpid).unwrap().name().to_string() + }; + + // For Windows compatibility, lowercase the shell's name + // ("BASH.EXE" is the same as "bash.exe"). + shell.make_ascii_lowercase(); + + // Remove any leading or trailing whitespace from `shell`. + // Remove anything up to and including the last '/' or '\\', + // in case the shell was identified by absolute path. + // Remove a trailing ".exe" if present, for Windows compatibility. + // Remove a leading "-" if present; on Unix this is a flag indicating + // a login shell (see login(1) and sh(1)), not part of the name. + // These are done unconditionally, not just when we are using + // the name of the parent process to identify the shell, because + // the installation instructions for older versions of silver + // said to set SILVER_SHELL to "$0" without trimming anything + // from it. + replace_with_subslice(&mut shell, |s| { + let s = s.trim(); + let s = s.rsplit(&['/', '\\'][..]).next().unwrap_or(s); + let s = s.strip_prefix('-').unwrap_or(s); + let s = s.strip_suffix(".exe").unwrap_or(s); + s + }); + + shell +} + +fn main() { let opt = cli::Silver::from_args(); - if let Some(path) = opt.config { + if let Some(ref path) = opt.config { let path = Path::new(path.as_str()).canonicalize().unwrap(); CONFIG_PATH.set(path).unwrap() } + let _shell = get_shell(&opt); + let shell = _shell.as_str(); + match opt.cmd { Command::Init => { - print!( - "{}", - match shell { - "bash" => include_str!("init.bash"), - "powershell" | "pwsh" | "powershell.exe" | "pwsh.exe" => - include_str!("init.ps1"), - "ion" => include_str!("init.ion"), - _ => - panic!( - "unknown shell: \"{}\". Supported shells: bash, ion, powershell", - shell - ), + let script = match shell { + "bash" => INIT_BASH, + "powershell" | "pwsh" => INIT_PS1, + "ion" => INIT_ION, + _ => { + use std::process::exit; + eprintln!("silver: unknown shell: \"{}\".", shell); + eprintln!("silver: supported shells: bash, ion, powershell"); + exit(1); } - .replace( + }; + let script = script.replace("@SILVER_SHELL@", shell); + let script = if let Some(path) = CONFIG_PATH.get() { + script.replace( "silver", - format!( - "silver{}", - if let Some(path) = CONFIG_PATH.get() { - format!(" --config {}", path.display()) - } else { - String::new() - } - ) - .as_str() + format!("silver --config {}", path.display()).as_str(), ) - ) + } else { + script + }; + print!("{}", script); } Command::Lprint => { print::prompt(&shell, &CONFIG.left, |_, (_, c, n)| { diff --git a/src/modules/git.rs b/src/modules/git.rs index bcb0db5..480c352 100644 --- a/src/modules/git.rs +++ b/src/modules/git.rs @@ -4,17 +4,18 @@ use std::path::Path; use url::Url; pub fn segment(segment: &mut Segment, args: &[&str]) { + let cwd = std::env::current_dir().unwrap(); for dir in CONFIG.git.ignore_dirs.iter() { - if std::env::current_dir().unwrap() - == Path::new( - &shellexpand::full_with_context_no_errors(dir, dirs::home_dir, |s| { - std::env::var(s).map(Some).unwrap_or_default() - }) - .into_owned(), - ) - .canonicalize() - .unwrap() - { + let expanded = Path::new( + &shellexpand::full_with_context_no_errors(dir, dirs::home_dir, |s| { + std::env::var(s).map(Some).unwrap_or_default() + }) + .into_owned(), + ) + .canonicalize() + .unwrap(); + + if cwd == expanded { return; } } diff --git a/src/print.rs b/src/print.rs index 3e371b4..50ce996 100644 --- a/src/print.rs +++ b/src/print.rs @@ -1,14 +1,14 @@ use crate::{config, modules, sh, Segment}; use std::iter::once; -pub fn prompt(shell: &str, args: &Vec, f: T) +pub fn prompt(shell: &str, args: &[config::Segment], f: T) where T: Fn(usize, (&Segment, &Segment, &Segment)) -> U, U: IntoIterator, { let v: Vec<_> = once(Segment::default()) .chain( - args.into_iter() + args.iter() .map(|arg| { let mut segment = Segment { background: arg.color.background.to_string(), diff --git a/src/sh.rs b/src/sh.rs index 41f42e7..93f2473 100644 --- a/src/sh.rs +++ b/src/sh.rs @@ -7,8 +7,8 @@ lazy_static! { } fn code(color: &str, prefix: &str, light_prefix: &str) -> Option { - let (color, prefix) = if color.starts_with("light") { - (&color[5..], light_prefix) + let (color, prefix) = if let Some(stripped) = color.strip_prefix("light") { + (stripped, light_prefix) } else { (color, prefix) };