From 0863146f072ae8382be63db26dcf9ddeff967aea Mon Sep 17 00:00:00 2001 From: David Cantrell Date: Fri, 25 Mar 2022 04:10:19 +0000 Subject: [PATCH] feat: Add a module for C projects (#3631) Co-authored-by: David Knaack --- .../presets/toml/bracketed-segments.toml | 3 + .../presets/toml/nerd-font-symbols.toml | 3 + .../presets/toml/plain-text-symbols.toml | 3 + docs/config/README.md | 49 ++++ src/configs/c.rs | 40 +++ src/configs/mod.rs | 3 + src/configs/starship_root.rs | 1 + src/context.rs | 7 + src/module.rs | 1 + src/modules/c.rs | 234 ++++++++++++++++++ src/modules/mod.rs | 3 + src/utils.rs | 24 ++ 12 files changed, 371 insertions(+) create mode 100644 src/configs/c.rs create mode 100644 src/modules/c.rs diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 2d41c3698..5415079c6 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -1,6 +1,9 @@ [aws] format = '\[[$symbol($profile)(\($region\))(\[$duration\])]($style)\]' +[c] +format = '\[[$symbol($version(-$name))]($style)\]' + [cmake] format = '\[[$symbol($version)]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml index c02b354c4..8f7ab9499 100644 --- a/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml +++ b/docs/.vuepress/public/presets/toml/nerd-font-symbols.toml @@ -4,6 +4,9 @@ symbol = " " [buf] symbol = " " +[c] +symbol = " " + [conda] symbol = " " diff --git a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml index f8687356f..7764535c3 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -16,6 +16,9 @@ deleted = "x" [aws] symbol = "aws " +[c] +symbol = "C " + [cobol] symbol = "cobol " diff --git a/docs/config/README.md b/docs/config/README.md index edcdae043..7df46402d 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -207,6 +207,7 @@ $hg_branch\ $docker_context\ $package\ $buf\ +$c\ $cmake\ $cobol\ $container\ @@ -496,6 +497,54 @@ The `buf` module shows the currently installed version of [Buf](https://buf.buil symbol = "🦬 " ``` +## C + +The `c` module shows some information about your C compiler. By default +the module will be shown if the current directory contains a `.c` or `.h` +file. + +### Options + +| Option | Default | Description | +| ------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `format` | `"via [$symbol($version(-$name) )]($style)"` | The format string for the module. | +| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` | +| `symbol` | `"C "` | The symbol used before displaying the compiler details | +| `detect_extensions` | `["c", "h"]` | Which extensions should trigger this module. | +| `detect_files` | `[]` | Which filenames should trigger this module. | +| `detect_folders` | `[]` | Which folders should trigger this module. | +| `commands` | [ [ "cc", "--version" ], [ "gcc", "--version" ], [ "clang", "--version" ] ] | How to detect what the compiler is | +| `style` | `"bold 149"` | The style for the module. | +| `disabled` | `false` | Disables the `c` module. | + +### Variables + +| Variable | Example | Description | +| -------- | ------- | ------------------------------------ | +| name | clang | The name of the compiler | +| version | 13.0.0 | The version of the compiler | +| symbol | | Mirrors the value of option `symbol` | +| style | | Mirrors the value of option `style` | + +NB that `version` is not in the default format. + +### Commands + +The `commands` option accepts a list of commands to determine the compiler version and name. + +Each command is represented as a list of the executable name, followed by its arguments, usually something like `["mycc", "--version"]`. Starship will try executing each command until it gets a result on STDOUT. + +If a C compiler is not supported by this module, you can request it by [raising an issue on GitHub](https://github.com/starship/starship/). + +### Example + +```toml +# ~/.config/starship.toml + +[c] +format = "via [$name $version]($style)" +``` + ## Character The `character` module shows a character (usually an arrow) beside where the text diff --git a/src/configs/c.rs b/src/configs/c.rs new file mode 100644 index 000000000..14a230ff9 --- /dev/null +++ b/src/configs/c.rs @@ -0,0 +1,40 @@ +use crate::config::ModuleConfig; + +use serde::Serialize; +use starship_module_config_derive::ModuleConfig; + +#[derive(Clone, ModuleConfig, Serialize)] +pub struct CConfig<'a> { + pub format: &'a str, + pub version_format: &'a str, + pub style: &'a str, + pub symbol: &'a str, + pub disabled: bool, + pub detect_extensions: Vec<&'a str>, + pub detect_files: Vec<&'a str>, + pub detect_folders: Vec<&'a str>, + pub commands: Vec>, +} + +impl<'a> Default for CConfig<'a> { + fn default() -> Self { + CConfig { + format: "via [$symbol($version(-$name) )]($style)", + version_format: "v${raw}", + style: "149 bold", + symbol: "C ", + disabled: false, + detect_extensions: vec!["c", "h"], + detect_files: vec![], + detect_folders: vec![], + commands: vec![ + // the compiler is usually cc, and --version works on gcc and clang + vec!["cc", "--version"], + // but on some platforms gcc is installed as *gcc*, not cc + vec!["gcc", "--version"], + // for completeness, although I've never seen a clang that wasn't cc + vec!["clang", "--version"], + ], + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index d945af85f..9a36d7481 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -7,6 +7,7 @@ pub mod aws; pub mod azure; pub mod battery; pub mod buf; +pub mod c; pub mod character; pub mod cmake; pub mod cmd_duration; @@ -93,6 +94,7 @@ pub struct FullConfig<'a> { azure: azure::AzureConfig<'a>, battery: battery::BatteryConfig<'a>, buf: buf::BufConfig<'a>, + c: c::CConfig<'a>, character: character::CharacterConfig<'a>, cmake: cmake::CMakeConfig<'a>, cmd_duration: cmd_duration::CmdDurationConfig<'a>, @@ -176,6 +178,7 @@ impl<'a> Default for FullConfig<'a> { azure: Default::default(), battery: Default::default(), buf: Default::default(), + c: Default::default(), character: Default::default(), cmake: Default::default(), cmd_duration: Default::default(), diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index da2c16463..b977d620d 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -36,6 +36,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "package", // ↓ Toolchain version modules ↓ // (Let's keep these sorted alphabetically) + "c", "cmake", "cobol", "dart", diff --git a/src/context.rs b/src/context.rs index 1511aadee..2ae65b8fb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -335,6 +335,13 @@ impl<'a> Context<'a> { Duration::from_millis(self.root_config.command_timeout), ) } + + /// Attempt to execute several commands with exec_cmd, return the results of the first that works + pub fn exec_cmds_return_first(&self, commands: Vec>) -> Option { + commands + .iter() + .find_map(|attempt| self.exec_cmd(attempt[0], &attempt[1..])) + } } #[derive(Debug)] diff --git a/src/module.rs b/src/module.rs index 0bd919818..1ef676743 100644 --- a/src/module.rs +++ b/src/module.rs @@ -13,6 +13,7 @@ pub const ALL_MODULES: &[&str] = &[ #[cfg(feature = "battery")] "battery", "buf", + "c", "character", "cmake", "cmd_duration", diff --git a/src/modules/c.rs b/src/modules/c.rs new file mode 100644 index 000000000..bbe796c95 --- /dev/null +++ b/src/modules/c.rs @@ -0,0 +1,234 @@ +use super::{Context, Module, RootModuleConfig}; + +use crate::configs::c::CConfig; +use crate::formatter::StringFormatter; +use crate::formatter::VersionFormatter; + +use once_cell::sync::Lazy; +use semver::Version; +use std::borrow::Cow; +use std::ops::Deref; + +/// Creates a module with the current C compiler and version +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("c"); + let config: CConfig = CConfig::try_load(module.config); + let is_c_project = context + .try_begin_scan()? + .set_extensions(&config.detect_extensions) + .set_files(&config.detect_files) + .set_folders(&config.detect_folders) + .is_match(); + + if !is_c_project { + return None; + } + + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + let c_compiler_info = Lazy::new(|| context.exec_cmds_return_first(config.commands)); + + formatter + .map_meta(|var, _| match var { + "symbol" => Some(config.symbol), + _ => None, + }) + .map_style(|variable| match variable { + "style" => Some(Ok(config.style)), + _ => None, + }) + .map(|variable| match variable { + "name" => { + let c_compiler_info = &c_compiler_info.deref().as_ref()?.stdout; + + let c_compiler = if c_compiler_info.contains("clang") { + "clang" + } else if c_compiler_info.contains("Free Software Foundation") { + "gcc" + } else { + return None; + }; + Some(c_compiler).map(Cow::Borrowed).map(Ok) + } + "version" => { + let c_compiler_info = &c_compiler_info.deref().as_ref()?.stdout; + + // Clang says ... + // Apple clang version 13.0.0 ...\n + // OpenBSD clang version 11.1.0\n... + // FreeBSD clang version 11.0.1 ...\n + // so we always want the first semver-ish whitespace- + // separated "word". + // gcc says ... + // gcc (OmniOS 151036/9.3.0-il-1) 9.3.0\n... + // gcc (Debian 10.2.1-6) 10.2.1 ...\n + // cc (GCC) 3.3.5 (Debian 1:3.3.5-13)\n... + // so again we always want the first semver-ish word. + VersionFormatter::format_module_version( + module.get_name(), + c_compiler_info.split_whitespace().find_map( + |word| match Version::parse(word) { + Ok(_v) => Some(word), + Err(_e) => None, + }, + )?, + config.version_format, + ) + .map(Cow::Owned) + .map(Ok) + } + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `c`:\n{}", error); + return None; + } + }); + + Some(module) +} + +#[cfg(test)] +mod tests { + use crate::{test::ModuleRenderer, utils::CommandOutput}; + use ansi_term::Color; + use std::fs::File; + use std::io; + + #[test] + fn folder_without_c_files() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let actual = ModuleRenderer::new("c").path(dir.path()).collect(); + + let expected = None; + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_c_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("any.c"))?.sync_all()?; + + // What happens when `cc --version` says it's modern gcc? + // The case when it claims to be clang is covered in folder_with_h_file, + // and uses the mock in src/test/mod.rs. + let actual = ModuleRenderer::new("c") + .cmd( + "cc --version", + Some(CommandOutput { + stdout: String::from( + "\ +cc (Debian 10.2.1-6) 10.2.1 20210110 +Copyright (C) 2020 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.", + ), + stderr: String::default(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("C v10.2.1-gcc ") + )); + assert_eq!(expected, actual); + + // What happens when `cc --version` says it's ancient gcc? + let actual = ModuleRenderer::new("c") + .cmd( + "cc --version", + Some(CommandOutput { + stdout: String::from( + "\ +cc (GCC) 3.3.5 (Debian 1:3.3.5-13) +Copyright (C) 2003 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.", + ), + stderr: String::default(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("C v3.3.5-gcc ") + )); + assert_eq!(expected, actual); + + // What happens with an unknown C compiler? Needless to say, we're + // not running on a Z80 so we're never going to see this one in reality! + let actual = ModuleRenderer::new("c") + .cmd( + "cc --version", + Some(CommandOutput { + stdout: String::from("HISOFT-C Compiler V1.2\nCopyright © 1984 HISOFT"), + stderr: String::default(), + }), + ) + .path(dir.path()) + .collect(); + let expected = Some(format!("via {}", Color::Fixed(149).bold().paint("C "))); + assert_eq!(expected, actual); + + // What happens when 'cc --version' doesn't work, but 'gcc --version' does? + // This stubs out `cc` but we'll fall back to `gcc --version` as defined in + // src/test/mod.rs. + // Also we don't bother to redefine the config for this one, as we've already + // proved we can parse its version. + let actual = ModuleRenderer::new("c") + .cmd("cc --version", None) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("C v10.2.1-gcc ") + )); + assert_eq!(expected, actual); + + // Now with both 'cc' and 'gcc' not working, this should fall back to 'clang --version' + let actual = ModuleRenderer::new("c") + .cmd("cc --version", None) + .cmd("gcc --version", None) + .path(dir.path()) + .collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("C v11.1.0-clang ") + )); + assert_eq!(expected, actual); + + // What happens when we can't find any of cc, gcc or clang? + let actual = ModuleRenderer::new("c") + .cmd("cc --version", None) + .cmd("gcc --version", None) + .cmd("clang --version", None) + .path(dir.path()) + .collect(); + let expected = Some(format!("via {}", Color::Fixed(149).bold().paint("C "))); + assert_eq!(expected, actual); + + dir.close() + } + + #[test] + fn folder_with_h_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("any.h"))?.sync_all()?; + + let actual = ModuleRenderer::new("c").path(dir.path()).collect(); + let expected = Some(format!( + "via {}", + Color::Fixed(149).bold().paint("C v11.0.1-clang ") + )); + assert_eq!(expected, actual); + dir.close() + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 4bdcc2348..67a6df5c3 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -2,6 +2,7 @@ mod aws; mod azure; mod buf; +mod c; mod character; mod cmake; mod cmd_duration; @@ -93,6 +94,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { #[cfg(feature = "battery")] "battery" => battery::module(context), "buf" => buf::module(context), + "c" => c::module(context), "character" => character::module(context), "cmake" => cmake::module(context), "cmd_duration" => cmd_duration::module(context), @@ -184,6 +186,7 @@ pub fn description(module: &str) -> &'static str { "azure" => "The current Azure subscription", "battery" => "The current charge of the device's battery and its current charging status", "buf" => "The currently installed version of the Buf CLI", + "c" => "Your C compiler type", "character" => { "A character (usually an arrow) beside where the text is entered in your terminal" } diff --git a/src/utils.rs b/src/utils.rs index b6820124d..11279100c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -133,6 +133,30 @@ pub fn mock_cmd + Debug, U: AsRef + Debug>( stdout: String::from("1.0.0"), stderr: String::default(), }), + "cc --version" => Some(CommandOutput { + stdout: String::from("\ +FreeBSD clang version 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3fe) +Target: x86_64-unknown-freebsd13.0 +Thread model: posix +InstalledDir: /usr/bin"), + stderr: String::default(), + }), + "gcc --version" => Some(CommandOutput { + stdout: String::from("\ +cc (Debian 10.2.1-6) 10.2.1 20210110 +Copyright (C) 2020 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."), + stderr: String::default(), + }), + "clang --version" => Some(CommandOutput { + stdout: String::from("\ +OpenBSD clang version 11.1.0 +Target: amd64-unknown-openbsd7.0 +Thread model: posix +InstalledDir: /usr/bin"), + stderr: String::default(), + }), "cobc -version" => Some(CommandOutput { stdout: String::from("\ cobc (GnuCOBOL) 3.1.2.0