diff --git a/Cargo.lock b/Cargo.lock index c48984b..c458ab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,7 +80,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -90,7 +90,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -286,12 +286,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -299,7 +326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -311,6 +338,17 @@ dependencies = [ "thread_local", ] +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -365,6 +403,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "instability" version = "0.3.2" @@ -411,6 +459,16 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.4.2", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -470,7 +528,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -497,6 +555,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -534,11 +598,14 @@ dependencies = [ "chrono", "clap", "crossterm", + "directories", "fuzzy-matcher", "http-test-server", "listeners", "ratatui", + "serde", "sysinfo", + "toml", "tui-textarea", ] @@ -610,6 +677,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.6" @@ -649,7 +727,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -670,6 +748,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -765,6 +872,26 @@ dependencies = [ "windows 0.57.0", ] +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -775,6 +902,40 @@ dependencies = [ "once_cell", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tui-textarea" version = "0.6.1" @@ -1035,6 +1196,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1165,6 +1335,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index ef03e92..2ef8713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ sysinfo = "0.31" listeners = "0.2.1" chrono = "0.4" clap = { version = "4.5", features = ["derive"] } +directories = "5.0" +toml = "0.8" +serde = { version = "1.0", features = ["derive"] } fuzzy-matcher = "0.3.7" [dev-dependencies] diff --git a/README.md b/README.md index 110619e..adfb006 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ cargo binstall pik ## Configuration +### Application configuration + +You may set your preferences in `pik.toml` file located in `~/.config/pik` directory. +All options are optional, if skipped default values will be used. +Example configuration with default settings can be found at [example config](example_config.toml) + ### Key maps - Esc | Ctrl + C - Quit diff --git a/example_config.toml b/example_config.toml new file mode 100644 index 0000000..8245cdb --- /dev/null +++ b/example_config.toml @@ -0,0 +1,3 @@ +# Size of the viewport +screen_size = { height = 20 } # run pik in 20 lines of the terminal +# screen_size = "fullscreen" # run pik in fullscreen diff --git a/src/args.rs b/src/args.rs index 7a239a7..e1847c4 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,11 +1,13 @@ -use clap::Parser; +use clap::{Args, Parser}; + +use crate::config; #[derive(Parser, Debug)] #[command(version, about, long_about = Some("Pik is a simple TUI tool for searching and killing processes in interactive way."))] -pub struct Args { +pub struct CliArgs { #[clap( default_value = "", - help = r#"Query string for searching processes. By default, all processes are searched. + help = r#"Query string for searching processes. You may use special prefix for different kind of search: - : - search by port, i.e ':8080' - / - search by command path, i.e. '/home/user/bin' @@ -17,10 +19,19 @@ pub struct Args { #[arg(short = 't', long, default_value_t = false)] pub include_threads_processes: bool, /// By default pik shows only proceseses owned by current user. This flag allows to show all processes - #[arg(short, long, default_value_t = false)] - pub all_processes: bool, + #[arg(short = 'a', long, default_value_t = false)] + pub include_other_users_processes: bool, + #[command(flatten)] + pub screen_size: Option, +} + +#[derive(Args, Debug, Clone, Copy)] +#[group(required = false, multiple = false)] +pub struct ScreenSizeOptions { + /// Start pik in fullscreen mode #[arg(short = 'F', long, default_value_t = false)] pub fullscreen: bool, - #[arg(short = 'H', long, default_value_t = 20)] + /// Number of lines of the screen pik will use + #[arg(short = 'H', long, default_value_t = config::DEFAULT_SCREEN_SIZE)] pub height: u16, } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..15dfa56 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,70 @@ +use anyhow::{Context, Result}; + +pub fn load_app_config() -> Result { + let config_path = directories::ProjectDirs::from("", "", "pik") + .map(|dirs| dirs.config_dir().join("config.toml")) + .filter(|path| path.exists()); + + match config_path { + Some(path) => load_config_from_file(&path), + None => Ok(AppConfig::default()), + } +} + +fn load_config_from_file(path: &std::path::PathBuf) -> Result { + let raw_toml = std::fs::read_to_string(path) + .with_context(|| format!("Failed to load config from file: {:?}", path))?; + toml::from_str(&raw_toml) + .with_context(|| format!("Failed to deserialize config from file: {:?}", path)) +} + +use serde::Deserialize; + +#[derive(Debug, Default, PartialEq, Eq, Deserialize)] +pub struct AppConfig { + #[serde(default)] + pub screen_size: ScreenSize, +} + +#[derive(Debug, Eq, PartialEq, Deserialize, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub enum ScreenSize { + Fullscreen, + Height(u16), +} + +pub const DEFAULT_SCREEN_SIZE: u16 = 25; + +impl Default for ScreenSize { + fn default() -> Self { + ScreenSize::Height(DEFAULT_SCREEN_SIZE) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_deserialize_empty_configuration() { + let default_settings = toml::from_str(""); + assert_eq!(default_settings, Ok(AppConfig::default())); + } + + #[test] + fn should_allow_to_override_defaults() { + let default_settings: AppConfig = toml::from_str( + r#" + screen_size = "fullscreen" + "#, + ) + .unwrap(); + assert_eq!( + default_settings, + AppConfig { + screen_size: ScreenSize::Fullscreen + } + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index caecdfe..d61cd37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ pub mod args; +pub mod config; pub mod processes; +pub mod settings; pub mod tui; diff --git a/src/main.rs b/src/main.rs index ce089e7..687aa91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,13 @@ use anyhow::Result; use clap::Parser; -use pik::args::Args; -use pik::processes::FilterOptions; +use pik::args::CliArgs; +use pik::settings::AppSettings; use pik::tui::start_app; fn main() -> Result<()> { - let args = Args::parse(); - start_app( - args.query, - FilterOptions { - ignore_threads: !args.include_threads_processes, - include_all_processes: args.all_processes, - }, - args.height, - args.fullscreen, - ) + let config = pik::config::load_app_config()?; + let args = CliArgs::parse(); + + let settings = AppSettings::from(config, &args); + start_app(args.query, settings) } diff --git a/src/processes/filters.rs b/src/processes/filters.rs index 347da3d..24c0659 100644 --- a/src/processes/filters.rs +++ b/src/processes/filters.rs @@ -94,7 +94,7 @@ impl QueryFilter { } } -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct FilterOptions { //NOTE: On linux threads can be listed as processes and thus needs filtering pub ignore_threads: bool, diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..cc2fe09 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,131 @@ +use ratatui::Viewport; + +use crate::{ + args::{CliArgs, ScreenSizeOptions}, + config::{AppConfig, ScreenSize}, + processes::FilterOptions, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct AppSettings { + pub viewport: Viewport, + pub filter_opions: FilterOptions, +} + +impl AppSettings { + pub fn from(config: AppConfig, cli_args: &CliArgs) -> Self { + Self { + viewport: prefer_override(config.screen_size, cli_args.screen_size), + filter_opions: FilterOptions { + ignore_threads: !cli_args.include_threads_processes, + include_all_processes: cli_args.include_other_users_processes, + }, + } + } +} + +fn prefer_override(config_value: C, override_opt: Option) -> V +where + C: Into, + A: Into, +{ + match override_opt { + Some(overidden_value) => overidden_value.into(), + None => config_value.into(), + } +} + +impl From for Viewport { + fn from(ss: ScreenSize) -> Self { + match ss { + ScreenSize::Fullscreen => Viewport::Fullscreen, + ScreenSize::Height(height) => Viewport::Inline(height), + } + } +} + +impl From for Viewport { + fn from(ss: ScreenSizeOptions) -> Self { + match (ss.fullscreen, ss.height) { + (true, _) => Viewport::Fullscreen, + (_, height) => Viewport::Inline(height), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_convert_screen_size_to_viewport() { + assert_eq!(Viewport::from(ScreenSize::Fullscreen), Viewport::Fullscreen); + assert_eq!(Viewport::from(ScreenSize::Height(25)), Viewport::Inline(25)); + } + + #[test] + fn should_convert_screen_size_options_to_viewport() { + assert_eq!( + Viewport::from(ScreenSizeOptions { + fullscreen: true, + height: 25 + }), + Viewport::Fullscreen + ); + assert_eq!( + Viewport::from(ScreenSizeOptions { + fullscreen: false, + height: 25 + }), + Viewport::Inline(25) + ); + } + + #[test] + fn should_create_settings() { + let config = AppConfig::default(); + let cli_args = CliArgs { + query: "".to_string(), + include_threads_processes: true, + include_other_users_processes: true, + screen_size: None, + }; + let settings = AppSettings::from(config, &cli_args); + assert_eq!( + settings, + AppSettings { + viewport: Viewport::Inline(25), + filter_opions: FilterOptions { + ignore_threads: false, + include_all_processes: true + } + } + ); + } + + #[test] + fn should_prefer_cli_args_screen_size() { + let config = AppConfig { + screen_size: ScreenSize::Height(40), + }; + let cli_args = CliArgs { + screen_size: Some(ScreenSizeOptions { + fullscreen: true, + height: 25, + }), + ..some_cli_args() + }; + let settings = AppSettings::from(config, &cli_args); + assert_eq!(settings.viewport, Viewport::Fullscreen); + } + + fn some_cli_args() -> CliArgs { + CliArgs { + query: "".to_string(), + include_threads_processes: true, + include_other_users_processes: true, + screen_size: None, + } + } +} diff --git a/src/tui.rs b/src/tui.rs index f866d25..8894fb8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -5,11 +5,14 @@ use crossterm::{ event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode}, }; -use ratatui::{prelude::*, TerminalOptions, Viewport}; +use ratatui::{prelude::*, TerminalOptions}; mod rendering; -use crate::processes::{FilterOptions, ProcessManager, ProcessSearchResults}; +use crate::{ + processes::{FilterOptions, ProcessManager, ProcessSearchResults}, + settings::AppSettings, +}; use self::rendering::Tui; @@ -21,11 +24,11 @@ struct App { } impl App { - fn new(search_criteria: String, filter_options: FilterOptions) -> Result { + fn new(search_criteria: String, app_settings: AppSettings) -> Result { let mut app = App { process_manager: ProcessManager::new()?, search_results: ProcessSearchResults::empty(), - filter_options, + filter_options: app_settings.filter_opions, tui: Tui::new(search_criteria), }; app.search_for_processess(); @@ -73,23 +76,15 @@ impl App { } } -pub fn start_app( - search_criteria: String, - filter_options: FilterOptions, - viewport_height: u16, - viewport_fullscreen: bool, -) -> Result<()> { +pub fn start_app(search_criteria: String, app_settings: AppSettings) -> Result<()> { // setup terminal enable_raw_mode()?; let backend = CrosstermBackend::new(io::stdout()); - let viewport = match (viewport_height, viewport_fullscreen) { - (_, true) => Viewport::Fullscreen, - (h, false) => Viewport::Inline(h), - }; + let viewport = app_settings.viewport.clone(); let mut terminal = Terminal::with_options(backend, TerminalOptions { viewport })?; // create app and run it - let app = App::new(search_criteria, filter_options)?; + let app = App::new(search_criteria, app_settings)?; let res = run_app(&mut terminal, app); // restore terminal