summaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2021-11-15 18:45:58 -0500
committerDan Davison <dandavison7@gmail.com>2021-11-15 21:03:10 -0500
commit45c802528adcb5646ec5fe0d92b8e59119c80fb7 (patch)
treedde0969b8c3f27b4ba431c81550e9a947d4ac064 /src/utils
parent24a07ff9474dc8dc4942026b7d6c3804132cb047 (diff)
Refactor: utils module
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/bat/LICENSE26
-rw-r--r--src/utils/bat/assets.rs144
-rw-r--r--src/utils/bat/dirs.rs41
-rw-r--r--src/utils/bat/less.rs66
-rw-r--r--src/utils/bat/mod.rs5
-rw-r--r--src/utils/bat/output.rs204
-rw-r--r--src/utils/bat/terminal.rs83
-rw-r--r--src/utils/mod.rs4
-rw-r--r--src/utils/process.rs304
-rw-r--r--src/utils/syntect.rs86
10 files changed, 963 insertions, 0 deletions
diff --git a/src/utils/bat/LICENSE b/src/utils/bat/LICENSE
new file mode 100644
index 00000000..8af3b8b8
--- /dev/null
+++ b/src/utils/bat/LICENSE
@@ -0,0 +1,26 @@
+Files under this directory (i.e. src/bat_utils/) originated as copies of
+files from the `bat` project (https://github.com/sharkdp/bat), with
+subsequent modifications, as recorded in git history. The `bat`
+license is reproduced below.
+
+-----------------------------------------------------------------------------
+
+Copyright (c) 2018 bat-developers (https://github.com/sharkdp/bat).
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/utils/bat/assets.rs b/src/utils/bat/assets.rs
new file mode 100644
index 00000000..cfe0f5bd
--- /dev/null
+++ b/src/utils/bat/assets.rs
@@ -0,0 +1,144 @@
+// Based on code from https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468
+// See src/utils/bat/LICENSE
+
+use std::fs::File;
+use std::io::{self, BufReader, Write};
+use std::path::PathBuf;
+
+use ansi_term::Colour::Green;
+use ansi_term::Style;
+use syntect::dumps::{from_binary, from_reader};
+use syntect::highlighting::ThemeSet;
+use syntect::parsing::SyntaxSet;
+
+use crate::errors::*;
+use crate::utils::bat::dirs::PROJECT_DIRS;
+
+pub struct HighlightingAssets {
+ pub syntax_set: SyntaxSet,
+ pub theme_set: ThemeSet,
+}
+
+impl HighlightingAssets {
+ pub fn new() -> Self {
+ Self::from_cache().unwrap_or_else(|_| Self::from_binary())
+ }
+
+ fn get_integrated_syntaxset() -> SyntaxSet {
+ from_binary(include_bytes!("../../../etc/assets/syntaxes.bin"))
+ }
+
+ fn get_integrated_themeset() -> ThemeSet {
+ from_binary(include_bytes!("../../../etc/assets/themes.bin"))
+ }
+
+ fn from_cache() -> Result<Self> {
+ let theme_set_path = theme_set_path();
+ let syntax_set_file = File::open(&syntax_set_path()).chain_err(|| {
+ format!(
+ "Could not load cached syntax set '{}'",
+ syntax_set_path().to_string_lossy()
+ )
+ })?;
+ let syntax_set: SyntaxSet = from_reader(BufReader::new(syntax_set_file))
+ .chain_err(|| "Could not parse cached syntax set")?;
+
+ let theme_set_file = File::open(&theme_set_path).chain_err(|| {
+ format!(
+ "Could not load cached theme set '{}'",
+ theme_set_path.to_string_lossy()
+ )
+ })?;
+ let theme_set: ThemeSet = from_reader(BufReader::new(theme_set_file))
+ .chain_err(|| "Could not parse cached theme set")?;
+
+ Ok(HighlightingAssets {
+ syntax_set,
+ theme_set,
+ })
+ }
+
+ fn from_binary() -> Self {
+ let syntax_set = Self::get_integrated_syntaxset();
+ let theme_set = Self::get_integrated_themeset();
+
+ HighlightingAssets {
+ syntax_set,
+ theme_set,
+ }
+ }
+}
+
+fn theme_set_path() -> PathBuf {
+ PROJECT_DIRS.cache_dir().join("themes.bin")
+}
+
+fn syntax_set_path() -> PathBuf {
+ PROJECT_DIRS.cache_dir().join("syntaxes.bin")
+}
+
+pub fn list_languages() -> std::io::Result<()> {
+ let assets = HighlightingAssets::new();
+ let mut languages = assets
+ .syntax_set
+ .syntaxes()
+ .iter()
+ .filter(|syntax| !syntax.hidden && !syntax.file_extensions.is_empty())
+ .collect::<Vec<_>>();
+ languages.sort_by_key(|lang| lang.name.to_uppercase());
+
+ let loop_through = false;
+ let colored_output = true;
+
+ let stdout = io::stdout();
+ let mut stdout = stdout.lock();
+
+ if loop_through {
+ for lang in languages {
+ writeln!(stdout, "{}:{}", lang.name, lang.file_extensions.join(","))?;
+ }
+ } else {
+ let longest = languages
+ .iter()
+ .map(|syntax| syntax.name.len())
+ .max()
+ .unwrap_or(32); // Fallback width if they have no language definitions.
+
+ let comma_separator = ", ";
+ let separator = " ";
+ // Line-wrapping for the possible file extension overflow.
+ let desired_width = 100;
+
+ let style = if colored_output {
+ Green.normal()
+ } else {
+ Style::default()
+ };
+
+ for lang in languages {
+ write!(stdout, "{:width$}{}", lang.name, separator, width = longest)?;
+
+ // Number of characters on this line so far, wrap before `desired_width`
+ let mut num_chars = 0;
+
+ let mut extension = lang.file_extensions.iter().peekable();
+ while let Some(word) = extension.next() {
+ // If we can't fit this word in, then create a line break and align it in.
+ let new_chars = word.len() + comma_separator.len();
+ if num_chars + new_chars >= desired_width {
+ num_chars = 0;
+ write!(stdout, "\n{:width$}{}", "", separator, width = longest)?;
+ }
+
+ num_chars += new_chars;
+ write!(stdout, "{}", style.paint(&word[..]))?;
+ if extension.peek().is_some() {
+ write!(stdout, "{}", comma_separator)?;
+ }
+ }
+ writeln!(stdout)?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/src/utils/bat/dirs.rs b/src/utils/bat/dirs.rs
new file mode 100644
index 00000000..efaef81f
--- /dev/null
+++ b/src/utils/bat/dirs.rs
@@ -0,0 +1,41 @@
+// Based on code from https://github.com/sharkdp/bat e981e974076a926a38f124b7d8746de2ca5f0a28
+// See src/utils/bat/LICENSE
+
+use lazy_static::lazy_static;
+use std::path::{Path, PathBuf};
+
+#[cfg(target_os = "macos")]
+use std::env;
+
+/// Wrapper for 'dirs' that treats MacOS more like Linux, by following the XDG specification.
+/// This means that the `XDG_CACHE_HOME` and `XDG_CONFIG_HOME` environment variables are
+/// checked first. The fallback directories are `~/.cache/bat` and `~/.config/bat`, respectively.
+pub struct BatProjectDirs {
+ cache_dir: PathBuf,
+}
+
+impl BatProjectDirs {
+ fn new() -> Option<BatProjectDirs> {
+ #[cfg(target_os = "macos")]
+ let cache_dir_op = env::var_os("XDG_CACHE_HOME")
+ .map(PathBuf::from)
+ .filter(|p| p.is_absolute())
+ .or_else(|| dirs_next::home_dir().map(|d| d.join(".cache")));
+
+ #[cfg(not(target_os = "macos"))]
+ let cache_dir_op = dirs_next::cache_dir();
+
+ let cache_dir = cache_dir_op.map(|d| d.join("bat"))?;
+
+ Some(BatProjectDirs { cache_dir })
+ }
+
+ pub fn cache_dir(&self) -> &Path {
+ &self.cache_dir
+ }
+}
+
+lazy_static! {
+ pub static ref PROJECT_DIRS: BatProjectDirs =
+ BatProjectDirs::new().unwrap_or_else(|| panic!("Could not get home directory"));
+}
diff --git a/src/utils/bat/less.rs b/src/utils/bat/less.rs
new file mode 100644
index 00000000..1ca9f76f
--- /dev/null
+++ b/src/utils/bat/less.rs
@@ -0,0 +1,66 @@
+use std::process::Command;
+
+pub fn retrieve_less_version() -> Option<usize> {
+ if let Ok(less_path) = grep_cli::resolve_binary("less") {
+ let cmd = Command::new(less_path).arg("--version").output().ok()?;
+ parse_less_version(&cmd.stdout)
+ } else {
+ None
+ }
+}
+
+fn parse_less_version(output: &[u8]) -> Option<usize> {
+ if output.starts_with(b"less ") {
+ let version = std::str::from_utf8(&output[5..]).ok()?;
+ let end = version.find(|c: char| !c.is_ascii_digit())?;
+ version[..end].parse::<usize>().ok()
+ } else {
+ None
+ }
+}
+
+#[test]
+fn test_parse_less_version_487() {
+ let output = b"less 487 (GNU regular expressions)
+Copyright (C) 1984-2016 Mark Nudelman
+
+less comes with NO WARRANTY, to the extent permitted by law.
+For information about the terms of redistribution,
+see the file named README in the less distribution.
+Homepage: http://www.greenwoodsoftware.com/less";
+
+ assert_eq!(Some(487), parse_less_version(output));
+}
+
+#[test]
+fn test_parse_less_version_529() {
+ let output = b"less 529 (Spencer V8 regular expressions)
+Copyright (C) 1984-2017 Mark Nudelman
+
+less comes with NO WARRANTY, to the extent permitted by law.
+For information about the terms of redistribution,
+see the file named README in the less distribution.
+Homepage: http://www.greenwoodsoftware.com/less";
+
+ assert_eq!(Some(529), parse_less_version(output));
+}
+
+#[test]
+fn test_parse_less_version_551() {
+ let output = b"less 551 (PCRE regular expressions)
+Copyright (C) 1984-2019 Mark Nudelman
+
+less comes with NO WARRANTY, to the extent permitted by law.
+For information about the terms of redistribution,
+see the file named README in the less distribution.
+Home page: http://www.greenwoodsoftware.com/less";
+
+ assert_eq!(Some(551), parse_less_version(output));
+}
+
+#[test]
+fn test_parse_less_version_wrong_program() {
+ let output = b"more from util-linux 2.34";
+
+ assert_eq!(None, parse_less_version(output));
+}
diff --git a/src/utils/bat/mod.rs b/src/utils/bat/mod.rs
new file mode 100644
index 00000000..7812e7c0
--- /dev/null
+++ b/src/utils/bat/mod.rs
@@ -0,0 +1,5 @@
+pub mod assets;
+pub mod dirs;
+mod less;
+pub mod output;
+pub mod terminal;
diff --git a/src/utils/bat/output.rs b/src/utils/bat/output.rs
new file mode 100644
index 00000000..a231168b
--- /dev/null
+++ b/src/utils/bat/output.rs
@@ -0,0 +1,204 @@
+// https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468
+// src/output.rs
+// See src/utils/bat/LICENSE
+use std::env;
+use std::ffi::OsString;
+use std::io::{self, Write};
+use std::path::PathBuf;
+use std::process::{Child, Command, Stdio};
+
+use super::less::retrieve_less_version;
+
+use crate::config;
+use crate::fatal;
+use crate::features::navigate;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[allow(dead_code)]
+pub enum PagingMode {
+ Always,
+ QuitIfOneScreen,
+ Never,
+}
+use crate::errors::*;
+
+pub enum OutputType {
+ Pager(Child),
+ Stdout(io::Stdout),
+}
+
+impl OutputType {
+ pub fn from_mode(
+ mode: PagingMode,
+ pager: Option<String>,
+ config: &config::Config,
+ ) -> Result<Self> {
+ use self::PagingMode::*;
+ Ok(match mode {
+ Always => OutputType::try_pager(false, pager, config)?,
+ QuitIfOneScreen => OutputType::try_pager(true, pager, config)?,
+ _ => OutputType::stdout(),
+ })
+ }
+
+ /// Try to launch the pager. Fall back to stdout in case of errors.
+ fn try_pager(
+ quit_if_one_screen: bool,
+ pager_from_config: Option<String>,
+ config: &config::Config,
+ ) -> Result<Self> {
+ let mut replace_arguments_to_less = false;
+
+ let pager_from_env = match (
+ env::var("DELTA_PAGER"),
+ env::var("BAT_PAGER"),
+ env::var("PAGER"),
+ ) {
+ (Ok(delta_pager), _, _) => Some(delta_pager),
+ (_, Ok(bat_pager), _) => Some(bat_pager),
+ (_, _, Ok(pager)) => {
+ // less needs to be called with the '-R' option in order to properly interpret ANSI
+ // color sequences. If someone has set PAGER="less -F", we therefore need to
+ // overwrite the arguments and add '-R'.
+ // We only do this for PAGER, since it is used in other contexts.
+ replace_arguments_to_less = true;
+ Some(pager)
+ }
+ _ => None,
+ };
+
+ if pager_from_config.is_some() {
+ replace_arguments_to_less = false;
+ }
+
+ let pager = pager_from_config
+ .or(pager_from_env)
+ .unwrap_or_else(|| String::from("less"));
+
+ let pagerflags =
+ shell_words::split(&pager).chain_err(|| "Could not parse pager command.")?;
+
+ Ok(match pagerflags.split_first() {
+ Some((pager_name, args)) => {
+ let pager_path = PathBuf::from(pager_name);
+
+ let is_less = pager_path.file_stem() == Some(&OsString::from("less"));
+
+ let process = if is_less {
+ _make_process_from_less_path(
+ pager_path,
+ args,
+ replace_arguments_to_less,
+ quit_if_one_screen,
+ config,
+ )
+ } else {
+ _make_process_from_pager_path(pager_path, args)
+ };
+ if let Some(mut process) = process {
+ process
+ .stdin(Stdio::piped())
+ .spawn()
+ .map(OutputType::Pager)
+ .unwrap_or_else(|_| OutputType::stdout())
+ } else {
+ OutputType::stdout()
+ }
+ }
+ None => OutputType::stdout(),
+ })
+ }
+
+ fn stdout() -> Self {
+ OutputType::Stdout(io::stdout())
+ }
+
+ pub fn handle(&mut self) -> Result<&mut dyn Write> {
+ Ok(match *self {
+ OutputType::Pager(ref mut command) => command
+ .stdin
+ .as_mut()
+ .chain_err(|| "Could not open stdin for pager")?,
+ OutputType::Stdout(ref mut handle) => handle,
+ })
+ }
+}
+
+fn _make_process_from_less_path(
+ less_path: PathBuf,
+ args: &[String],
+ replace_arguments_to_less: bool,
+ quit_if_one_screen: bool,
+ config: &config::Config,
+) -> Option<Command> {
+ if let Ok(less_path) = grep_cli::resolve_binary(less_path) {
+ let mut p = Command::new(&less_path);
+ if args.is_empty() || replace_arguments_to_less {
+ p.args(vec!["--RAW-CONTROL-CHARS"]);
+
+ // Passing '--no-init' fixes a bug with '--quit-if-one-screen' in older
+ // versions of 'less'. Unfortunately, it also breaks mouse-wheel support.
+ //
+ // See: http://www.greenwoodsoftware.com/less/news.530.html
+ //
+ // For newer versions (530 or 558 on Windows), we omit '--no-init' as it
+ // is not needed anymore.
+ match retrieve_less_version() {
+ None => {
+ p.arg("--no-init");
+ }
+ Some(version) if (version < 530 || (cfg!(windows) && version < 558)) => {
+ p.arg("--no-init");
+ }
+ _ => {}
+ }
+
+ if quit_if_one_screen {
+ p.arg("--quit-if-one-screen");
+ }
+ } else {
+ p.args(args);
+ }
+ p.env("LESSCHARSET", "UTF-8");
+ p.env("LESSANSIENDCHARS", "mK");
+ if config.navigate {
+ if let Ok(hist_file) = navigate::copy_less_hist_file_and_append_navigate_regexp(config)
+ {
+ p.env("LESSHISTFILE", hist_file);
+ if config.show_themes {
+ p.arg("+n");
+ }
+ }
+ }
+ Some(p)
+ } else {
+ None
+ }
+}
+
+fn _make_process_from_pager_path(pager_path: PathBuf, args: &[String]) -> Option<Command> {
+ if pager_path.file_stem() == Some(&OsString::from("delta")) {
+ fatal(
+ "\
+It looks like you have set delta as the value of $PAGER. \
+This would result in a non-terminating recursion. \
+delta is not an appropriate value for $PAGER \
+(but it is an appropriate value for $GIT_PAGER).",
+ );
+ }
+ if let Ok(pager_path) = grep_cli::resolve_binary(pager_path) {
+ let mut p = Command::new(&pager_path);
+ p.args(args);
+ Some(p)
+ } else {
+ None
+ }
+}
+
+impl Drop for OutputType {
+ fn drop(&mut self) {
+ if let OutputType::Pager(ref mut command) = *self {
+ let _ = command.wait();
+ }
+ }
+}
diff --git a/src/utils/bat/terminal.rs b/src/utils/bat/terminal.rs
new file mode 100644
index 00000000..15c75d24
--- /dev/null
+++ b/src/utils/bat/terminal.rs
@@ -0,0 +1,83 @@
+use ansi_term::Color::{self, Fixed, RGB};
+use ansi_term::{self, Style};
+
+use syntect::highlighting::{self, FontStyle};
+
+pub fn to_ansi_color(color: highlighting::Color, true_color: bool) -> Option<ansi_term::Color> {
+ if color.a == 0 {
+ // Themes can specify one of the user-configurable terminal colors by
+ // encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set
+ // to the 8-bit color palette number. The built-in themes ansi, base16,
+ // and base16-256 use this.
+ Some(match color.r {
+ // For the first 8 colors, use the Color enum to produce ANSI escape
+ // sequences using codes 30-37 (foreground) and 40-47 (background).
+ // For example, red foreground is \x1b[31m. This works on terminals
+ // without 256-color support.
+ 0x00 => Color::Black,
+ 0x01 => Color::Red,
+ 0x02 => Color::Green,
+ 0x03 => Color::Yellow,
+ 0x04 => Color::Blue,
+ 0x05 => Color::Purple,
+ 0x06 => Color::Cyan,
+ 0x07 => Color::White,
+ // For all other colors, use Fixed to produce escape sequences using
+ // codes 38;5 (foreground) and 48;5 (background). For example,
+ // bright red foreground is \x1b[38;5;9m. This only works on
+ // terminals with 256-color support.
+ //
+ // TODO: When ansi_term adds support for bright variants using codes
+ // 90-97 (foreground) and 100-107 (background), we should use those
+ // for values 0x08 to 0x0f and only use Fixed for 0x10 to 0xff.
+ n => Fixed(n),
+ })
+ } else if color.a == 1 {
+ // Themes can specify the terminal's default foreground/background color
+ // (i.e. no escape sequence) using the encoding #RRGGBBAA with AA set to
+ // 01. The built-in theme ansi uses this.
+ None
+ } else if true_color {
+ Some(RGB(color.r, color.g, color.b))
+ } else {
+ Some(Fixed(ansi_colours::ansi256_from_rgb((
+ color.r, color.g, color.b,
+ ))))
+ }
+}
+
+#[allow(dead_code)]
+pub fn as_terminal_escaped(
+ style: highlighting::Style,
+ text: &str,
+ true_color: bool,
+ colored: bool,
+ italics: bool,
+ background_color: Option<highlighting::Color>,
+) -> String {
+ if text.is_empty() {
+ return text.to_string();
+ }
+
+ let mut style = if !colored {
+ Style::default()
+ } else {
+ let mut color = Style {
+ foreground: to_ansi_color(style.foreground, true_color),
+ ..Style::default()
+ };
+ if style.font_style.contains(FontStyle::BOLD) {
+ color = color.bold();
+ }
+ if style.font_style.contains(FontStyle::UNDERLINE) {
+ color = color.underline();
+ }
+ if italics && style.font_style.contains(FontStyle::ITALIC) {
+ color = color.italic();
+ }
+ color
+ };
+
+ style.background = background_color.and_then(|c| to_ansi_color(c, true_color));
+ style.paint(text).to_string()
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
new file mode 100644
index 00000000..70026dd3
--- /dev/null
+++ b/src/utils/mod.rs
@@ -0,0 +1,4 @@
+#[cfg(not(tarpaulin_include))]
+pub mod bat;
+pub mod process;
+pub mod syntect;
diff --git a/src/utils/process.rs b/src/utils/process.rs
new file mode 100644
index 00000000..844f9f31
--- /dev/null
+++ b/src/utils/process.rs
@@ -0,0 +1,304 @@
+use std::collections::{HashMap, HashSet};
+use sysinfo::{Pid, Process, ProcessExt, SystemExt};
+
+#[derive(Debug, PartialEq)]
+pub enum GitBlameExtension {
+ Some(String),
+ None,
+ NotGitBlame,
+}
+
+pub fn git_blame_filename_extension() -> Option<String> {
+ let mut info = sysinfo::System::new();
+ let my_pid = std::process::id() as Pid;
+
+ // 1) Try the parent process. If delta is set as the pager in git, then git is the parent process.
+ let parent = parent_process(&mut info, my_pid)?;
+
+ match guess_git_blame_filename_extension(parent.cmd()) {
+ GitBlameExtension::Some(ext) => return Some(ext),
+ GitBlameExtension::None => return None,
+
+ // 2) The parent process was something else, this can happen if git output is piped into delta, e.g.
+ // `git blame foo.txt | delta`. When the shell sets up the pipe it creates the two processes, the pids
+ // are usually consecutive, so check if the proceess with `my_pid - 1` matches.
+ GitBlameExtension::NotGitBlame => {
+ let sibling = naive_sibling_process(&mut info, my_pid);
+ if let Some(proc) = sibling {
+ if let GitBlameExtension::Some(ext) = guess_git_blame_filename_extension(proc.cmd())
+ {
+ return Some(ext);
+ }
+ }
+ // else try the fallback
+ }
+ }
+
+ /*
+ 3) Neither parent nor direct sibling were a match.
+ The most likely case is that the input program of the pipe wrote all its data and exited before delta
+ started, so no file extension can be retrieved. Same if the data was piped from an input file.
+
+ There might also be intermediary scripts in between or piped input with randomized pids, so check all
+ processes for the closest `git blame` in the process tree.
+
+ 100 /usr/bin/some-terminal-emulator
+ 124 \_ -shell
+ 301 | \_ /usr/bin/git blame src/main.rs
+ 302 | \_ wraps_delta.sh
+ 303 | \_ delta
+ 304 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
+ 125 \_ -shell
+ 800 | \_ /usr/bin/git blame src/main.rs
+ 400 | \_ delta
+ 200 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
+ 126 \_ -shell
+ 501 | \_ /bin/sh /wrapper/for/git blame src/main.rs
+ 555 | | \_ /usr/bin/git blame src/main.rs
+ 502 | \_ delta
+ 567 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen
+
+ */
+ find_sibling_process(&mut info, my_pid)
+}
+
+// Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in
+// `skip_this_plus_parameter` plus their respective next argument.
+// Keep all arguments once a '--' is encountered.
+// (Note that some an argument work with and without '=', e.g. '--foo' 'bar' and '--foo=bar')
+fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>(
+ mut args_it: ArgsI,
+ skip_this_plus_parameter: SkipI,
+) -> Vec<&'a str>
+where
+ ArgsI: Iterator<Item = &'a str>,
+ SkipI: Iterator<Item = &'b str>,
+{
+ let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect();
+
+ let mut result = Vec::new();
+ loop {
+ match args_it.next() {
+ None => break result,
+ Some("--") => {
+ result.extend(args_it);
+ break result;
+ }
+ Some(arg) if arg_follows_space.contains(arg) => {
+ let _skip_parameter = args_it.next();
+ }
+ Some(arg) if !arg.starts_with('-') => {
+ result.push(arg);
+ }
+ Some(_) => { /* skip: --these -and --also=this */ }
+ }
+ }
+}
+
+fn guess_git_blame_filename_extension(args: &[String]) -> GitBlameExtension {
+ {
+ let mut it = args.iter();
+ match (it.next(), it.next()) {
+ // git blame or git -C/-c etc. and then (maybe) blame
+ (Some(git), Some(blame))
+ if git.contains("git") && (blame == "blame" || blame.starts_with('-')) => {}
+ _ => return GitBlameExtension::NotGitBlame,
+ }
+ }
+
+ let args = args.iter().skip(2).map(|s| s.as_str());
+
+ // See git(1) and git-blame(1). Some arguments separate their parameter with space or '=', e.g.
+ // --date=2015 or --date 2015.
+ let git_blame_options_with_parameter =
+ "-C -c -L --since --ignore-rev --ignore-revs-file --contents --reverse --date";
+
+ match skip_uninteresting_args(args, git_blame_options_with_parameter.split(' '))
+ .last()
+ .and_then(|&s| s.split('.').last())
+ .map(str::to_owned)
+ {
+ Some(ext) => GitBlameExtension::Some(ext),
+ None => GitBlameExtension::None,
+ }
+}
+
+fn parent_process(info: &mut sysinfo::System, my_pid: Pid) -> Option<&Process> {
+ info.refresh_process(my_pid).then(|| ())?;
+
+ let parent_pid = info.process(my_pid)?.parent()?;
+ info.refresh_process(parent_pid).then(|| ())?;
+ info.process(parent_pid)
+}
+
+fn naive_sibling_process(info: &mut sysinfo::System, my_pid: Pid) -> Option<&Process> {
+ let sibling_pid = my_pid - 1;
+ info.refresh_process(sibling_pid).then(|| ())?;
+ info.process(sibling_pid)
+}
+
+fn iter_parents<F>(info: &sysinfo::System, pid: Pid, distance: usize, mut f: F)
+where
+ F: FnMut(Pid, usize),
+{
+ if let Some(proc) = info.process(pid) {
+ if let Some(pid) = proc.parent() {
+ f(pid, distance);
+ iter_parents(info, pid, distance + 1, f)
+ }
+ }
+}
+
+fn find_sibling_process(info: &mut sysinfo::System, my_pid: Pid) -> Option<String> {
+ info.refresh_processes();
+
+ let this_start_time = info.process(my_pid)?.start_time();
+
+ /*
+
+ $ start_blame_of.sh src/main.rs | delta
+
+ \_ /usr/bin/some-terminal-emulator
+ | \_ common_git_and_delta_ancestor
+ | \_ /bin/sh /opt/git/start_blame_of.sh src/main.rs
+ | | \_ /bin/sh /opt/some/wrapper git blame src/main.rs
+ | | \_ /usr/bin/git blame src/main.rs
+ | \_ /bin/sh /opt/some/wrapper delta
+ | \_ delta
+
+ Walk up the process tree of delta and of every matching other process, counting the steps
+ along the way.
+ Find the common ancestor processes, calculate the distance, and select the one with the shortest.
+
+ */
+
+ let mut pid_distances = HashMap::<Pid, usize>::new();
+ let mut collect_parent_pids = |pid: Pid, distance| {
+ pid_distances.insert(pid, distance);
+ };
+
+ iter_parents(info, my_pid, 1, &mut collect_parent_pids);
+