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",
|
"anyhow",
|
||||||
"async-oneshot",
|
"async-oneshot",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"dirs 4.0.0",
|
||||||
"flume",
|
"flume",
|
||||||
"futures",
|
"futures",
|
||||||
"futures_codec",
|
"futures_codec",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ edition = "2018"
|
||||||
anyhow = "1.0.56"
|
anyhow = "1.0.56"
|
||||||
async-oneshot = "0.5.0"
|
async-oneshot = "0.5.0"
|
||||||
async-trait = "0.1.53"
|
async-trait = "0.1.53"
|
||||||
|
dirs = "4.0.0"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
futures_codec = "0.4.1"
|
futures_codec = "0.4.1"
|
||||||
gen-z = "0.1.0"
|
gen-z = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod plugins;
|
mod plugins;
|
||||||
|
mod recent;
|
||||||
|
mod priority;
|
||||||
|
|
||||||
pub use client::*;
|
pub use client::*;
|
||||||
pub use plugins::config;
|
pub use plugins::config;
|
||||||
pub use plugins::external::load;
|
pub use plugins::external::load;
|
||||||
|
|
||||||
use crate::plugins::*;
|
use crate::plugins::*;
|
||||||
|
use crate::recent::RecentUseStorage;
|
||||||
|
use crate::priority::Priority;
|
||||||
use flume::{Receiver, Sender};
|
use flume::{Receiver, Sender};
|
||||||
use futures::{future, SinkExt, Stream, StreamExt};
|
use futures::{future, SinkExt, Stream, StreamExt};
|
||||||
use pop_launcher::*;
|
use pop_launcher::*;
|
||||||
|
|
@ -16,7 +20,7 @@ use regex::Regex;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
io::{self, Write},
|
io::{self, Write}, path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type PluginKey = usize;
|
pub type PluginKey = usize;
|
||||||
|
|
@ -34,7 +38,38 @@ pub struct PluginHelp {
|
||||||
pub help: Option<String>,
|
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() {
|
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.
|
// Listens for a stream of requests from stdin.
|
||||||
let input_stream = json_input_stream(tokio::io::stdin()).filter_map(|result| {
|
let input_stream = json_input_stream(tokio::io::stdin()).filter_map(|result| {
|
||||||
future::ready(match result {
|
future::ready(match result {
|
||||||
|
|
@ -49,7 +84,7 @@ pub async fn main() {
|
||||||
let (output_tx, output_rx) = flume::bounded(16);
|
let (output_tx, output_rx) = flume::bounded(16);
|
||||||
|
|
||||||
// Service will operate for as long as it is being awaited
|
// 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
|
// Responses from the service will be streamed to stdout
|
||||||
let responder = async move {
|
let responder = async move {
|
||||||
|
|
@ -73,10 +108,11 @@ pub struct Service<O> {
|
||||||
output: O,
|
output: O,
|
||||||
plugins: Slab<PluginConnector>,
|
plugins: Slab<PluginConnector>,
|
||||||
search_scheduled: bool,
|
search_scheduled: bool,
|
||||||
|
recent: RecentUseStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||||
pub fn new(output: O) -> Self {
|
pub fn new(output: O, recent: RecentUseStorage) -> Self {
|
||||||
Self {
|
Self {
|
||||||
active_search: Vec::new(),
|
active_search: Vec::new(),
|
||||||
associated_list: HashMap::new(),
|
associated_list: HashMap::new(),
|
||||||
|
|
@ -86,6 +122,7 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||||
no_sort: false,
|
no_sort: false,
|
||||||
plugins: Slab::new(),
|
plugins: Slab::new(),
|
||||||
search_scheduled: false,
|
search_scheduled: false,
|
||||||
|
recent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,16 +278,24 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn activate(&mut self, id: Indice) {
|
async fn activate(&mut self, id: Indice) {
|
||||||
|
let mut ex = None;
|
||||||
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||||
|
ex = meta.cache_identifier();
|
||||||
let _ = plugin
|
let _ = plugin
|
||||||
.sender_exec()
|
.sender_exec()
|
||||||
.send_async(Request::Activate(meta.id))
|
.send_async(Request::Activate(meta.id))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
if let Some(e) = ex {
|
||||||
|
self.recent.add(&e);
|
||||||
|
store_cache(&self.recent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn activate_context(&mut self, id: Indice, context: Indice) {
|
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) {
|
if let Some((plugin, meta)) = self.search_result(id as usize) {
|
||||||
|
ex = meta.cache_identifier();
|
||||||
let _ = plugin
|
let _ = plugin
|
||||||
.sender_exec()
|
.sender_exec()
|
||||||
.send_async(Request::ActivateContext {
|
.send_async(Request::ActivateContext {
|
||||||
|
|
@ -259,6 +304,10 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
if let Some(e) = ex {
|
||||||
|
self.recent.add(&e);
|
||||||
|
store_cache(&self.recent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append(&mut self, plugin: PluginKey, append: PluginSearchResult) {
|
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 mut no_sort,
|
||||||
ref last_query,
|
ref last_query,
|
||||||
ref plugins,
|
ref plugins,
|
||||||
|
ref recent,
|
||||||
..
|
..
|
||||||
} = self;
|
} = 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) {
|
let plug1 = match plugins.get(a.0) {
|
||||||
Some(plug) => plug,
|
Some(plug) => plug,
|
||||||
None => return Ordering::Greater,
|
None => return Ordering::Greater,
|
||||||
|
|
@ -524,11 +557,18 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
|
||||||
None => return Ordering::Less,
|
None => return Ordering::Less,
|
||||||
};
|
};
|
||||||
|
|
||||||
plug1
|
let get_prio = |sr: &PluginSearchResult, plg: &PluginConnector| -> Priority {
|
||||||
.config
|
let ex = sr.cache_identifier();
|
||||||
.query
|
Priority {
|
||||||
.priority
|
plugin_priority: plg.config.query.priority,
|
||||||
.cmp(&plug2.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)>,
|
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.
|
// Sent to the input pipe of the launcher service, and disseminated to its plugins.
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue