Skip to content

Commit

Permalink
Merge pull request #71 from euank/tmp/wip/editx
Browse files Browse the repository at this point in the history
 edit: implement pazi edit
  • Loading branch information
euank authored Aug 4, 2018
2 parents 0b9c23d + 74959d5 commit dcacfde
Show file tree
Hide file tree
Showing 11 changed files with 681 additions and 200 deletions.
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

0 comments on commit dcacfde

Please sign in to comment.