feat(config): add a configuration file (#12)

This commit adds a configuration file (`systeroid.conf`) for
configuring the CLI/TUI settings. Consult README.md or the
file itself for more information.
This commit is contained in:
Orhun Parmaksız 2022-08-09 22:19:33 +02:00
parent 570fdf9c2d
commit 907b337158
No known key found for this signature in database
GPG Key ID: F83424824B3E4B90
20 changed files with 577 additions and 136 deletions

View File

@ -4,6 +4,7 @@
/target/
/.cargo/
/assets/
/config/
# Files
.editorconfig

74
Cargo.lock generated
View File

@ -8,6 +8,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
@ -186,6 +197,15 @@ dependencies = [
"once_cell",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@ -196,6 +216,17 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@ -216,6 +247,12 @@ dependencies = [
"libloading",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "downcast-rs"
version = "1.2.0"
@ -288,6 +325,15 @@ dependencies = [
"walkdir",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -491,6 +537,16 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "ordered-multimap"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown",
]
[[package]]
name = "parseit"
version = "0.1.0"
@ -606,6 +662,16 @@ version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
name = "rust-ini"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "ryu"
version = "1.0.10"
@ -735,10 +801,12 @@ name = "systeroid-core"
version = "0.1.1"
dependencies = [
"colored",
"dirs",
"dirs-next",
"lazy_static",
"parseit",
"rayon",
"rust-ini",
"serde",
"serde_json",
"sysctl",
@ -831,6 +899,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.2"

View File

@ -93,6 +93,7 @@ Although **systeroid** does not need the parameter section to be specified expli
- [Changing the colors](#changing-the-colors)
- [Viewing the parameter documentation](#viewing-the-parameter-documentation)
- [Setting the refresh rate](#setting-the-refresh-rate)
- [Configuration](#configuration)
- [Resources](#resources)
- [References](#references)
- [Logo](#logo)
@ -215,6 +216,7 @@ systeroid [options] [variable[=value] ...] --load[=<file>]
-P, --no-pager do not pipe output into a pager
-v, --verbose enable verbose logging
--tui show terminal user interface
-c, --config <path> set the path of the configuration file
-h, --help display this help and exit (-d)
-V, --version output version information and exit
```
@ -416,6 +418,7 @@ systeroid-tui [options]
set the foreground color [default: white]
-n, --no-docs do not show the kernel documentation
--deprecated include deprecated variables while listing
-c, --config <path> set the path of the configuration file
-h, --help display this help and exit
-V, --version output version information and exit
```
@ -548,6 +551,28 @@ It is possible to specify a value in milliseconds via `--tick-rate` argument for
systeroid-tui --tick-rate 500
```
## Configuration
**systeroid** can be configured with a configuration file that uses the [INI format](https://en.wikipedia.org/wiki/INI_file). It can be specified via `--config` or `SYSTEROID_CONFIG` environment variable. It can also be placed in one of the following global locations:
- `$HOME/.config/systeroid/systeroid.conf`
- `$HOME/.systeroid/systeroid.conf`
```sh
# set the config path via argument
systeroid --config config/systeroid.conf
# set the config path via env
SYSTEROID_CONFIG=config/systeroid.conf systeroid
# use a global path
mkdir -p "$HOME/.config/systeroid"
cp config/systeroid.conf "$HOME/.config/systeroid"
systeroid
```
See the example [systeroid.conf](./config/systeroid.conf) for the configuration options.
## Resources
### References

66
config/systeroid.conf Normal file
View File

@ -0,0 +1,66 @@
; systeroid ~ configuration file
; https://github.com/orhun/systeroid
;
; Each line either contains a comment or a command line argument grouped under a section.
; Run "systeroid --help" or "systeroid-tui --help" to get a list of all possible configuration options.
[general]
; display the deprecated parameters such as base_reachable_time and retrans_time while listing
; See https://bugzilla.redhat.com/show_bug.cgi?id=152435
display_deprecated = false
; path of the Linux kernel documentation
; this is distro dependent, systeroid checks the following locations as default:
; - /usr/share/doc/linux/
; - /usr/share/doc/linux-doc/
; - /usr/share/doc/linux-docs/
; - /usr/share/doc/kernel-doc-*/Documentation/
kernel_docs = "/usr/share/doc/linux"
[cli]
; enable verbose logging
verbose = false
; ignore unknown variable errors
ignore_errors = true
; do not print variable after the value is set
quiet = false
; do not pipe output into a pager
; note that the default pager is less(1) and you can change it by using `PAGER` environment variable
no_pager = false
; display type for the parameter, available options are:
; - default: print the parameter name along with its value
; - name: print only the name of the parameter
; - value: print only the value of the parameter
; - binary: print only the value of the parameter without new line
display_type = "default"
; output type for the list, available options are:
; - default: print the output as is
; - tree: print the output in a tree-like format
; - json: print the output in JSON format
output_type = "default"
[cli.colors]
; available colors are defined in https://docs.rs/colored/latest/colored/enum.Color.html
; default color for the symbols
default_color = "bright black"
; section colors
section_abi = "red"
section_fs = "green"
section_kernel = "magenta"
section_net = "blue"
section_sunrpc = "yellow"
section_user = "cyan"
section_vm = "bright red"
section_unknown = "white"
[tui]
; tick rate of the terminal
tick_rate = 250
; disable showing the parameter documentation
no_docs = true
[tui.colors]
; available colors are defined in https://docs.rs/tui/latest/tui/style/enum.Color.html
; terminal foreground color
fg_color = "white"
; terminal background color
bg_color = "black"

View File

@ -21,3 +21,5 @@ serde = { version = "1.0.140", features = ["derive"] }
serde_json = "1.0.82"
dirs-next = "2.0.0"
parseit = { version = "0.1.0", features = ["gzip"] }
rust-ini = "0.18.0"
dirs = "4.0.0"

View File

@ -1,9 +1,26 @@
use crate::sysctl::display::DisplayType;
use crate::error::Result;
use crate::sysctl::r#type::{DisplayType, OutputType};
use crate::sysctl::section::Section;
use colored::Color;
use ini::Ini;
use std::collections::HashMap;
use std::path::PathBuf;
/* Macro for the concise initialization of HashMap */
/// Default configuration file.
pub const DEFAULT_CONFIG: &str = "systeroid.conf";
/// Environment variable for setting the path of the configuration file.
pub const CONFIG_ENV: &str = "SYSTEROID_CONFIG";
lazy_static! {
/// Default locations for the configuration file.
pub static ref DEFAULT_CONFIG_PATHS: Vec<Option<PathBuf>> = vec![
dirs::config_dir().map(|p| p.join("systeroid").join(DEFAULT_CONFIG)),
dirs_next::home_dir().map(|p| p.join(".systeroid").join(DEFAULT_CONFIG)),
];
}
/// Macro for the concise initialization of HashMap
macro_rules! map {
($( $key: expr => $val: expr ),*) => {{
let mut map = ::std::collections::HashMap::new();
@ -12,47 +29,227 @@ macro_rules! map {
}}
}
/// Macro for parsing a boolean value from INI format
macro_rules! parse_ini_flag {
($self: ident, $config: ident, $section: ident, $name: ident) => {
if let Some($name) = $section.get(stringify!($name)) {
$self.$config.$name = $name == "true";
}
};
}
/// Configuration.
#[derive(Clone, Debug)]
pub struct Config {
/// Whether if the deprecated variables should be included while listing.
pub display_deprecated: bool,
/// Path of the Linux kernel documentation.
pub kernel_docs: Option<PathBuf>,
/// CLI configuration.
pub cli: CliConfig,
/// TUI configuration.
pub tui: TuiConfig,
}
/// CLI configuration.
#[derive(Clone, Debug)]
pub struct CliConfig {
/// Whether if the verbose logging is enabled.
pub verbose: bool,
/// Whether if the errors should be ignored.
pub ignore_errors: bool,
/// Whether if the deprecated variables should be included while listing.
pub display_deprecated: bool,
/// Whether if the quiet mode is enabled.
pub quiet: bool,
/// Whether if the pager is disabled.
pub no_pager: bool,
/// Sections and the corresponding colors.
pub section_colors: HashMap<Section, Color>,
/// Default color for the output
pub default_color: Color,
/// Display type of the kernel parameters.
pub display_type: DisplayType,
/// Output type of the application.
pub output_type: OutputType,
/// Color configuration.
pub color: CliColorConfig,
}
/// CLI color configuration.
#[derive(Clone, Debug)]
pub struct CliColorConfig {
/// Default color for the output
pub default_color: Color,
/// Sections and the corresponding colors.
pub section_colors: HashMap<Section, Color>,
}
/// TUI configuration.
#[derive(Clone, Debug)]
pub struct TuiConfig {
/// Refresh rate of the terminal.
pub tick_rate: u64,
/// Do not parse/show Linux kernel documentation.
pub no_docs: bool,
/// Color configuration.
pub color: TuiColorConfig,
}
/// TUI color configuration.
#[derive(Clone, Debug)]
pub struct TuiColorConfig {
/// Foreground color.
pub fg_color: String,
/// Background color.
pub bg_color: String,
}
impl Config {
/// Parses the configuration file and overrides values.
pub fn parse(&mut self, path: Option<PathBuf>) -> Result<()> {
let mut config_paths = DEFAULT_CONFIG_PATHS.clone();
if path.is_some() {
config_paths.insert(0, path);
}
let mut config_path = None;
for path in config_paths.into_iter().flatten() {
if path.exists() {
config_path = Some(path);
break;
}
}
if let Some(path) = config_path {
let ini = Ini::load_from_file(path)?;
if let Some(general_section) = ini.section(Some("general")) {
if let Some(display_deprecated) = general_section.get("display_deprecated") {
self.display_deprecated = display_deprecated == "true";
}
if let Some(kernel_docs) = general_section.get("kernel_docs") {
self.kernel_docs = Some(PathBuf::from(kernel_docs));
}
}
if let Some(section) = ini.section(Some("cli")) {
parse_ini_flag!(self, cli, section, verbose);
parse_ini_flag!(self, cli, section, ignore_errors);
parse_ini_flag!(self, cli, section, quiet);
parse_ini_flag!(self, cli, section, no_pager);
if let Some(display_type) = section.get("display_type").map(DisplayType::from) {
self.cli.display_type = display_type;
}
if let Some(output_type) = section.get("output_type").map(OutputType::from) {
self.cli.output_type = output_type;
}
}
if let Some(section) = ini.section(Some("cli.colors")) {
if let Some(default_color) = section
.get("default_color")
.and_then(|v| Color::try_from(v).ok())
{
self.cli.color.default_color = default_color;
}
for (key, value) in section.iter() {
if key.starts_with("section_") {
if let (sysctl_section, Some(color)) = (
Section::from(key.trim_start_matches("section_").to_string()),
Color::try_from(value).ok(),
) {
self.cli.color.section_colors.insert(sysctl_section, color);
}
}
}
}
if let Some(section) = ini.section(Some("tui")) {
if let Some(tick_rate) = section.get("tick_rate").and_then(|v| v.parse().ok()) {
self.tui.tick_rate = tick_rate;
}
parse_ini_flag!(self, tui, section, no_docs);
}
if let Some(section) = ini.section(Some("tui.colors")) {
if let Some(fg_color) = section.get("fg_color") {
self.tui.color.fg_color = fg_color.to_string();
}
if let Some(bg_color) = section.get("bg_color") {
self.tui.color.bg_color = bg_color.to_string();
}
}
}
Ok(())
}
}
impl Default for Config {
fn default() -> Self {
Self {
verbose: false,
ignore_errors: false,
display_deprecated: false,
quiet: false,
no_pager: false,
section_colors: map! {
Section::Abi => Color::Red,
Section::Fs => Color::Green,
Section::Kernel => Color::Magenta,
Section::Net => Color::Blue,
Section::Sunrpc => Color::Yellow,
Section::User => Color::Cyan,
Section::Vm => Color::BrightRed,
Section::Unknown => Color::White
kernel_docs: None,
cli: CliConfig {
verbose: false,
ignore_errors: false,
quiet: false,
no_pager: false,
display_type: DisplayType::Default,
output_type: OutputType::Default,
color: CliColorConfig {
default_color: Color::BrightBlack,
section_colors: map! {
Section::Abi => Color::Red,
Section::Fs => Color::Green,
Section::Kernel => Color::Magenta,
Section::Net => Color::Blue,
Section::Sunrpc => Color::Yellow,
Section::User => Color::Cyan,
Section::Vm => Color::BrightRed,
Section::Unknown => Color::White
},
},
},
tui: TuiConfig {
tick_rate: 250,
no_docs: false,
color: TuiColorConfig {
fg_color: String::from("white"),
bg_color: String::from("black"),
},
},
default_color: Color::BrightBlack,
display_type: DisplayType::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config() -> Result<()> {
let mut config = Config::default();
config.display_deprecated = true;
config.cli.display_type = DisplayType::Value;
config.cli.color.default_color = Color::Blue;
config.cli.color.section_colors = HashMap::new();
config.tui.tick_rate = 3000;
config.tui.color.fg_color = String::new();
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("config")
.join(DEFAULT_CONFIG);
config.parse(Some(path))?;
assert_eq!(
Config::default().display_deprecated,
config.display_deprecated
);
assert_eq!(
Some(PathBuf::from("/usr/share/doc/linux")),
config.kernel_docs
);
assert_eq!(Config::default().cli.display_type, config.cli.display_type);
assert_eq!(
Config::default().cli.color.default_color,
config.cli.color.default_color
);
assert_eq!(
Config::default().cli.color.section_colors,
config.cli.color.section_colors
);
assert_eq!(Config::default().tui.tick_rate, config.tui.tick_rate);
assert_eq!(
Config::default().tui.color.fg_color,
config.tui.color.fg_color
);
Ok(())
}
}

View File

@ -24,6 +24,9 @@ pub enum Error {
/// Error that may occur while handling sysctl operations.
#[error("sysctl error: `{0}`")]
SysctlError(#[from] sysctl::SysctlError),
/// Error that may occur while parsing an INI document.
#[error("INI parsing error: `{0}`")]
IniError(#[from] ini::Error),
}
/// Type alias for the standard [`Result`] type.

View File

@ -10,7 +10,7 @@ use parseit::globwalk;
use rayon::prelude::*;
use std::convert::TryFrom;
use std::env;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::result::Result as StdResult;
use sysctl::{CtlFlags, CtlIter, Sysctl as SysctlImpl};
@ -47,7 +47,7 @@ impl Sysctl {
}
}
Err(e) => {
if config.verbose {
if config.cli.verbose {
eprintln!("{} ({})", e, ctl.name()?);
}
}
@ -73,7 +73,7 @@ impl Sysctl {
|| param.get_absolute_name() == Some(&query.replace('/', "."))
})
.collect::<Vec<&Parameter>>();
if parameters.is_empty() && !self.config.ignore_errors {
if parameters.is_empty() && !self.config.cli.ignore_errors {
eprintln!(
"{}: cannot stat {}{}: No such file or directory",
env!("CARGO_PKG_NAME").split('-').collect::<Vec<_>>()[0],
@ -85,12 +85,8 @@ impl Sysctl {
}
/// Updates the descriptions of the kernel parameters using the given cached data.
pub fn update_docs_from_cache(
&mut self,
kernel_docs: Option<&PathBuf>,
cache: &Cache,
) -> Result<()> {
let mut kernel_docs_path = if let Some(path) = kernel_docs {
pub fn update_docs_from_cache(&mut self, cache: &Cache) -> Result<()> {
let mut kernel_docs_path = if let Some(path) = &self.config.kernel_docs {
vec![path.to_path_buf()]
} else {
Vec::new()
@ -197,7 +193,7 @@ mod tests {
);
assert!(sysctl.get_parameters("---").is_empty());
sysctl.update_docs_from_cache(None, &Cache::init()?)?;
sysctl.update_docs_from_cache(&Cache::init()?)?;
let parameter = sysctl
.get_parameter("kernel.hostname")

View File

@ -1,18 +0,0 @@
/// Possible ways of displaying the kernel parameters.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DisplayType {
/// Print the kernel parameter name along with its value.
Default,
/// Print only the name of the parameter.
Name,
/// Print only the value of the parameter.
Value,
/// Print only the value of the parameter without new line.
Binary,
}
impl Default for DisplayType {
fn default() -> Self {
Self::Default
}
}

View File

@ -4,8 +4,8 @@ pub mod controller;
/// Sysctl section.
pub mod section;
/// Sysctl display options.
pub mod display;
/// Sysctl display/output options.
pub mod r#type;
/// Kernel parameter.
pub mod parameter;

View File

@ -1,6 +1,6 @@
use crate::config::Config;
use crate::error::Result;
use crate::sysctl::display::DisplayType;
use crate::sysctl::r#type::DisplayType;
use crate::sysctl::section::Section;
use colored::*;
use serde::{Deserialize, Serialize};
@ -53,9 +53,11 @@ impl Parameter {
/// Returns the parameter name with corresponding section colors.
pub fn get_colored_name(&self, config: &Config) -> String {
let section_color = *(config
.cli
.color
.section_colors
.get(&self.section)
.unwrap_or(&config.default_color));
.unwrap_or(&config.cli.color.default_color));
let fields = self.name.split('.').collect::<Vec<&str>>();
fields
.iter()
@ -66,7 +68,7 @@ impl Parameter {
result,
"{}{}",
v.color(section_color),
".".color(config.default_color)
".".color(config.cli.color.default_color)
);
} else {
result += v;
@ -75,14 +77,16 @@ impl Parameter {
})
}
/// Returns the components of the parameter to contruct a [`Tree`].
/// Returns the components of the parameter to construct a [`Tree`].
///
/// [`Tree`]: crate::tree::Tree
pub fn get_tree_components(&self, config: &Config) -> Vec<String> {
let section_color = *(config
.cli
.color
.section_colors
.get(&self.section)
.unwrap_or(&config.default_color));
.unwrap_or(&config.cli.color.default_color));
let mut components = self
.name
.split('.')
@ -95,11 +99,11 @@ impl Parameter {
.for_each(|(i, component)| {
if i != total_components - 1 {
*component = component.color(section_color).to_string();
} else if config.display_type != DisplayType::Name {
} else if config.cli.display_type != DisplayType::Name {
*component = format!(
"{} {} {}",
component,
"=".color(config.default_color),
"=".color(config.cli.color.default_color),
self.value.replace('\n', " ").bold()
);
}
@ -109,7 +113,7 @@ impl Parameter {
/// Prints the kernel parameter to given output.
pub fn display_value<Output: Write>(&self, config: &Config, output: &mut Output) -> Result<()> {
match config.display_type {
match config.cli.display_type {
DisplayType::Name => {
writeln!(output, "{}", self.get_colored_name(config))?;
}
@ -125,7 +129,7 @@ impl Parameter {
output,
"{} {} {}",
self.get_colored_name(config),
"=".color(config.default_color),
"=".color(config.cli.color.default_color),
value.bold(),
)?;
}
@ -187,7 +191,7 @@ impl Parameter {
let ctl = Ctl::new(&self.name)?;
let new_value = ctl.set_value_string(new_value)?;
self.value = new_value;
if !config.quiet {
if !config.cli.quiet {
self.display_value(config, output)?;
}
Ok(())
@ -211,10 +215,12 @@ mod tests {
assert_eq!(Some("test_param"), parameter.get_absolute_name());
let mut config = Config {
default_color: Color::White,
..Default::default()
};
config.cli.color.default_color = Color::White;
*(config
.cli
.color
.section_colors
.get_mut(&Section::Kernel)
.expect("failed to get color")) = Color::Yellow;
@ -237,7 +243,7 @@ mod tests {
);
output.clear();
config.display_type = DisplayType::Name;
config.cli.display_type = DisplayType::Name;
parameter.display_value(&config, &mut output)?;
assert_eq!(
"kernel.fictional.test_param\n",
@ -245,12 +251,12 @@ mod tests {
);
output.clear();
config.display_type = DisplayType::Value;
config.cli.display_type = DisplayType::Value;
parameter.display_value(&config, &mut output)?;
assert_eq!("1\n", String::from_utf8_lossy(&output));
output.clear();
config.display_type = DisplayType::Binary;
config.cli.display_type = DisplayType::Binary;
parameter.display_value(&config, &mut output)?;
assert_eq!("1", String::from_utf8_lossy(&output));

View File

@ -0,0 +1,73 @@
/// Macro for generating enum type with a Default variant and common implementations.
macro_rules! gen_type_property {
($name: ident,
$($variant: ident,)+
) => {
/// Enum containing variants.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum $name {
/// Default variant.
Default,
$(
/// Variant.
$variant
),+
}
impl<'a> From<&'a str> for $name {
fn from(value: &'a str) -> Self {
for section in Self::variants() {
if value.to_lowercase() == section.to_string() {
return *section;
}
}
Self::Default
}
}
impl Default for $name {
fn default() -> Self {
Self::Default
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "{}", format!("{:?}", self).to_lowercase())
}
}
impl $name {
/// Returns the variants.
pub fn variants() -> &'static [Self] {
&[Self::Default, $(Self::$variant),+]
}
}
};
}
gen_type_property!(DisplayType, Name, Value, Binary,);
gen_type_property!(OutputType, Tree, Json,);
#[cfg(test)]
mod tests {
#[test]
fn test_gen_type() {
gen_type_property!(TestType, One, Two, Three,);
assert_eq!(TestType::Two, TestType::from("two"));
assert_eq!(TestType::Two, TestType::from("TwO"));
assert_eq!(TestType::Default, TestType::from("tw0"));
assert_eq!(TestType::Default, TestType::default());
assert_eq!("three", &TestType::Three.to_string());
assert_eq!("one", &TestType::One.to_string());
assert_eq!(
&[
TestType::Default,
TestType::One,
TestType::Two,
TestType::Three
],
TestType::variants()
);
}
}

View File

@ -1,7 +1,7 @@
use crate::style::Colors;
use getopts::Options;
use std::env;
use std::path::PathBuf;
use systeroid_core::config::CONFIG_ENV;
use systeroid_core::sysctl::section::Section;
use systeroid_core::sysctl::KERNEL_DOCS_ENV;
@ -18,6 +18,8 @@ For more details see {bin}(8)."#;
/// Command-line arguments.
#[derive(Debug, Default)]
pub struct Args {
/// Location of the configuration file.
pub config: Option<PathBuf>,
/// Refresh rate of the terminal.
pub tick_rate: u64,
/// Path of the Linux kernel documentation.
@ -26,8 +28,10 @@ pub struct Args {
pub section: Option<Section>,
/// Query to search on startup.
pub search_query: Option<String>,
/// Background/foreground colors.
pub colors: Colors,
/// Foreground color.
pub fg_color: String,
/// Background color.
pub bg_color: String,
/// Do not parse/show Linux kernel documentation.
pub no_docs: bool,
/// Whether if the deprecated variables should be included while listing.
@ -70,6 +74,12 @@ impl Args {
"deprecated",
"include deprecated variables while listing",
);
opts.optopt(
"c",
"config",
"set the path of the configuration file",
"<path>",
);
opts.optflag("h", "help", "display this help and exit");
opts.optflag("V", "version", "output version information and exit");
opts
@ -106,14 +116,18 @@ impl Args {
.map(PathBuf::from),
section: matches.opt_str("s").map(Section::from),
search_query: matches.opt_str("q"),
colors: Colors::new(
matches.opt_str("bg-color").as_deref().unwrap_or("black"),
matches.opt_str("fg-color").as_deref().unwrap_or("white"),
)
.map_err(|e| eprintln!("error: `{}`", e))
.ok()?,
fg_color: matches
.opt_str("fg-color")
.unwrap_or_else(|| String::from("white")),
bg_color: matches
.opt_str("bg-color")
.unwrap_or_else(|| String::from("black")),
no_docs: matches.opt_present("n"),
display_deprecated: matches.opt_present("deprecated"),
config: matches
.opt_str("c")
.or_else(|| env::var(CONFIG_ENV).ok())
.map(PathBuf::from),
})
}
}

View File

@ -26,6 +26,7 @@ use crate::args::Args;
use crate::command::Command;
use crate::error::Result;
use crate::event::{Event, EventHandler};
use crate::style::Colors;
use systeroid_core::cache::Cache;
use systeroid_core::config::Config;
use systeroid_core::sysctl::controller::Sysctl;
@ -34,18 +35,25 @@ use tui::terminal::Terminal;
/// Runs `systeroid-tui`.
pub fn run<B: Backend>(args: Args, backend: B) -> Result<()> {
let config = Config {
let mut config = Config {
display_deprecated: args.display_deprecated,
kernel_docs: args.kernel_docs,
..Default::default()
};
config.tui.tick_rate = args.tick_rate;
config.tui.no_docs = args.no_docs;
config.tui.color.fg_color = args.fg_color;
config.tui.color.bg_color = args.bg_color;
config.parse(args.config)?;
let colors = Colors::new(&config.tui.color.bg_color, &config.tui.color.fg_color)?;
let mut sysctl = Sysctl::init(config)?;
if !args.no_docs {
sysctl.update_docs_from_cache(args.kernel_docs.as_ref(), &Cache::init()?)?;
if !sysctl.config.tui.no_docs {
sysctl.update_docs_from_cache(&Cache::init()?)?;
}
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
terminal.clear()?;
let event_handler = EventHandler::new(args.tick_rate);
let event_handler = EventHandler::new(sysctl.config.tui.tick_rate);
let mut app = App::new(&mut sysctl);
if let Some(section) = args.section {
app.section_list.state.select(Some(
@ -63,7 +71,7 @@ pub fn run<B: Backend>(args: Args, backend: B) -> Result<()> {
app.input = None;
}
while app.running {
terminal.draw(|frame| ui::render(frame, &mut app, &args.colors))?;
terminal.draw(|frame| ui::render(frame, &mut app, &colors))?;
match event_handler.next()? {
Event::KeyPress(key) => {
let command = Command::parse(key, app.is_input_mode());
@ -91,6 +99,8 @@ mod tests {
fn test_systeroid_tui() -> Result<()> {
let args = Args {
tick_rate: 1000,
fg_color: String::from("white"),
bg_color: String::from("black"),
..Args::default()
};
let backend = TestBackend::new(40, 10);

View File

@ -1,4 +1,3 @@
use crate::output::OutputType;
use std::env;
use std::io::{self, BufRead, Write};
use std::path::PathBuf;
@ -9,6 +8,7 @@ use systeroid_core::parseit::reader;
use systeroid_core::parseit::regex::Regex;
use systeroid_core::sysctl::controller::Sysctl;
use systeroid_core::sysctl::parameter::Parameter;
use systeroid_core::sysctl::r#type::OutputType;
use systeroid_core::sysctl::{DEPRECATED_PARAMS, SYSTEM_PRELOAD};
use systeroid_core::tree::{Tree, TreeNode};
@ -19,18 +19,12 @@ pub struct App<'a, Output: Write> {
sysctl: &'a mut Sysctl,
/// Standard output.
output: &'a mut Output,
/// Output type.
output_type: OutputType,
}
impl<'a, Output: Write> App<'a, Output> {
/// Constructs a new instance.
pub fn new(sysctl: &'a mut Sysctl, output: &'a mut Output, output_type: OutputType) -> Self {
Self {
sysctl,
output,
output_type,
}
pub fn new(sysctl: &'a mut Sysctl, output: &'a mut Output) -> Self {
Self { sysctl, output }
}
/// Prints the given parameters to stdout.
@ -38,7 +32,7 @@ impl<'a, Output: Write> App<'a, Output> {
where
I: Iterator<Item = &'b Parameter>,
{
match self.output_type {
match self.sysctl.config.cli.output_type {
OutputType::Default => {
parameters.try_for_each(|parameter| {
parameter.display_value(&self.sysctl.config, self.output)
@ -54,7 +48,8 @@ impl<'a, Output: Write> App<'a, Output> {
.map(|v| v.as_ref()),
);
});
Tree::new(root_node.childs).print(self.output, self.sysctl.config.default_color)?;
Tree::new(root_node.childs)
.print(self.output, self.sysctl.config.cli.color.default_color)?;
}
OutputType::Json => {
Parameter::display_bulk_json(parameters.collect(), self.output)?;
@ -81,7 +76,7 @@ impl<'a, Output: Write> App<'a, Output> {
/// Displays the documentation of a parameter.
pub fn display_documentation(&mut self, param_name: &str) -> Result<()> {
let no_pager = self.sysctl.config.no_pager;
let no_pager = self.sysctl.config.cli.no_pager;
for parameter in self.sysctl.get_parameters(param_name) {
let mut fallback_to_default = false;
if no_pager {
@ -231,13 +226,12 @@ mod tests {
#[test]
fn test_app() -> Result<()> {
let mut output = Vec::new();
let mut sysctl = Sysctl::init(Config {
no_pager: true,
..Config::default()
})?;
sysctl.update_docs_from_cache(None, &Cache::init()?)?;
let mut config = Config::default();
config.cli.no_pager = true;
let mut sysctl = Sysctl::init(config)?;
sysctl.update_docs_from_cache(&Cache::init()?)?;
let mut app = App::new(&mut sysctl, &mut output, OutputType::Default);
let mut app = App::new(&mut sysctl, &mut output);
app.display_parameters(Regex::new("kernel|vm").ok(), false)?;
let result = String::from_utf8_lossy(app.output);
@ -245,7 +239,7 @@ mod tests {
assert!(result.contains("kernel.version ="));
app.output.clear();
app.output_type = OutputType::Tree;
app.sysctl.config.cli.output_type = OutputType::Tree;
app.display_parameters(None, false)?;
assert!(String::from_utf8_lossy(app.output).contains("─ osrelease ="));
app.output.clear();
@ -255,7 +249,7 @@ mod tests {
app.output.clear();
let param_name = String::from("kernel.version");
app.output_type = OutputType::Default;
app.sysctl.config.cli.output_type = OutputType::Default;
app.process_parameter(param_name.clone(), true, false)?;
let result = String::from_utf8_lossy(app.output);
assert_eq!(1, result.lines().count());
@ -263,7 +257,7 @@ mod tests {
app.output.clear();
let param_name = String::from("kernel.version");
app.output_type = OutputType::Json;
app.sysctl.config.cli.output_type = OutputType::Json;
app.process_parameter(param_name.clone(), true, false)?;
let result = String::from_utf8_lossy(app.output);
assert!(result.contains("\"section\":\"kernel\""));

View File

@ -1,9 +1,10 @@
use crate::output::OutputType;
use getopts::Options;
use std::env;
use std::path::PathBuf;
use systeroid_core::config::CONFIG_ENV;
use systeroid_core::parseit::regex::Regex;
use systeroid_core::sysctl::display::DisplayType;
use systeroid_core::sysctl::r#type::DisplayType;
use systeroid_core::sysctl::r#type::OutputType;
use systeroid_core::sysctl::{DEFAULT_PRELOAD, KERNEL_DOCS_ENV};
/// Help message for the arguments.
@ -19,6 +20,8 @@ For more details see {bin}(8)."#;
/// Command-line arguments.
#[derive(Debug, Default)]
pub struct Args {
/// Location of the configuration file.
pub config: Option<PathBuf>,
/// Whether if the verbose logging is enabled.
pub verbose: bool,
/// Whether if the quiet mode is enabled.
@ -97,6 +100,12 @@ impl Args {
opts.optflag("P", "no-pager", "do not pipe output into a pager");
opts.optflag("v", "verbose", "enable verbose logging");
opts.optflag("", "tui", "show terminal user interface");
opts.optopt(
"c",
"config",
"set the path of the configuration file",
"<path>",
);
opts.optflag("h", "help", "display this help and exit (-d)");
opts.optflag("V", "version", "output version information and exit");
opts
@ -195,6 +204,10 @@ impl Args {
explain: matches.opt_present("E"),
output_type,
show_tui: matches.opt_present("tui"),
config: matches
.opt_str("c")
.or_else(|| env::var(CONFIG_ENV).ok())
.map(PathBuf::from),
values: matches.free,
})
}

View File

@ -6,8 +6,6 @@
pub mod app;
/// Command-line argument parser.
pub mod args;
/// Application output types.
pub mod output;
use crate::app::App;
use crate::args::Args;
@ -20,20 +18,23 @@ use systeroid_core::sysctl::controller::Sysctl;
/// Runs `systeroid`.
pub fn run<Output: Write>(args: Args, output: &mut Output) -> Result<()> {
let config = Config {
verbose: args.verbose,
ignore_errors: args.ignore_errors,
let mut config = Config {
display_deprecated: args.display_deprecated,
quiet: args.quiet,
no_pager: args.no_pager,
display_type: args.display_type,
kernel_docs: args.kernel_docs,
..Default::default()
};
config.cli.verbose = args.verbose;
config.cli.ignore_errors = args.ignore_errors;
config.cli.quiet = args.quiet;
config.cli.no_pager = args.no_pager;
config.cli.display_type = args.display_type;
config.cli.output_type = args.output_type;
config.parse(args.config)?;
let mut sysctl = Sysctl::init(config)?;
if args.explain {
sysctl.update_docs_from_cache(args.kernel_docs.as_ref(), &Cache::init()?)?;
sysctl.update_docs_from_cache(&Cache::init()?)?;
}
let mut app = App::new(&mut sysctl, output, args.output_type);
let mut app = App::new(&mut sysctl, output);
if args.preload_system_files {
app.preload_from_system()?;

View File

@ -8,6 +8,9 @@ fn main() {
if args.show_tui {
let bin = format!("{}-tui", env!("CARGO_PKG_NAME"));
let mut command = Command::new(&bin);
if let Some(config) = args.config {
command.arg("--config").arg(config);
}
if let Some(kernel_docs) = args.kernel_docs {
command.arg("--docs").arg(kernel_docs);
}

View File

@ -1,18 +0,0 @@
/// Possible output types for the [`App`].
///
/// [`App`]: crate::app::App
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OutputType {
/// Print the output as is.
Default,
/// Print the output in a tree-like format.
Tree,
/// Print the output in JSON format.
Json,
}
impl Default for OutputType {
fn default() -> Self {
Self::Default
}
}

View File

@ -1,7 +1,6 @@
use {
systeroid::args::Args, systeroid_core::error::Result,
systeroid_core::sysctl::display::DisplayType,
};
use systeroid::args::Args;
use systeroid_core::error::Result;
use systeroid_core::sysctl::r#type::DisplayType;
#[cfg_attr(not(feature = "live-tests"), ignore)]
#[test]