diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d25908ef..c0ed35fc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,6 +20,14 @@ _Remove the irrelevant one:_ _Please state how this was tested:_ + + +_Please tick which platforms this change was tested on:_ + +- [ ] Windows +- [ ] macOS +- [ ] Linux + ## Checklist _Please ensure all are ticked (and actually done):_ diff --git a/README.md b/README.md index ffd4ab35..01e0be51 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Run using `btm`. - `-s`, `--show_disabled_data` will show data entries in the graph legends even if the lines for that entry are disabled. -- `-C`, `--config` takes in a file path leading to a TOML file. +- `-C`, `--config` takes in a file path leading to a TOML file. If doesn't exist, creates one. - `-b`, `--basic` will enable basic mode, removing all graphs from the main interface and condensing data. diff --git a/docs/config.md b/docs/config.md index 08e171bf..e18db307 100644 --- a/docs/config.md +++ b/docs/config.md @@ -38,7 +38,7 @@ Note some colours may not be compatible with the terminal you are using. For exa ## Default config locations -bottom will check specific locations by default for a config file. +bottom will check specific locations by default for a config file. If no file is found, it will be created. - For Unix-based systems: `$HOME/.config/bottom/bottom.toml`. - For Windows: `{FOLDERID_RoamingAppData}\bottom\bottom.toml` (for example, `C:\Users\Clement\AppData\Roaming\bottom\bottom.toml`). diff --git a/src/constants.rs b/src/constants.rs index f00d0aa6..3ccc7b32 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -56,3 +56,95 @@ pub const SEARCH_HELP_TEXT: [&str; 13] = [ "Alt-w/F2 Toggle whether to match the whole word\n", "Alt-r/F3 Toggle whether to use regex\n", ]; + +pub const DEFAULT_CONFIG_CONTENT: &str = r##" +# This is a default config file for bottom. All of the settings are commented +# out by default; if you wish to change them uncomment and modify as you see +# fit. + +# This group of options represents a command-line flag/option. Flags explicitly +# added when running (ie: btm -a) will override this config file if an option +# is also set here. +[flags] + +#avg_cpu = true +#dot_marker = false +#rate = 1000 +#left_legend = false +#current_usage = false +#group_processes = false +#case_sensitive = false +#whole_word = true +#regex = true +#show_disabled_data = true + +# Defaults to Celsius. Temperature is one of: +#temperature_type = "k" +#temperature_type = "f" +#temperature_type = "c" +#temperature_type = "kelvin" +#temperature_type = "fahrenheit" +#temperature_type = "celsius" + +# Defaults to processes. Default widget is one of: +#default_widget = "cpu_default" +#default_widget = "memory_default" +#default_widget = "disk_default" +#default_widget = "temperature_default" +#default_widget = "network_default" +#default_widget = "process_default" + + +# These are all the components that support custom theming. Currently, it only +# supports taking in a string representing a hex colour. Note that colour support +# will, at the end of the day, depend on terminal support - for example, the +# macOS default Terminal does NOT like custom colours and it will glitch out. +# +# The default options here are based on gruvbox: https://github.com/morhetz/gruvbox +[colors] + +# Represents the colour of table headers (processes, CPU, disks, temperature). +#table_header_color="#458588" + +# Represents the colour of the label each widget has. +#widget_title_color="#cc241d" + +# Represents the average CPU color +#avg_cpu_color="#d3869b" + +# Represents the colour the core will use in the CPU legend and graph. +#cpu_core_colors=["#cc241d", "#98971a"] + +# Represents the colour RAM will use in the memory legend and graph. +#ram_color="#fb4934" + +# Represents the colour SWAP will use in the memory legend and graph. +#swap_color="#fabd2f" + +# Represents the colour rx will use in the network legend and graph. +#rx_color="#458588" + +# Represents the colour tx will use in the network legend and graph. +#tx_color="#689d6a" + +# Represents the colour of the border of unselected widgets. +#border_color="#ebdbb2" + +# Represents the colour of the border of selected widgets. +#highlighted_border_color="#fe8019" + +# Represents the colour of most text. +#text_color="#ebdbb2" + +# Represents the colour of text that is selected. +#selected_text_color="#282828" + +# Represents the background colour of text that is selected. +#selected_bg_color="#458588" + +# Represents the colour of the lines and text of the graph. +#graph_color="#ebdbb2" + +# Represents the cursor's colour. +#cursor_color="#458588" +"##; diff --git a/src/main.rs b/src/main.rs index e1d6a727..04451540 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ extern crate clap; extern crate futures; #[macro_use] extern crate lazy_static; -#[macro_use] +#[macro_use]CEvent extern crate log; use std::{ @@ -79,7 +79,7 @@ fn get_matches() -> clap::ArgMatches<'static> { (@arg RATE_MILLIS: -r --rate +takes_value "Sets a refresh rate in milliseconds; the minimum is 250ms, defaults to 1000ms. Smaller values may take more resources.") (@arg LEFT_LEGEND: -l --left_legend "Puts external chart legends on the left side rather than the default right side.") (@arg USE_CURR_USAGE: -u --current_usage "Within Linux, sets a process' CPU usage to be based on the total current CPU usage, rather than assuming 100% usage.") - (@arg CONFIG_LOCATION: -C --config +takes_value "Sets the location of the config file. Expects a config file in the TOML format.") + (@arg CONFIG_LOCATION: -C --config +takes_value "Sets the location of the config file. Expects a config file in the TOML format. If it doesn't exist, one is created.") (@arg BASIC_MODE: -b --basic "Hides graphs and uses a more basic look") (@arg GROUP_PROCESSES: -g --group "Groups processes with the same name together on launch.") (@arg CASE_SENSITIVE: -S --case_sensitive "Match case when searching by default.") @@ -392,32 +392,250 @@ fn create_logger() -> error::Result<()> { } fn create_config(flag_config_location: Option<&str>) -> error::Result { - use std::ffi::OsString; - let config_path = if let Some(conf_loc) = flag_config_location { - OsString::from(conf_loc) - } else if cfg!(target_os = "windows") { - if let Some(home_path) = dirs::config_dir() { - let mut path = home_path; - path.push(DEFAULT_WINDOWS_CONFIG_FILE_PATH); - path.into_os_string() - } else { - OsString::new() - } - } else if let Some(home_path) = dirs::home_dir() { - let mut path = home_path; - path.push(DEFAULT_UNIX_CONFIG_FILE_PATH); - path.into_os_string() - } else { - OsString::new() - }; + use std::{ffi::OsString, fs}; + let config_path = if let Some(conf_loc) = flag_config_location { + OsString::from(conf_loc) + } else if cfg!(target_os = "windows") { + if let Some(home_path) = dirs::config_dir() { + let mut path = home_path; + path.push(DEFAULT_WINDOWS_CONFIG_FILE_PATH); + path.into_os_string() + } else { + OsString::new() + } + } else if let Some(home_path) = dirs::home_dir() { + let mut path = home_path; + path.push(DEFAULT_UNIX_CONFIG_FILE_PATH); + path.into_os_string() + } else { + OsString::new() + }; - let path = std::path::Path::new(&config_path); + let path = std::path::Path::new(&config_path); - if let Ok(config_str) = std::fs::read_to_string(path) { - Ok(toml::from_str(config_str.as_str())?) - } else { - Ok(Config::default()) - } + if let Ok(config_string) = fs::read_to_string(path) { + Ok(toml::from_str(config_string.as_str())?) + } else { + if let Some(parent_path) = path.parent() { + fs::create_dir_all(parent_path)?; + } + fs::File::create(path)?.write_all(DEFAULT_CONFIG_CONTENT.as_bytes())?; + Ok(toml::from_str(DEFAULT_CONFIG_CONTENT)?) + } +} + +fn get_update_rate_in_milliseconds( + update_rate: &Option<&str>, config: &Config, +) -> error::Result { + let update_rate_in_milliseconds = if let Some(update_rate) = update_rate { + update_rate.parse::()? + } else if let Some(flags) = &config.flags { + if let Some(rate) = flags.rate { + rate as u128 + } else { + constants::DEFAULT_REFRESH_RATE_IN_MILLISECONDS + } + } else { + constants::DEFAULT_REFRESH_RATE_IN_MILLISECONDS + }; + + if update_rate_in_milliseconds < 250 { + return Err(BottomError::InvalidArg( + "Please set your update rate to be greater than 250 milliseconds.".to_string(), + )); + } else if update_rate_in_milliseconds > u128::from(std::u64::MAX) { + return Err(BottomError::InvalidArg( + "Please set your update rate to be less than unsigned INT_MAX.".to_string(), + )); + } + + Ok(update_rate_in_milliseconds) +} + +fn get_temperature_option( + matches: &clap::ArgMatches<'static>, config: &Config, +) -> error::Result { + if matches.is_present("FAHRENHEIT") { + return Ok(data_harvester::temperature::TemperatureType::Fahrenheit); + } else if matches.is_present("KELVIN") { + return Ok(data_harvester::temperature::TemperatureType::Kelvin); + } else if matches.is_present("CELSIUS") { + return Ok(data_harvester::temperature::TemperatureType::Celsius); + } else if let Some(flags) = &config.flags { + if let Some(temp_type) = &flags.temperature_type { + // Give lowest priority to config. + return match temp_type.as_str() { + "fahrenheit" | "f" => { + Ok(data_harvester::temperature::TemperatureType::Fahrenheit) + } + "kelvin" | "k" => { + Ok(data_harvester::temperature::TemperatureType::Kelvin) + } + "celsius" | "c" => { + Ok(data_harvester::temperature::TemperatureType::Celsius) + } + _ => { + Err(BottomError::ConfigError( + "Invalid temperature type. Please have the value be of the form \ + " + .to_string(), + )) + } + } + } + } + Ok(data_harvester::temperature::TemperatureType::Celsius) +} + +fn get_avg_cpu_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("AVG_CPU") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(avg_cpu) = flags.avg_cpu { + return avg_cpu; + } + } + + false +} + +fn get_use_dot_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("DOT_MARKER") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(dot_marker) = flags.dot_marker { + return dot_marker; + } + } + false +} + +fn get_use_left_legend_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("LEFT_LEGEND") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(left_legend) = flags.left_legend { + return left_legend; + } + } + + false +} + +fn get_use_current_cpu_total_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("USE_CURR_USAGE") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(current_usage) = flags.current_usage { + return current_usage; + } + } + + false +} + +fn get_show_disabled_data_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("SHOW_DISABLED_DATA") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(show_disabled_data) = flags.show_disabled_data { + return show_disabled_data; + } + } + + false +} + +fn enable_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) { + if matches.is_present("GROUP_PROCESSES") { + app.toggle_grouping(); + } else if let Some(flags) = &config.flags { + if let Some(grouping) = flags.group_processes { + if grouping { + app.toggle_grouping(); + } + } + } +} + +fn enable_app_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) { + if matches.is_present("CASE_SENSITIVE") { + app.process_search_state.search_toggle_ignore_case(); + } else if let Some(flags) = &config.flags { + if let Some(case_sensitive) = flags.case_sensitive { + if case_sensitive { + app.process_search_state.search_toggle_ignore_case(); + } + } + } +} + +fn enable_app_match_whole_word( + matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App, +) { + if matches.is_present("WHOLE_WORD") { + app.process_search_state.search_toggle_whole_word(); + } else if let Some(flags) = &config.flags { + if let Some(whole_word) = flags.whole_word { + if whole_word { + app.process_search_state.search_toggle_whole_word(); + } + } + } +} + +fn enable_app_use_regex(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) { + if matches.is_present("REGEX_DEFAULT") { + app.process_search_state.search_toggle_regex(); + } else if let Some(flags) = &config.flags { + if let Some(regex) = flags.regex { + if regex { + app.process_search_state.search_toggle_regex(); + } + } + } +} + +fn get_default_widget(matches: &clap::ArgMatches<'static>, config: &Config) -> app::WidgetPosition { + if matches.is_present("CPU_WIDGET") { + return app::WidgetPosition::Cpu; + } else if matches.is_present("MEM_WIDGET") { + return app::WidgetPosition::Mem; + } else if matches.is_present("DISK_WIDGET") { + return app::WidgetPosition::Disk; + } else if matches.is_present("TEMP_WIDGET") { + return app::WidgetPosition::Temp; + } else if matches.is_present("NET_WIDGET") { + return app::WidgetPosition::Network; + } else if matches.is_present("PROC_WIDGET") { + return app::WidgetPosition::Process; + } else if let Some(flags) = &config.flags { + if let Some(default_widget) = &flags.default_widget { + match default_widget.as_str() { + "cpu_default" => { + return app::WidgetPosition::Cpu; + } + "memory_default" => { + return app::WidgetPosition::Mem; + } + "processes_default" => { + return app::WidgetPosition::Process; + } + "network_default" => { + return app::WidgetPosition::Network; + } + "temperature_default" => { + return app::WidgetPosition::Temp; + } + "disk_default" => { + return app::WidgetPosition::Disk; + } + _ => {} + } + } + } + + app::WidgetPosition::Process } fn try_drawing(