feat(service): prefer recently/often used applications in search
This commit is contained in:
parent
f382690b28
commit
4eef0caae5
6 changed files with 225 additions and 25 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1122,6 +1122,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-oneshot",
|
||||
"async-trait",
|
||||
"dirs 4.0.0",
|
||||
"flume",
|
||||
"futures",
|
||||
"futures_codec",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ edition = "2018"
|
|||
anyhow = "1.0.56"
|
||||
async-oneshot = "0.5.0"
|
||||
async-trait = "0.1.53"
|
||||
dirs = "4.0.0"
|
||||
futures = "0.3.21"
|
||||
futures_codec = "0.4.1"
|
||||
gen-z = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@
|
|||
|
||||
mod client;
|
||||
mod plugins;
|
||||
mod recent;
|
||||
mod priority;
|
||||
|
||||
pub use client::*;
|
||||
pub use plugins::config;
|
||||
pub use plugins::external::load;
|
||||
|
||||
use crate::plugins::*;
|
||||
use crate::recent::RecentUseStorage;
|
||||
use crate::priority::Priority;
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::{future, SinkExt, Stream, StreamExt};
|
||||
use pop_launcher::*;
|
||||
|
|
@ -16,7 +20,7 @@ use regex::Regex;
|
|||
use slab::Slab;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
io::{self, Write},
|
||||
io::{self, Write}, path::PathBuf,
|
||||
};
|
||||
|
||||
pub type PluginKey = usize;
|
||||
|
|
@ -34,7 +38,38 @@ pub struct PluginHelp {
|
|||
pub help: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
pub fn ensure_cache_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let cachepath = dirs::home_dir().ok_or("failed to find home dir")?.join(".cache/pop-launcher");
|
||||
std::fs::create_dir_all(&cachepath)?;
|
||||
Ok(cachepath.join("recent"))
|
||||
}
|
||||
|
||||
pub fn store_cache(storage: &RecentUseStorage) {
|
||||
let write_recent = || -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cachepath = ensure_cache_path()?;
|
||||
Ok(serde_json::to_writer(
|
||||
std::fs::File::create(cachepath)?,
|
||||
storage
|
||||
)?)
|
||||
};
|
||||
if let Err(e)= write_recent() {
|
||||
eprintln!("could not write to cache file\n{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn main() {
|
||||
let cachepath = ensure_cache_path();
|
||||
let read_recent = || -> Result<RecentUseStorage, Box<dyn std::error::Error>> {
|
||||
let cachepath = std::fs::File::open(cachepath?)?;
|
||||
Ok(serde_json::from_reader(cachepath)?)
|
||||
};
|
||||
let recent = match read_recent() {
|
||||
Ok(r) => r,
|
||||
Err(e) => {eprintln!("could not read cache file\n{}", e); RecentUseStorage::default()}
|
||||
};
|
||||
|
||||
|
||||
// Listens for a stream of requests from stdin.
|
||||
let input_stream = json_input_stream(tokio::io::stdin()).filter_map(|result| {
|
||||
future::ready(match result {
|
||||
|
|
@ -49,7 +84,7 @@ pub async fn main() {
|
|||
let (output_tx, output_rx) = flume::bounded(16);
|
||||
|
||||
// Service will operate for as long as it is being awaited
|
||||
let service = Service::new(output_tx.into_sink()).exec(input_stream);
|
||||
let service = Service::new(output_tx.into_sink(), recent).exec(input_stream);
|
||||
|
||||
// Responses from the service will be streamed to stdout
|
||||
let responder = async move {
|
||||
|
|
@ -73,10 +108,11 @@ pub struct Service<O> {
|
|||
output: O,
|
||||
plugins: Slab<PluginConnector>,
|
||||
search_scheduled: bool,
|
||||
recent: RecentUseStorage,
|
||||
}
|
||||
|
||||
impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||
pub fn new(output: O) -> Self {
|
||||
pub fn new(output: O, recent: RecentUseStorage) -> Self {
|
||||
Self {
|
||||
active_search: Vec::new(),
|
||||
associated_list: HashMap::new(),
|
||||
|
|
@ -86,6 +122,7 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
|||
no_sort: false,
|
||||
plugins: Slab::new(),
|
||||
search_scheduled: false,
|
||||
recent,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,16 +278,24 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
|||
}
|
||||
|
||||
async fn activate(&mut self, id: Indice) {
|
||||
let mut ex = None;
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
ex = meta.cache_identifier();
|
||||
let _ = plugin
|
||||
.sender_exec()
|
||||
.send_async(Request::Activate(meta.id))
|
||||
.await;
|
||||
}
|
||||
if let Some(e) = ex {
|
||||
self.recent.add(&e);
|
||||
store_cache(&self.recent);
|
||||
}
|
||||
}
|
||||
|
||||
async fn activate_context(&mut self, id: Indice, context: Indice) {
|
||||
let mut ex = None;
|
||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||
ex = meta.cache_identifier();
|
||||
let _ = plugin
|
||||
.sender_exec()
|
||||
.send_async(Request::ActivateContext {
|
||||
|
|
@ -259,6 +304,10 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
|||
})
|
||||
.await;
|
||||
}
|
||||
if let Some(e) = ex {
|
||||
self.recent.add(&e);
|
||||
store_cache(&self.recent);
|
||||
}
|
||||
}
|
||||
|
||||
fn append(&mut self, plugin: PluginKey, append: PluginSearchResult) {
|
||||
|
|
@ -445,6 +494,7 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
|||
ref mut no_sort,
|
||||
ref last_query,
|
||||
ref plugins,
|
||||
ref recent,
|
||||
..
|
||||
} = self;
|
||||
|
||||
|
|
@ -497,23 +547,6 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
|||
})
|
||||
}
|
||||
|
||||
let a_weight = calculate_weight(&a.1, query);
|
||||
let b_weight = calculate_weight(&b.1, query);
|
||||
|
||||
match a_weight.partial_cmp(&b_weight) {
|
||||
Some(Ordering::Equal) => {
|
||||
let a_len = a.1.name.len();
|
||||
let b_len = b.1.name.len();
|
||||
|
||||
a_len.cmp(&b_len)
|
||||
}
|
||||
Some(Ordering::Less) => Ordering::Greater,
|
||||
Some(Ordering::Greater) => Ordering::Less,
|
||||
None => Ordering::Greater,
|
||||
}
|
||||
});
|
||||
|
||||
active_search.sort_by(|a, b| {
|
||||
let plug1 = match plugins.get(a.0) {
|
||||
Some(plug) => plug,
|
||||
None => return Ordering::Greater,
|
||||
|
|
@ -524,11 +557,18 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
|||
None => return Ordering::Less,
|
||||
};
|
||||
|
||||
plug1
|
||||
.config
|
||||
.query
|
||||
.priority
|
||||
.cmp(&plug2.config.query.priority)
|
||||
let get_prio = |sr: &PluginSearchResult, plg: &PluginConnector| -> Priority {
|
||||
let ex = sr.cache_identifier();
|
||||
Priority {
|
||||
plugin_priority: plg.config.query.priority,
|
||||
match_score: calculate_weight(sr, query),
|
||||
recent_use_index: ex.as_ref().map(|s| recent.get_recent(s)).unwrap_or(0),
|
||||
use_freq: ex.as_ref().map(|s| recent.get_freq(s)).unwrap_or(0),
|
||||
execlen: sr.name.len()
|
||||
}
|
||||
};
|
||||
|
||||
get_prio(&b.1, plug2).cmp(&get_prio(&a.1, plug1))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
59
service/src/priority.rs
Normal file
59
service/src/priority.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use crate::PluginPriority;
|
||||
|
||||
|
||||
// holds all values used for ordering search results
|
||||
pub struct Priority {
|
||||
pub plugin_priority: PluginPriority,
|
||||
pub match_score: f64,
|
||||
pub recent_use_index: usize,
|
||||
pub use_freq: usize,
|
||||
pub execlen: usize,
|
||||
}
|
||||
|
||||
|
||||
fn signum(val: i32) -> f64 {
|
||||
if val > 0 { return 1.0; }
|
||||
if val < 0 { return -1.0; }
|
||||
0.0
|
||||
}
|
||||
|
||||
impl Priority {
|
||||
fn compute_value(&self, other: &Self) -> f64{
|
||||
// increases compared jw-score if this search result
|
||||
// was activated more frequent or recent by constant values
|
||||
let score = self.match_score
|
||||
+ 0.06 * signum(self.recent_use_index as i32 - other.recent_use_index as i32)
|
||||
+ 0.03 * signum(self.use_freq as i32 - other.use_freq as i32);
|
||||
// score cannot surpass exact matches
|
||||
if self.match_score < 1.0 {
|
||||
return score.min(0.99);
|
||||
}
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Priority {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.plugin_priority == other.plugin_priority
|
||||
&& self.compute_value(other) == other.match_score
|
||||
&& self.execlen == other.execlen
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Priority {}
|
||||
|
||||
impl PartialOrd for Priority {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
(other.plugin_priority, self.compute_value(other), self.execlen).partial_cmp(
|
||||
&(self.plugin_priority, other.match_score, other.execlen)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Priority {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
90
service/src/recent.rs
Normal file
90
service/src/recent.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use std::collections::{HashMap, hash_map::DefaultHasher};
|
||||
use std::hash::{Hasher, Hash};
|
||||
use serde::{Deserialize, Serialize, Serializer, Deserializer};
|
||||
|
||||
const SHORTTERM_CAP: usize = 20;
|
||||
const LONGTERM_CAP: usize = 100;
|
||||
|
||||
// Holds a long term storage that tracks how often a search
|
||||
// result was activated, and a short term storage that stores
|
||||
// the order of recently activated search results (higher
|
||||
// vales are more recent).
|
||||
// Keys for both mappings are hashes of the acvtivated result's
|
||||
// command string.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RecentUseStorage {
|
||||
long_term: HashMap<usize, usize>,
|
||||
short_term: HashMap<usize, usize>,
|
||||
}
|
||||
|
||||
|
||||
fn hash_key<K: Hash>(key: K) -> usize {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
key.hash(&mut hasher);
|
||||
hasher.finish() as usize
|
||||
}
|
||||
|
||||
|
||||
impl RecentUseStorage {
|
||||
pub fn add<K: Hash>(&mut self, exec: &K) {
|
||||
let key = hash_key(exec);
|
||||
*self.long_term.entry(key).or_insert(0) += 1;
|
||||
let short_term_idx = self.short_term.values().max().unwrap_or( &0)+1;
|
||||
self.short_term.insert(key, short_term_idx);
|
||||
self.trim()
|
||||
}
|
||||
|
||||
fn trim(&mut self) {
|
||||
while self.short_term.len() > SHORTTERM_CAP {
|
||||
let key = *self.short_term.iter().min_by_key(|kv| kv.1).unwrap().0;
|
||||
self.short_term.remove(&key);
|
||||
}
|
||||
|
||||
while self.long_term.values().sum::<usize>() > LONGTERM_CAP {
|
||||
let mut delete_keys = Vec::new();
|
||||
for (k, v) in self.long_term.iter_mut() {
|
||||
*v /= 2;
|
||||
if *v == 0 {
|
||||
delete_keys.push(*k);
|
||||
}
|
||||
}
|
||||
for k in delete_keys {
|
||||
self.long_term.remove(&k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_recent<K: Hash>(&self, exec: &K) -> usize {
|
||||
self.short_term.get(&hash_key(exec)).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn get_freq<K: Hash>(&self, exec: &K) -> usize {
|
||||
self.long_term.get(&hash_key(exec)).copied().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RecentUseStorage {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut stvec: Vec<_> = self.short_term.keys().copied().collect();
|
||||
stvec.sort_by_key(|k| self.short_term[k]);
|
||||
(&self.long_term, stvec).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RecentUseStorage {
|
||||
fn deserialize<D>(deserializer: D) -> Result<RecentUseStorage, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
type SerType = (HashMap<usize, usize>, Vec<usize>);
|
||||
let (long_term, stv) = SerType::deserialize(deserializer)?;
|
||||
let short_term: HashMap<_, _> = stv.into_iter().enumerate().map(|(v,k)| (k,v)).collect();
|
||||
Ok(RecentUseStorage {
|
||||
long_term,
|
||||
short_term,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +110,15 @@ pub struct PluginSearchResult {
|
|||
pub window: Option<(Generation, Indice)>,
|
||||
}
|
||||
|
||||
impl PluginSearchResult {
|
||||
pub fn cache_identifier(&self) -> Option<String> {
|
||||
// the exec field may clash in multiple search results as the arguments
|
||||
// are cut from the string
|
||||
// self.exec.to_owned().unwrap_or_else(|| self.name.to_owned())
|
||||
self.exec.as_ref().map(|_| self.name.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
// Sent to the input pipe of the launcher service, and disseminated to its plugins.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub enum Request {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue