z_rs/
lib.rs

1//! z_rs implements a directory jumping command line utility
2//!
3//! The original is a 200 line shell classic
4//! https://github.com/rupa/z/blob/master/z.sh
5//!
6//! This is an **opinionated** rewrite instead of being 1:1
7//! with the original. Most notably, this does not override
8//! `cd` in any way, and needs to be used instead of `cd` to
9//! build the cache.
10//!
11//! This provides a `z_rs` binary, which can be invoked
12//! for configuration and lookup.
13//!
14//! ## Installation
15//!
16//! Currently unpublished, do
17//!
18//! ```sh
19//! cargo install https://github.com/suyash/z-rs
20//! ```
21//!
22//! ## Configuration
23//!
24//! add
25//!
26//! ```sh
27//! eval "$(z_rs init --cmd z --cache $HOME/.z_rs)"
28//! ```
29//!
30//! in your shell configuration file.
31//!
32//! The `--cmd` option is the name of the command that will
33//! be added to the shell. For example, in this case, a `z`
34//! command will be created.
35//!
36//! ## Usage
37//!
38//! If configured with the command name `z`, then simply do
39//!
40//! ```sh
41//! z foo bar
42//! ```
43//!
44//! will try to match `foo` and `bar` and if a match is found
45//! in the cache, will `cd` to it, otherwise error out.
46//!
47//! Matches occur based on "frecency". The rank of a path is
48//! how many times it has been accessed. The command will return
49//! the highest ranked matching record. If there are multiple,
50//! it will return the most recently accessed one.
51
52#![deny(missing_docs)]
53
54use std::collections::HashMap;
55
56use bstr::ByteSlice;
57use serde::{Deserialize, Serialize};
58
59#[cfg(test)]
60mod tests;
61
62#[derive(Serialize, Deserialize, Debug)]
63/// Records contains a record of all stored values
64pub struct Records {
65    map: HashMap<Vec<u8>, (f64, u64)>,
66    sum: f64,
67}
68
69impl Records {
70    /// new creates a new Records instance
71    pub fn new() -> Records {
72        Self::default()
73    }
74
75    /// update increments the rank of the provided path
76    /// and updates its timestamp
77    pub fn update<T>(&mut self, path: T, ts: u64)
78    where
79        T: AsRef<[u8]>,
80    {
81        let v = self.map.entry(path.as_ref().to_owned()).or_insert((0.0, 0));
82        v.0 += 1.0;
83        v.1 = ts;
84
85        self.sum += 1.0;
86
87        // TODO: implement pruning and reorging
88    }
89
90    /// query matches a record from the saved records from the given
91    /// arguments. The rank of a record is how many times it has been used.
92    /// The match is returned based on "frecency", i.e.
93    /// the highest ranked matching record is given priority,
94    /// if there are multiple then the most recently accessed record
95    /// with the same rank is returned.
96    pub fn query<T>(&self, arguments: &[T]) -> Option<Vec<u8>>
97    where
98        T: AsRef<[u8]>,
99    {
100        let mut ans = None;
101
102        // NOTE: since rust hash maps are swiss tables now,
103        // assuming straight up iteration is a feasible op
104        for (path, (score, timestamp)) in self.map.iter() {
105            let mut index = 0;
106
107            for argument in arguments {
108                match path[index..].find(argument) {
109                    None => {
110                        index = path.len() + 1;
111                        break;
112                    }
113                    Some(fix) => {
114                        index += fix + argument.as_ref().len();
115                    }
116                }
117            }
118
119            if index <= path.len() {
120                ans = match ans {
121                    None => Some((path, (score, timestamp))),
122                    Some((ppath, (pscore, ptimestamp))) => {
123                        if score > pscore
124                            || ((score - pscore).abs() < std::f64::EPSILON
125                                && timestamp > ptimestamp)
126                        {
127                            Some((path, (score, timestamp)))
128                        } else {
129                            Some((ppath, (pscore, ptimestamp)))
130                        }
131                    }
132                };
133            }
134        }
135
136        match ans {
137            None => None,
138            Some((path, _)) => Some(path.clone()),
139        }
140    }
141
142    /// query_and_update runs a query followed by an update
143    /// with the result of the query.
144    pub fn query_and_update<T>(&mut self, arguments: &[T], ts: u64) -> Option<Vec<u8>>
145    where
146        T: AsRef<[u8]>,
147    {
148        match self.query(arguments) {
149            None => None,
150            Some(v) => {
151                self.update(&v, ts);
152                Some(v)
153            }
154        }
155    }
156}
157
158impl Default for Records {
159    fn default() -> Self {
160        Records {
161            map: HashMap::new(),
162            sum: 0.0,
163        }
164    }
165}