feat(c++): adding c++ module (#6570)

This commit is contained in:
Siddharth Kumar Jha
2025-04-27 01:42:35 +05:30
committed by GitHub
parent daf8d93d27
commit ed07646099
20 changed files with 618 additions and 102 deletions
+123 -2
View File
@@ -134,7 +134,7 @@
},
"allOf": [
{
"$ref": "#/definitions/CConfig"
"$ref": "#/definitions/CcConfig_for_CConfigMarker"
}
]
},
@@ -241,6 +241,47 @@
}
]
},
"cpp": {
"default": {
"commands": [
[
"c++",
"--version"
],
[
"g++",
"--version"
],
[
"clang++",
"--version"
]
],
"detect_extensions": [
"cpp",
"cc",
"cxx",
"c++",
"hpp",
"hh",
"hxx",
"h++",
"tcc"
],
"detect_files": [],
"detect_folders": [],
"disabled": true,
"format": "via [$symbol($version(-$name) )]($style)",
"style": "149 bold",
"symbol": "C++ ",
"version_format": "v${raw}"
},
"allOf": [
{
"$ref": "#/definitions/CcConfig_for_CppConfigMarker"
}
]
},
"crystal": {
"default": {
"detect_extensions": [
@@ -2272,7 +2313,7 @@
},
"additionalProperties": false
},
"CConfig": {
"CcConfig_for_CConfigMarker": {
"type": "object",
"properties": {
"format": {
@@ -2583,6 +2624,86 @@
},
"additionalProperties": false
},
"CcConfig_for_CppConfigMarker": {
"type": "object",
"properties": {
"format": {
"default": "via [$symbol($version(-$name) )]($style)",
"type": "string"
},
"version_format": {
"default": "v${raw}",
"type": "string"
},
"style": {
"default": "149 bold",
"type": "string"
},
"symbol": {
"default": "C++ ",
"type": "string"
},
"disabled": {
"default": true,
"type": "boolean"
},
"detect_extensions": {
"default": [
"cpp",
"cc",
"cxx",
"c++",
"hpp",
"hh",
"hxx",
"h++",
"tcc"
],
"type": "array",
"items": {
"type": "string"
}
},
"detect_files": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"detect_folders": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"commands": {
"default": [
[
"c++",
"--version"
],
[
"g++",
"--version"
],
[
"clang++",
"--version"
]
],
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"additionalProperties": false
},
"CrystalConfig": {
"type": "object",
"properties": {
+48
View File
@@ -705,6 +705,54 @@ If a C compiler is not supported by this module, you can request it by [raising
format = 'via [$name $version]($style)'
```
## CPP
The `cpp` module shows some information about your `C++` compiler. By default,
the module will be shown if the current directory contains a `.cpp`, `.hpp`, or other `C++`-related files.
### 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` | `['cpp', 'cc', 'cxx', 'c++', 'hpp', 'hh', 'hxx', 'h++', 'tcc']` | Which extensions should trigger this module. |
| `detect_files` | `[]` | Which filenames should trigger this module. |
| `detect_folders` | `[]` | Which folders should trigger this module. |
| `commands` | `[ [ 'c++', '--version' ], [ 'g++', '--version' ], [ 'clang++', '--version' ] ]` | How to detect what the compiler is |
| `style` | `'bold 149'` | The style for the module. |
| `disabled` | `true` | Disables the `cpp` 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 `['mycpp', '--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
[cpp]
disabled = false
format = 'via [$name $version]($style)'
```
## Character
The `character` module shows a character (usually an arrow) beside where the text
@@ -7,6 +7,9 @@ format = '\[[$symbol($version)]($style)\]'
[c]
format = '\[[$symbol($version(-$name))]($style)\]'
[cpp]
format = '\[[$symbol($version(-$name))]($style)\]'
[cmake]
format = '\[[$symbol($version)]($style)\]'
@@ -11,6 +11,7 @@ $git_branch\
$git_status\
[](fg:color_aqua bg:color_blue)\
$c\
$cpp\
$rust\
$golang\
$nodejs\
@@ -106,6 +107,11 @@ symbol = " "
style = "bg:color_blue"
format = '[[ $symbol( $version) ](fg:color_fg0 bg:color_blue)]($style)'
[cpp]
symbol = " "
style = "bg:color_blue"
format = '[[ $symbol( $version) ](fg:color_fg0 bg:color_blue)]($style)'
[rust]
symbol = ""
style = "bg:color_blue"
+5
View File
@@ -29,6 +29,7 @@ $pijul_channel\
$docker_context\
$package\
$c\
$cpp\
$cmake\
$cobol\
$daml\
@@ -258,6 +259,10 @@ format = " [buf](italic) [$symbol $version $buf_version]($style)"
symbol = " "
format = " [$symbol($version(-$name))]($style)"
[cpp]
symbol = " "
format = " [$symbol($version(-$name))]($style)"
[conda]
symbol = "◯ "
format = " conda [$symbol$environment]($style)"
@@ -10,6 +10,9 @@ symbol = " "
[c]
symbol = " "
[cpp]
symbol = " "
[cmake]
symbol = " "
@@ -7,6 +7,9 @@ format = '(via [$symbol($version )]($style))'
[c]
format = '(via [$symbol($version(-$name) )]($style))'
[cpp]
format = '(via [$symbol($version(-$name) )]($style))'
[cmake]
format = '(via [$symbol($version )]($style))'
@@ -69,6 +69,11 @@ symbol = " "
style = "bg:#86BBD8"
format = '[ $symbol ($version) ]($style)'
[cpp]
symbol = " "
style = "bg:#86BBD8"
format = '[ $symbol ($version) ]($style)'
[docker_context]
symbol = " "
style = "bg:#06969A"
@@ -28,6 +28,9 @@ symbol = "bun "
[c]
symbol = "C "
[cpp]
symbol = "C++ "
[cobol]
symbol = "cobol "
+8 -14
View File
@@ -1,27 +1,21 @@
use crate::configs::cc::CcConfig;
use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Serialize)]
#[derive(Default, Clone, Deserialize, Serialize)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
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<Vec<&'a str>>,
}
pub struct CConfigMarker;
pub type CConfig<'a> = CcConfig<'a, CConfigMarker>;
impl Default for CConfig<'_> {
fn default() -> Self {
CConfig {
CcConfig {
marker: std::marker::PhantomData::<CConfigMarker>,
format: "via [$symbol($version(-$name) )]($style)",
version_format: "v${raw}",
style: "149 bold",
+24
View File
@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields),
schemars(bound = "CcConfig<'a, T>: std::default::Default, T: schemars::JsonSchema")
)]
#[serde(default)]
pub struct CcConfig<'a, T> {
#[serde(skip)]
pub marker: std::marker::PhantomData<T>,
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<Vec<&'a str>>,
}
+34
View File
@@ -0,0 +1,34 @@
use crate::configs::cc::CcConfig;
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Deserialize, Serialize)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
pub struct CppConfigMarker;
pub type CppConfig<'a> = CcConfig<'a, CppConfigMarker>;
impl Default for CppConfig<'_> {
fn default() -> Self {
CcConfig {
marker: std::marker::PhantomData::<CppConfigMarker>,
format: "via [$symbol($version(-$name) )]($style)",
version_format: "v${raw}",
style: "149 bold",
symbol: "C++ ",
disabled: true,
detect_extensions: vec!["cpp", "cc", "cxx", "c++", "hpp", "hh", "hxx", "h++", "tcc"],
detect_files: vec![],
detect_folders: vec![],
commands: vec![
vec!["c++", "--version"],
vec!["g++", "--version"],
vec!["clang++", "--version"],
],
}
}
}
+4
View File
@@ -7,12 +7,14 @@ pub mod battery;
pub mod buf;
pub mod bun;
pub mod c;
pub mod cc;
pub mod character;
pub mod cmake;
pub mod cmd_duration;
pub mod cobol;
pub mod conda;
pub mod container;
pub mod cpp;
pub mod crystal;
pub mod custom;
pub mod daml;
@@ -140,6 +142,8 @@ pub struct FullConfig<'a> {
#[serde(borrow)]
container: container::ContainerConfig<'a>,
#[serde(borrow)]
cpp: cpp::CppConfig<'a>,
#[serde(borrow)]
crystal: crystal::CrystalConfig<'a>,
#[serde(borrow)]
daml: daml::DamlConfig<'a>,
+1
View File
@@ -57,6 +57,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"c",
"cmake",
"cobol",
"cpp",
"daml",
"dart",
"deno",
+1
View File
@@ -20,6 +20,7 @@ pub const ALL_MODULES: &[&str] = &[
"cobol",
"conda",
"container",
"cpp",
"crystal",
"daml",
"dart",
+3 -86
View File
@@ -1,92 +1,9 @@
use super::{Context, Module, ModuleConfig};
use crate::configs::c::CConfig;
use crate::formatter::StringFormatter;
use crate::formatter::VersionFormatter;
use semver::Version;
use std::borrow::Cow;
use std::ops::Deref;
use std::sync::LazyLock;
use super::{Context, Module};
use crate::modules::cc::{Lang, module as cc_module};
/// Creates a module with the current C compiler and version
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
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 = LazyLock::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(Ok(Cow::Borrowed(c_compiler)))
}
"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(|word| Version::parse(word).is_ok())?,
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)
cc_module(context, Lang::C)
}
#[cfg(test)]
+130
View File
@@ -0,0 +1,130 @@
use super::{Context, Module, ModuleConfig};
use crate::configs::c::{CConfig, CConfigMarker};
use crate::configs::cc::CcConfig;
use crate::configs::cpp::{CppConfig, CppConfigMarker};
use crate::formatter::StringFormatter;
use crate::formatter::VersionFormatter;
use crate::segment::Segment;
use semver::Version;
use std::borrow::Cow;
use std::ops::Deref;
use std::sync::LazyLock;
pub enum Lang {
C,
Cpp,
}
fn is_cc_project<T>(config: &CcConfig<T>, context: &Context) -> Option<bool> {
Some(
context
.try_begin_scan()?
.set_extensions(&config.detect_extensions)
.set_files(&config.detect_files)
.set_folders(&config.detect_folders)
.is_match(),
)
}
fn parse_module<T>(
context: &Context,
module: &mut Module,
config: CcConfig<T>,
compilers: [(&str, &str); 2],
) -> Result<Vec<Segment>, crate::formatter::string_formatter::StringFormatterError> {
StringFormatter::new(config.format).and_then(|formatter| {
let cc_compiler_info = LazyLock::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 cc_compiler_info = &cc_compiler_info.deref().as_ref()?.stdout;
let cc_compiler =
compilers
.iter()
.find_map(|(compiler_name, compiler_hint)| {
cc_compiler_info
.contains(compiler_hint)
.then_some(*compiler_name)
})?;
Some(Ok(Cow::Borrowed(cc_compiler)))
}
"version" => {
let cc_compiler_info = &cc_compiler_info.deref().as_ref()?.stdout;
VersionFormatter::format_module_version(
module.get_name(),
cc_compiler_info
.split_whitespace()
.find(|word| Version::parse(word).is_ok())?,
config.version_format,
)
.map(Cow::Owned)
.map(Ok)
}
_ => None,
})
.parse(None, Some(context))
})
}
fn create_module<'a, T>(
context: &'a Context,
lang: &str,
compilers: [(&str, &str); 2],
mut module: Module<'a>,
config: CcConfig<T>,
) -> Option<Module<'a>> {
if config.disabled {
return None;
}
if !is_cc_project(&config, context)? {
return None;
}
let parsed = parse_module(context, &mut module, config, compilers);
module.set_segments(match parsed {
Ok(segments) => segments,
Err(error) => {
log::warn!("Error in module `cc` for `lang: {}` :\n{}", lang, error);
return None;
}
});
Some(module)
}
pub fn module<'a>(context: &'a Context, lang: Lang) -> Option<Module<'a>> {
match lang {
Lang::C => {
let lang = "c";
let compilers = [("clang", "clang"), ("gcc", "Free Software Foundation")];
let module = context.new_module(lang);
let config = CConfig::try_load(module.config);
create_module::<CConfigMarker>(context, lang, compilers, module, config)
}
Lang::Cpp => {
let lang = "cpp";
let compilers = [("clang++", "clang"), ("g++", "Free Software Foundation")];
let module = context.new_module(lang);
let config = CppConfig::try_load(module.config);
create_module::<CppConfigMarker>(context, lang, compilers, module, config)
}
}
}
+180
View File
@@ -0,0 +1,180 @@
use super::{Context, Module};
use crate::modules::cc::{Lang, module as cc_module};
/// Creates a module with the current C compiler and version
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
cc_module(context, Lang::Cpp)
}
#[cfg(test)]
mod tests {
use crate::{test::ModuleRenderer, utils::CommandOutput};
use nu_ansi_term::Color;
use std::fs::File;
use std::io;
#[test]
fn folder_without_cpp_files() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.path(dir.path())
.collect();
let expected = None;
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn folder_with_cpp_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("any.cpp"))?.sync_all()?;
// What happens when `c++ --version` says it's modern clang++?
// The case when it claims to be g++ is covered in folder_with_hpp_file,
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.cmd(
"c++ --version",
Some(CommandOutput {
stdout: String::from(
"\
clang version 19.1.7
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin",
),
stderr: String::default(),
}),
)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C++ v19.1.7-clang++ ")
));
assert_eq!(expected, actual);
// What happens when `c++ --version` says it's ancient gcc?
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.cmd(
"c++ --version",
Some(CommandOutput {
stdout: String::from(
"\
c++ (GCC) 3.3.5
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-g++ ")
));
assert_eq!(expected, actual);
// What happens with an unknown C++ compiler?
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.cmd(
"c++ --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 'c++ --version' doesn't work, but 'g++ --version' does?
// This stubs out `c++` but we'll fall back to `g++ --version`
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.cmd("c++ --version", None)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C++ v14.2.1-g++ ")
));
assert_eq!(expected, actual);
// Now with both 'c++' and 'g++' not working, this should fall back to 'clang++ --version'
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.cmd("c++ --version", None)
.cmd("g++ --version", None)
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C++ v19.1.7-clang++ ")
));
assert_eq!(expected, actual);
// What happens when we can't find any of c++, g++ or clang++?
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.cmd("c++ --version", None)
.cmd("g++ --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_hpp_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("any.hpp"))?.sync_all()?;
let actual = ModuleRenderer::new("cpp")
.config(toml::toml! {
[cpp]
disabled = false
})
.path(dir.path())
.collect();
let expected = Some(format!(
"via {}",
Color::Fixed(149).bold().paint("C++ v14.2.1-g++ ")
));
assert_eq!(expected, actual);
dir.close()
}
}
+4
View File
@@ -4,12 +4,14 @@ mod azure;
mod buf;
mod bun;
mod c;
mod cc;
mod character;
mod cmake;
mod cmd_duration;
mod cobol;
mod conda;
mod container;
mod cpp;
mod crystal;
pub mod custom;
mod daml;
@@ -125,6 +127,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
"cobol" => cobol::module(context),
"conda" => conda::module(context),
"container" => container::module(context),
"cpp" => cpp::module(context),
"daml" => daml::module(context),
"dart" => dart::module(context),
"deno" => deno::module(context),
@@ -251,6 +254,7 @@ pub fn description(module: &str) -> &'static str {
"cobol" => "The currently installed version of COBOL/GNUCOBOL",
"conda" => "The current conda environment, if $CONDA_DEFAULT_ENV is set",
"container" => "The container indicator, if inside a container.",
"cpp" => "your cpp compiler type",
"crystal" => "The currently installed version of Crystal",
"daml" => "The Daml SDK version of your project",
"dart" => "The currently installed version of Dart",
+30
View File
@@ -197,6 +197,36 @@ warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
OpenBSD clang version 11.1.0
Target: amd64-unknown-openbsd7.0
Thread model: posix
InstalledDir: /usr/bin",
),
stderr: String::default(),
}),
"c++ --version" => Some(CommandOutput {
stdout: String::from(
"\
c++ (GCC) 14.2.1 20240910
Copyright (C) 2024 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(),
}),
"g++ --version" => Some(CommandOutput {
stdout: String::from(
"\
g++ (GCC) 14.2.1 20240910
Copyright (C) 2024 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(
"\
clang version 19.1.7
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin",
),
stderr: String::default(),