mirror of
https://github.com/lelgenio/dhist.git
synced 2025-07-06 12:39:26 -03:00
init
This commit is contained in:
commit
3ecf8c981d
12 changed files with 832 additions and 0 deletions
65
src/cli.rs
Normal file
65
src/cli.rs
Normal file
|
@ -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<Mode> 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!(),
|
||||
}
|
||||
}
|
||||
}
|
67
src/db.rs
Normal file
67
src/db.rs
Normal file
|
@ -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<HistoryItem> {
|
||||
let (count, value) = line.split_once(" ")?;
|
||||
|
||||
Some(HistoryItem {
|
||||
count: count.parse::<i32>().ok()?,
|
||||
value: value.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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");
|
||||
}
|
||||
}
|
118
src/increment.rs
Normal file
118
src/increment.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
use crate::{
|
||||
HistoryItem,
|
||||
HistoryResults,
|
||||
};
|
||||
|
||||
fn update_history(
|
||||
input: Vec<String>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
let mut res: HashMap<String, i32> = 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::<Vec<_>>()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
history: HistoryResults,
|
||||
input_lines: Vec<String>,
|
||||
) -> 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()
|
||||
);
|
||||
}
|
||||
}
|
67
src/main.rs
Normal file
67
src/main.rs
Normal file
|
@ -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<HistoryItem>);
|
||||
|
||||
impl From<Vec<HistoryItem>> for HistoryResults {
|
||||
fn from(val: Vec<HistoryItem>) -> Self {
|
||||
HistoryResults(val)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_lines() -> Vec<String> {
|
||||
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::<Vec<_>>())
|
||||
.unwrap();
|
||||
db::put_history(wrap::run(history, input_lines, cmd));
|
||||
}
|
||||
}
|
||||
}
|
162
src/sort.rs
Normal file
162
src/sort.rs
Normal file
|
@ -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<String>) -> HistoryResults {
|
||||
input.dedup();
|
||||
input
|
||||
.into_iter()
|
||||
.map(|item| HistoryItem {
|
||||
count: 1,
|
||||
value: item,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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<String>) -> 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);
|
||||
}
|
||||
}
|
0
src/tests.rs
Normal file
0
src/tests.rs
Normal file
89
src/wrap.rs
Normal file
89
src/wrap.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use std::{
|
||||
io::Write,
|
||||
process::{
|
||||
Command,
|
||||
Stdio,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
increment,
|
||||
sort,
|
||||
HistoryResults,
|
||||
};
|
||||
|
||||
pub fn run(
|
||||
history: HistoryResults,
|
||||
input_lines: Vec<String>,
|
||||
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<String>,
|
||||
stdin: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
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<String> =
|
||||
"hello\nworld!".lines().map(|x| x.to_string()).collect();
|
||||
|
||||
assert_eq!(shell_command(command, Vec::new(), input), expected_output)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue