From 475674a9eaf6487427dd0308b9fc48a0088cba4e Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Tue, 24 Nov 2020 14:49:23 -0500 Subject: [PATCH 1/3] fix pre-existing clippy complaints --- src/modules/git.rs | 21 +++++++++++---------- src/print.rs | 4 ++-- src/sh.rs | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) 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) }; From c2ff2794f523b294065368ad343d30cd6054d7f6 Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Tue, 24 Nov 2020 13:45:40 -0500 Subject: [PATCH 2/3] More robust shell identification logic (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All silver subcommands look up the parent process’s name, and use that, verbatim, to identify the shell in use. This has several problems: - On Windows, the parent process’s name may or may not end with “.exe”; there was special case logic to handle “powershell.exe” versus “powershell” but not “bash.exe” versus “bash”. Relatedly, the parent process’s name ought to be interpreted case- insensitively on Windows; BASH.EXE is the same as bash.exe. - On Unix, there is a notion of a _login_ shell (invoked directly by login(1) for console logins, or sshd(1) for remote logins); these are identified by a leading dash in their process name, *not* by a special option. There was no code to handle this at all (bug #67). - sysinfo is slow, because it reads a whole bunch of files in /proc whether it’s going to need the data within or not. Some but not all of this overhead can be eliminated by using `System::new_with_specifics(RefreshKind::new())` instead of `System::new_all()`, but even then the wall-clock time for `silver init` drops from 0.021s to 0.002s (release build) if sysinfo is not invoked at all. This patch addresses all these problems, as follows: - There is a new command line option `--shell` which may be used to explicitly set the shell in use. Also, support for the `SILVER_SHELL` environment variable (used for this purpose in 1.1 and previous) is restored. sysinfo is only consulted if neither the command line option, nor the environment variable, is available. - No matter where we get the process name from, it is lowercased, a trailing “.exe” is removed, a leading dash is removed, and any leading directory names are also removed (bug #16). - The initialization scripts set `SILVER_SHELL`, so sysinfo will be consulted at most once per session, not once per command. I also reorganized the code in the Command::Init branch of main for readability’s sake, and made the “unknown shell” error message not be a panic. (Panics are for _bugs_, not for errors with a cause outside the program.) --- src/cli.rs | 2 + src/init.bash | 1 + src/init.ion | 3 +- src/init.ps1 | 3 +- src/main.rs | 133 ++++++++++++++++++++++++++++++++++++++------------ 5 files changed, 109 insertions(+), 33 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e7eed99..3c53ba3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,6 +9,8 @@ pub use structopt::StructOpt; pub struct Silver { #[structopt(short, long)] pub config: Option, + #[structopt(short, long)] + pub shell: Option, #[structopt(subcommand)] pub cmd: Command, } 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)| { From 2d8bf5737b8927ea5b21ed9b1f302d2aa0031524 Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Tue, 24 Nov 2020 14:38:55 -0500 Subject: [PATCH 3/3] Improve --help text. All of the options and subcommands now have one-line descriptions, and the after_help text gives a basic explanation of how to set up silver as well as referring to the wiki. --- src/cli.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 3c53ba3..b80e731 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,11 +4,22 @@ 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)] @@ -17,7 +28,10 @@ pub struct Silver { #[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, }