commit 3ecf8c981d0ce15579ef81167bb61391bb90d0bd Author: lelgenio Date: Tue Feb 22 20:33:10 2022 -0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..66eeed9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,216 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f1fea81f183005ced9e59cdb01737ef2423956dac5a6d731b06b2ecfaa3467" +dependencies = [ + "atty", + "bitflags", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "dhist" +version = "0.1.0" +dependencies = [ + "clap", + "dirs", +] + +[[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-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9235696 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dhist" +description = "Save and sort most often used dmenu-like input" +authors = ["Leonardo EugĂȘnio "] +license = "GPL-3.0" +version = "0.1.1" +edition = "2021" +readme = "README.md" + +[dependencies] +dirs = "4.0.0" + +[dependencies.clap] +version = "3.0.0-beta.4" +features = ["color", "cargo"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..0533c15 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# dhist + +Save and sort most often used dmenu-like input + +``` +USAGE: + dhist [OPTIONS] + +OPTIONS: + -h, --help Print help information + +SUBCOMMANDS: + help Print this message or the help of the given subcommand(s) + increment Increase usage of input by 1 + query Print history + sort Sort input by history frequency + wrap Wrap a command to sort before and increment after +``` + +## Examples + +``` +# sort input of dmenu based on usage +printf "%s\n" hello world | dhist wrap -- dmenu + +# same as above, but more verbose +# dhist increment also prints out it's input, so you can still use it for another program +printf "%s\n" hello world | dhist sort | dmenu | dhist increment +``` diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..a197aa9 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable-2022-01-20" +targets = [ "x86_64-unknown-linux-gnu", ] diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..dfa8be0 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,65 @@ +use clap::{ + Command, + Arg, +}; + +pub fn get_cli() -> clap::Command<'static> { + Command::new("dhist") + .about(clap::crate_description!()) + .author(clap::crate_authors!()) + .arg_required_else_help(true) + // .arg( + // Arg::new("histfile-path") + // .short('H') + // .long("history") + // .about("Path to history file"), + // ) + .subcommand(Command::new(Mode::Query).about("Print history")) + .subcommand( + Command::new(Mode::Sort).about("Sort input by history frequency"), + ) + .subcommand( + Command::new(Mode::Increment).about("Increase usage of input by 1"), + ) + .subcommand( + Command::new(Mode::Wrap) + .about("Wrap a command to sort before and increment after") + .arg_required_else_help(true) + .arg( + Arg::new("command") + .required(true) + .multiple_values(true) + .last(true), + ), + ) +} + +pub enum Mode { + Query, + Sort, + Increment, + Wrap, +} + +impl From for String { + fn from(val: Mode) -> Self { + match val { + Mode::Query => "query".to_string(), + Mode::Sort => "sort".to_string(), + Mode::Increment => "increment".to_string(), + Mode::Wrap => "wrap".to_string(), + } + } +} + +impl From<&str> for Mode { + fn from(val: &str) -> Self { + match val { + "query" => Mode::Query, + "sort" => Mode::Sort, + "increment" => Mode::Increment, + "wrap" => Mode::Wrap, + _ => unreachable!(), + } + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..34ffa0a --- /dev/null +++ b/src/db.rs @@ -0,0 +1,67 @@ +use std::{ + fs::File, + io::{ + BufRead, + BufReader, + BufWriter, + Write, + }, + path::PathBuf, +}; + +use dirs::data_dir; + +use crate::{ + HistoryItem, + HistoryResults, +}; + +fn get_db_path() -> PathBuf { + if let Ok(histfile_path) = std::env::var("DMENU_HISTORY_FILE") { + PathBuf::from(histfile_path) + } else { + data_dir() + .expect("Could not get a data dir") + .join("dmenu-history") + } +} + +fn read_history(file: &File) -> HistoryResults { + let reader = BufReader::new(file); + + reader + .lines() + .flatten() + .filter_map(|line| -> Option { + let (count, value) = line.split_once(" ")?; + + Some(HistoryItem { + count: count.parse::().ok()?, + value: value.to_string(), + }) + }) + .collect::>() + .into() +} + +pub fn get_history() -> HistoryResults { + let mut hist = match &File::open(get_db_path()) { + Ok(file) => read_history(file), + Err(_) => Vec::new().into(), + }; + + hist.0.sort_by_key(|i| -i.count); + + hist +} + +pub fn put_history(res: HistoryResults) { + let file = File::create(get_db_path()).expect("Cannot write to data dir"); + let mut history_writer = BufWriter::new(file); + for HistoryItem { count, value } in res.0 { + let newline = format!("{} {}\n", count, value); + history_writer + .write_all(newline.as_bytes()) + .expect("Cannot write to history file"); + } +} diff --git a/src/increment.rs b/src/increment.rs new file mode 100644 index 0000000..d2937e9 --- /dev/null +++ b/src/increment.rs @@ -0,0 +1,118 @@ +use crate::{ + HistoryItem, + HistoryResults, +}; + +fn update_history( + input: Vec, + history: &HistoryResults, +) -> HistoryResults { + use std::collections::HashMap; + + let input_lines = input + .into_iter() + // Here we copy stdin to stdout so other programs can use it + .inspect(|i| println!("{}", i)) + .map(|k| HistoryItem { value: k, count: 1 }) + .collect::>(); + + let mut res: HashMap = HashMap::new(); + + history.0.iter().chain(input_lines.iter()).for_each( + |hist_item: &HistoryItem| { + let entry = res + .entry(hist_item.value.clone()) + .or_insert(hist_item.count - 1); + *entry += 1; + }, + ); + + res.into_iter() + .map(|(k, v)| HistoryItem { value: k, count: v }) + .collect::>() + .into() +} + +pub fn run( + history: HistoryResults, + input_lines: Vec, +) -> HistoryResults { + // Read + + // Proccess + update_history(input_lines, &history) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn increment_empty() { + let input = vec![].into(); + let history = vec![].into(); + + update_history(input, &history); + + assert_eq!(history, vec![].into()); + } + + #[test] + fn increment_keeps_history() { + let input = vec![].into(); + let history = vec![HistoryItem { + count: 1, + value: "one".to_string(), + }] + .into(); + + update_history(input, &history); + + assert_eq!( + history, + vec![HistoryItem { + count: 1, + value: "one".to_string(), + },] + .into() + ); + } + + #[test] + fn increment_empty_history() { + let input = vec!["one".to_string()].into(); + let history = vec![].into(); + + let res = update_history(input, &history); + + assert_eq!( + res, + vec![HistoryItem { + count: 1, + value: "one".to_string(), + },] + .into() + ); + } + + #[test] + fn increment_history() { + let input = vec!["one".to_string()].into(); + let history = vec![HistoryItem { + count: 1, + value: "one".to_string(), + }] + .into(); + + let res = update_history(input, &history); + + assert_eq!( + res, + vec![HistoryItem { + count: 2, + value: "one".to_string(), + },] + .into() + ); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0ece566 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,67 @@ +use cli::Mode; + +mod cli; +mod db; +mod increment; +mod sort; +mod wrap; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct HistoryItem { + count: i32, + value: String, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct HistoryResults(Vec); + +impl From> for HistoryResults { + fn from(val: Vec) -> Self { + HistoryResults(val) + } +} + +fn read_lines() -> Vec { + use std::io::{ + stdin, + BufRead, + }; + stdin().lock().lines().flatten().collect() +} + +fn main() { + let matches = cli::get_cli().get_matches(); + let history = db::get_history(); + + match matches.subcommand_name().unwrap().into() { + Mode::Query => { + let history = &history; + for line in &history.0 { + println!("{} {}", line.count, line.value); + } + } + Mode::Sort => { + let input_lines = read_lines(); + let history = &sort::run(history, input_lines); + + for line in &history.0 { + println!("{}", line.value); + } + } + Mode::Increment => { + let input_lines = read_lines(); + db::put_history(increment::run(history, input_lines)); + } + Mode::Wrap => { + let input_lines = read_lines(); + + let args = matches.subcommand().unwrap().1; + + let cmd = args + .values_of("command") + .map(|x| x.collect::>()) + .unwrap(); + db::put_history(wrap::run(history, input_lines, cmd)); + } + } +} diff --git a/src/sort.rs b/src/sort.rs new file mode 100644 index 0000000..1bf3420 --- /dev/null +++ b/src/sort.rs @@ -0,0 +1,162 @@ +use crate::{ + HistoryItem, + HistoryResults, +}; + +fn increment_with(main: &mut HistoryResults, rhs: &HistoryResults) { + for main_item in main.0.iter_mut() { + for rhs_item in rhs.0.iter() { + if rhs_item.value == main_item.value { + main_item.count += rhs_item.count; + } + } + } +} + +fn results_from_vec(mut input: Vec) -> HistoryResults { + input.dedup(); + input + .into_iter() + .map(|item| HistoryItem { + count: 1, + value: item, + }) + .collect::>() + .into() +} + +fn sort_lines( + history: HistoryResults, + mut input: HistoryResults, +) -> HistoryResults { + increment_with(&mut input, &history); + + // Sort such that items with a bigger count are at the start + input.0.sort_by_key(|i| -i.count); + input +} + +pub fn run(history: HistoryResults, stdin_read: Vec) -> HistoryResults { + let input = results_from_vec(stdin_read); + sort_lines(history, input) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn history_stays_empty() { + let input = vec![].into(); + let history = vec![].into(); + + let res = sort_lines(input, history); + + assert_eq!(res, vec![].into()); + } + + #[test] + fn history_stays_no_input() { + let input = vec![].into(); + let history = vec![ + HistoryItem { + count: 1, + value: "sample text".to_string(), + }, + HistoryItem { + count: 2, + value: "sample text".to_string(), + }, + HistoryItem { + count: 3, + value: "sample text".to_string(), + }, + ] + .into(); + + let res = sort_lines(history, input); + + assert_eq!(res, vec![].into()); + } + #[test] + fn history_stays_no_history() { + let input = vec![ + HistoryItem { + count: 1, + value: "sample text".to_string(), + }, + HistoryItem { + count: 2, + value: "sample text".to_string(), + }, + HistoryItem { + count: 3, + value: "sample text".to_string(), + }, + ] + .into(); + let history = vec![].into(); + + let res = sort_lines(history, input); + + assert_eq!( + res, + vec![ + HistoryItem { + count: 3, + value: "sample text".to_string(), + }, + HistoryItem { + count: 2, + value: "sample text".to_string(), + }, + HistoryItem { + count: 1, + value: "sample text".to_string(), + }, + ] + .into() + ); + } + + #[test] + fn count_occurances() { + let input = + vec!["one".to_string(), "two".to_string(), "three".to_string()]; + let history = vec![ + HistoryItem { + count: 1, + value: "one".to_string(), + }, + HistoryItem { + count: 3, + value: "two".to_string(), + }, + HistoryItem { + count: 1, + value: "three".to_string(), + }, + ] + .into(); + + let res = sort_lines(history, results_from_vec(input)); + + let expected: HistoryResults = vec![ + HistoryItem { + count: 4, + value: "two".to_string(), + }, + HistoryItem { + count: 2, + value: "one".to_string(), + }, + HistoryItem { + count: 2, + value: "three".to_string(), + }, + ] + .into(); + + assert_eq!(res, expected); + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/wrap.rs b/src/wrap.rs new file mode 100644 index 0000000..8d5d546 --- /dev/null +++ b/src/wrap.rs @@ -0,0 +1,89 @@ +use std::{ + io::Write, + process::{ + Command, + Stdio, + }, +}; + +use crate::{ + increment, + sort, + HistoryResults, +}; + +pub fn run( + history: HistoryResults, + input_lines: Vec, + cmd: Vec<&str>, +) -> HistoryResults { + // let history = ; + + let mut arguments = cmd.into_iter().map(|f| f.to_string()); + let command = arguments.next().unwrap(); + + let command_input = sort::run(history.clone(), input_lines) + .0 + .into_iter() + .map(|f| f.value) + .collect(); + + let child_output = + shell_command(command, arguments.collect(), command_input); + + increment::run(history, child_output) +} + +fn shell_command( + command: String, + args: Vec, + stdin: Vec, +) -> Vec { + let mut child = Command::new(&command) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap_or_else(|e| { + panic!("failed to spawn program {}: {}", command, e) + }); + { + let mut child_stdin = child + .stdin + .take() + .unwrap_or_else(|| panic!("failed to get stdin of {}", command)); + for line in &stdin { + let text = format!("{}\n", line); + child_stdin.write_all(text.as_bytes()).ok(); + } + } + let child_output_bytes = child + .wait_with_output() + .unwrap_or_else(|e| { + panic!("failed to get stdout of {}: {}", command, e) + }) + .stdout; + let child_output = std::str::from_utf8(&child_output_bytes) + .unwrap_or_else(|e| { + panic!("failed to read output of {}: {}", command, e) + }) + .lines() + .map(|i| i.to_string()) + .collect(); + child_output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_shell() { + let command = "cat".to_string(); + let input = "hello\nworld!".lines().map(|x| x.to_string()).collect(); + let expected_output: Vec = + "hello\nworld!".lines().map(|x| x.to_string()).collect(); + + assert_eq!(shell_command(command, Vec::new(), input), expected_output) + } +}