feat(service): prefer recently/often used applications in search

This commit is contained in:
Thomas Ruprecht 2022-11-01 22:41:23 +01:00 committed by GitHub
parent f382690b28
commit 4eef0caae5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 225 additions and 25 deletions

1
Cargo.lock generated
View file

@ -1122,6 +1122,7 @@ dependencies = [
"anyhow",
"async-oneshot",
"async-trait",
"dirs 4.0.0",
"flume",
"futures",
"futures_codec",

View file

@ -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"

View file

@ -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
View 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
View 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,
})
}
}

View file

@ -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 {