Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

edit: implement pazi edit #71

Merged
merged 11 commits into from
Aug 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 227 additions & 74 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ log = "~0.3"
termion = "~1"
chan = "~0.1"
chan-signal = "~0.3"
tempfile = "3"
which = "2"
snailquote = "0.1.0"
159 changes: 159 additions & 0 deletions src/edit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use frecent_paths::PathFrecencyDiff;
use snailquote;
use std::char;
use std::collections::HashMap;
use std::env;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use std::process::Command;
use std::str;
use tempfile::Builder;
use which;

// edit opens up EDITOR with the given input matches for the user to edit. It returns a 'diff' of
// what has changed.
pub fn edit(data: &Vec<(String, f64)>) -> Result<PathFrecencyDiff, String> {
let mut editor = env::var("PAZI_EDITOR")
.or_else(|_| env::var("EDITOR"))
.or_else(|_| env::var("VISUAL"))
.map_err(|_| "please set PAZI_EDITOR or EDITOR")
.and_then(|ed| {
// support 'EDITOR=bin args' by splitting out possible args
// note, this unfortunately means editors with spaces in their paths won't work.
// This matches systemd's behavior as best I can tell:
// https://github.com/systemd/systemd/blob/d32d473d66caafb4e448fa5fc056589b7763c478/src/systemctl/systemctl.c#L6795-L6803
let mut parts = ed.split(char::is_whitespace);

let edpart = parts.next();
let rest = parts.map(|s| s.to_owned()).collect();
match edpart {
Some(s) => Ok((PathBuf::from(s), rest)),
None => Err("empty editor"),
}
})
.or_else(|_| {
for bin in vec!["editor", "nano", "vim", "vi"] {
match which::which(bin) {
Ok(ed) => {
return Ok((ed, vec![]));
}
Err(_) => (),
}
}
Err("could not find editor in path")
})?;

let mut tmpf = Builder::new()
.prefix("pazi_edit")
.tempfile()
.map_err(|e| format!("error creating tempfile: {}", e))?;

let serialized_data = serialize(data);
tmpf.write_all(serialized_data.as_bytes())
.map_err(|e| format!("could not write data to tempfile: {}", e))?;

debug!("created tmpfile at: {}", tmpf.path().to_str().unwrap());
editor.1.push(tmpf.path().to_str().unwrap().to_string());
let mut cmd = Command::new(editor.0);
cmd.args(editor.1);

let mut child = cmd
.spawn()
.map_err(|e| format!("error spawning editor: {}", e))?;

let exit = child
.wait()
.map_err(|e| format!("error waiting for editor: {}", e))?;

if !exit.success() {
return Err(format!("editor exited non-zero: {}", exit.code().unwrap()))?;
}
tmpf.seek(SeekFrom::Start(0))
.map_err(|e| format!("could not seek in tempfile: {}", e))?;
let mut new_contents = String::new();
tmpf.read_to_string(&mut new_contents)
.map_err(|e| format!("error reading file: {}", e))?;

if new_contents.trim() == serialized_data.trim() {
debug!("identical data read; shortcutting out");
return Ok(PathFrecencyDiff::new(Vec::new(), Vec::new()));
}

let mut new_map = deserialize(&new_contents)?;

let mut removals = Vec::new();
let mut additions = Vec::new();

// Find all items in the input set and see if they've changed
for item in data {
debug!("edit: processing {:?}", item);
match new_map.remove(&item.0) {
Some(w) => {
if item.1 != w {
debug!("edit: update {:?}", item.0);
// weight edited, aka remove + add
removals.push(item.0.clone());
additions.push((item.0.clone(), w));
}
// otherwise, no diff
}
None => {
debug!("edit: removal {:?}", item.0);
removals.push(item.0.clone());
}
}
}
// anything that was in the input has been removed from our new_map now, so anything
// remaining is an addition to the database
for (k, v) in new_map {
debug!("edit: add {:?}", k);
additions.push((k, v));
}

Ok(PathFrecencyDiff::new(additions, removals))
}

pub fn serialize(matches: &Vec<(String, f64)>) -> String {
format!(
r#"# Edit your frecency fearlessly!
#
# Lines starting with '#' are comments. sh-esque quoting and escapes may be used in paths.
# Columns are whitespace separated. The first column is the current score, the second is the path.
# Any changes saved here will be applied back to your frecency database immediately.
{}"#,
matches
.iter()
.map(|(s, w)| format!("{}\t{}", w, snailquote::escape(s)))
.collect::<Vec<String>>()
.join("\n")
)
}

pub fn deserialize(s: &str) -> Result<HashMap<String, f64>, String> {
let mut res = HashMap::new();
for mut line in s.lines() {
line = line.trim();

if line.len() == 0 || line.starts_with('#') {
continue;
}

let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect();

if parts.len() != 2 {
return Err(format!(
"line '{}' did not have whitespace to split on",
line
));
}

let path = snailquote::unescape(parts[1])?;
let w = parts[0]
.parse::<f64>()
.map_err(|e| format!("could not parse {} as float: {}", parts[1], e))?;

res.insert(path, w);
}

Ok(res)
}
107 changes: 69 additions & 38 deletions src/frecency.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::f64;
use std::fmt;
use std::hash::Hash;
use std::time::{SystemTime, UNIX_EPOCH};
use std::fmt;

const DECAY_RATE: f64 = f64::consts::LN_2 / (30. * 24. * 60. * 60.);

Expand All @@ -19,6 +19,16 @@ where
max_size: usize,
}

#[derive(Debug, Clone, PartialEq)]
pub struct FrecencyView<'a, T, I>
where
T: Hash + Eq + Ord + Clone,
T: 'a,
I: IntoIterator<Item = (&'a T, &'a f64)>,
{
items: I,
}

impl<T> Frecency<T>
where
T: Hash + Eq + Ord + Clone + fmt::Debug,
Expand Down Expand Up @@ -62,6 +72,10 @@ where
self.insert_with_time(key, SystemTime::now())
}

pub fn overwrite(&mut self, key: T, value: f64) {
self.frecency.insert(key, value);
}

fn insert_with_time(&mut self, key: T, now: SystemTime) {
if !self.frecency.contains_key(&key) {
self.visit_with_time(key, now)
Expand Down Expand Up @@ -93,32 +107,36 @@ where
}
}

#[cfg(test)]
fn items(&self) -> Vec<&T> {
self.items_with_frecency().iter().map(|&(k, _)| k).collect()
}

pub fn items_with_frecency(&self) -> Vec<(&T, f64)> {
let mut v = self.frecency
.iter()
.map(|(ref t, f)| (*t, f.clone()))
.collect::<Vec<_>>();
v.sort_by(descending_frecency);
v
pub fn items(&self) -> FrecencyView<T, &HashMap<T, f64>> {
FrecencyView {
items: &self.frecency,
}
}

pub fn remove(&mut self, key: &T) -> Option<f64> {
self.frecency.remove(key)
}
}

pub fn normalized_frecency(&self) -> Vec<(&T, f64)> {
let items = self.items_with_frecency();
impl<'a, T, I> FrecencyView<'a, T, I>
where
T: Hash + Eq + Ord + Clone,
T: 'a,
I: IntoIterator<Item = (&'a T, &'a f64)>,
{
pub fn normalized(self) -> Vec<(&'a T, f64)> {
let mut items: Vec<_> = self
.items
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect();
if items.len() == 0 {
return items;
return Vec::new();
}
items.sort_by(descending_frecency);
let min = items[items.len() - 1].1;
let max = items[0].1;
let mut items: Vec<_> = items
items
.into_iter()
.map(|(s, v)| {
let normalized = (v - min) / (max - min);
Expand All @@ -128,9 +146,14 @@ where
(s, normalized)
}
})
.collect();
items.sort_by(descending_frecency);
items
.collect()
}

pub fn raw(self) -> Vec<(&'a T, f64)> {
self.items
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect()
}
}

Expand All @@ -143,41 +166,49 @@ pub fn descending_frecency<T>(lhs: &(T, f64), rhs: &(T, f64)) -> Ordering {

#[cfg(test)]
mod test {
use super::Frecency;
use std::time::{SystemTime, UNIX_EPOCH};
use super::{Frecency, FrecencyView};
use std;
use std::hash::Hash;
use std::time;
use std::time::{SystemTime, UNIX_EPOCH};

fn timef(u: u64) -> SystemTime {
UNIX_EPOCH + time::Duration::from_secs(u)
}

fn keys<'a, T, I>(f: FrecencyView<'a, T, I>) -> Vec<T>
where
I: IntoIterator<Item = (&'a T, &'a f64)>,
T: 'a,
T: Ord + Clone + Hash + std::fmt::Debug,
{
f.normalized().into_iter().map(|(k, _)| k.clone()).collect()
}

#[test]
fn basic_frecency_test() {
let mut f = Frecency::<String>::new(100);
f.visit_with_time("foo".to_string(), timef(10));
f.visit_with_time("bar".to_string(), timef(20));
f.visit_with_time("foo".to_string(), timef(50));
f.insert_with_time("quux".to_string(), timef(70));
assert_eq!(
f.items(),
vec![&"foo".to_string(), &"quux".to_string(), &"bar".to_string()]
);
let mut f = Frecency::<&str>::new(100);
f.visit_with_time("foo", timef(10));
f.visit_with_time("bar", timef(20));
f.visit_with_time("foo", timef(50));
f.insert_with_time("quux", timef(70));
assert_eq!(keys(f.items()), vec!["foo", "quux", "bar"]);
let f_clone = f.clone();
f.insert_with_time("quux".to_string(), timef(77));
assert_eq!(f_clone.items_with_frecency(), f.items_with_frecency());
f.insert_with_time("quux", timef(77));
assert_eq!(f_clone.items(), f.items());
}

#[test]
fn trims_min() {
let mut f = Frecency::<&str>::new(2);
f.visit_with_time("foo", timef(10));
assert_eq!(f.items().len(), 1);
assert_eq!(f.items().normalized().len(), 1);
f.visit_with_time("bar", timef(10));
f.visit_with_time("bar", timef(10));
f.visit_with_time("bar", timef(20));
assert_eq!(f.items().clone(), vec![&"bar", &"foo"]);
assert_eq!(keys(f.items()), vec!["bar", "foo"]);
f.visit_with_time("baz", timef(30));
assert_eq!(f.items().clone(), vec![&"bar", &"baz"]);
assert_eq!(keys(f.items()), vec!["bar", "baz"]);
}

#[test]
Expand All @@ -190,6 +221,6 @@ mod test {
f.visit_with_time("foo", now - time::Duration::from_secs(31 * 24 * 60 * 60));
f.visit_with_time("foo", now - time::Duration::from_secs(31 * 24 * 60 * 60));
f.visit_with_time("bar", now);
assert_eq!(f.items().clone(), vec![&"bar", &"foo"]);
assert_eq!(keys(f.items()), vec!["bar", "foo"]);
}
}
Loading