skim-demo (#695)

* skim-demo

* skim some more

* Weight first word match higher (#712)

* some improvements

* make skim opt-in

---------

Co-authored-by: Frank Hamand <frankhamand@gmail.com>
This commit is contained in:
Conrad Ludgate 2023-03-19 20:49:57 +00:00 committed by GitHub
parent 529793fbcb
commit edcd477153
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 595 additions and 72 deletions

326
Cargo.lock generated
View File

@ -37,6 +37,12 @@ version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.58" version = "0.1.58"
@ -98,6 +104,7 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"skim",
"tiny-bip39", "tiny-bip39",
"tokio", "tokio",
"tracing-subscriber", "tracing-subscriber",
@ -324,7 +331,7 @@ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde", "serde",
"time", "time 0.1.44",
"wasm-bindgen", "wasm-bindgen",
"winapi", "winapi",
] ]
@ -452,6 +459,20 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff"
[[package]]
name = "crossbeam"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
dependencies = [
"cfg-if",
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.6" version = "0.5.6"
@ -462,6 +483,30 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset 0.7.1",
"scopeguard",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.6" version = "0.3.6"
@ -518,6 +563,82 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "darling"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "defer-drop"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57"
dependencies = [
"crossbeam-channel",
"once_cell",
]
[[package]]
name = "derive_builder"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.5" version = "0.10.5"
@ -547,6 +668,16 @@ dependencies = [
"dirs-sys", "dirs-sys",
] ]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.3.7" version = "0.3.7"
@ -558,6 +689,17 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.3" version = "0.15.3"
@ -767,6 +909,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.6" version = "0.14.6"
@ -973,6 +1124,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -1194,6 +1351,24 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.16" version = "0.3.16"
@ -1224,6 +1399,31 @@ dependencies = [
"windows-sys 0.36.1", "windows-sys 0.36.1",
] ]
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags",
"cfg-if",
"libc",
"memoffset 0.6.5",
"pin-utils",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.1"
@ -1563,6 +1763,28 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rayon"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@ -1971,6 +2193,31 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90531723b08e4d6d71b791108faf51f03e1b4a7784f96b2b87f852ebc247228" checksum = "e90531723b08e4d6d71b791108faf51f03e1b4a7784f96b2b87f852ebc247228"
[[package]]
name = "skim"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cebed5f897cd6c0d80fbe30adb36c0abf7400e93043a63ae56458495642b3485"
dependencies = [
"beef",
"bitflags",
"chrono",
"crossbeam",
"defer-drop",
"derive_builder",
"fuzzy-matcher",
"lazy_static",
"log",
"nix 0.25.1",
"rayon",
"regex",
"time 0.3.17",
"timer",
"tuikit",
"unicode-width",
"vte",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.7" version = "0.4.7"
@ -2192,6 +2439,17 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.1.3"
@ -2251,6 +2509,31 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "time"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [
"serde",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "timer"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "tiny-bip39" name = "tiny-bip39"
version = "1.0.0" version = "1.0.0"
@ -2464,6 +2747,20 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "tuikit"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8"
dependencies = [
"bitflags",
"lazy_static",
"log",
"nix 0.24.3",
"term",
"unicode-width",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.15.0" version = "1.15.0"
@ -2538,6 +2835,12 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.2.1" version = "1.2.1"
@ -2559,6 +2862,27 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vte"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aae21c12ad2ec2d168c236f369c38ff332bc1134f7246350dca641437365045"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.3.2" version = "2.3.2"

View File

@ -73,6 +73,7 @@ semver = "1.0.14"
runtime-format = "0.1.2" runtime-format = "0.1.2"
tiny-bip39 = "1" tiny-bip39 = "1"
futures-util = "0.3" futures-util = "0.3"
skim = { version = "0.10.2", default-features = false }
# from tui # from tui
bitflags = "1.3" bitflags = "1.3"

View File

@ -21,9 +21,9 @@ use super::{
}; };
pub struct Context { pub struct Context {
session: String, pub session: String,
cwd: String, pub cwd: String,
hostname: String, pub hostname: String,
} }
pub fn current_context() -> Context { pub fn current_context() -> Context {
@ -85,6 +85,8 @@ pub trait Database: Send + Sync {
) -> Result<Vec<History>>; ) -> Result<Vec<History>>;
async fn query_history(&self, query: &str) -> Result<Vec<History>>; async fn query_history(&self, query: &str) -> Result<Vec<History>>;
async fn all_with_count(&self) -> Result<Vec<(History, i32)>>;
} }
// Intended for use on a developer machine and not a sync server. // Intended for use on a developer machine and not a sync server.
@ -428,7 +430,7 @@ impl Database for Sqlite {
match search_mode { match search_mode {
SearchMode::Prefix => sql.and_where_like_left("command", query), SearchMode::Prefix => sql.and_where_like_left("command", query),
SearchMode::FullText => sql.and_where_like_any("command", query), SearchMode::FullText => sql.and_where_like_any("command", query),
SearchMode::Fuzzy => { SearchMode::Skim | SearchMode::Fuzzy => {
// don't recompile the regex on successive calls! // don't recompile the regex on successive calls!
lazy_static! { lazy_static! {
static ref SPLIT_REGEX: Regex = Regex::new(r" +").unwrap(); static ref SPLIT_REGEX: Regex = Regex::new(r" +").unwrap();
@ -492,6 +494,40 @@ impl Database for Sqlite {
Ok(res) Ok(res)
} }
async fn all_with_count(&self) -> Result<Vec<(History, i32)>> {
debug!("listing history");
let mut query = SqlBuilder::select_from(SqlName::new("history").alias("h").baquoted());
query
.fields(&[
"id",
"max(timestamp) as timestamp",
"max(duration) as duration",
"exit",
"command",
"group_concat(cwd, ':') as cwd",
"group_concat(session) as session",
"group_concat(hostname, ',') as hostname",
"count(*) as count",
])
.group_by("command")
.group_by("exit")
.order_desc("timestamp");
let query = query.sql().expect("bug in list query. please report");
let res = sqlx::query(&query)
.map(|row: SqliteRow| {
let count: i32 = row.get("count");
(Self::query_history(row), count)
})
.fetch_all(&self.pool)
.await?;
Ok(res)
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -18,7 +18,7 @@ pub const LAST_SYNC_FILENAME: &str = "last_sync_time";
pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time"; pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time";
pub const LATEST_VERSION_FILENAME: &str = "latest_version"; pub const LATEST_VERSION_FILENAME: &str = "latest_version";
#[derive(Clone, Debug, Deserialize, Copy, ValueEnum)] #[derive(Clone, Debug, Deserialize, Copy, ValueEnum, PartialEq)]
pub enum SearchMode { pub enum SearchMode {
#[serde(rename = "prefix")] #[serde(rename = "prefix")]
Prefix, Prefix,
@ -29,6 +29,9 @@ pub enum SearchMode {
#[serde(rename = "fuzzy")] #[serde(rename = "fuzzy")]
Fuzzy, Fuzzy,
#[serde(rename = "skim")]
Skim,
} }
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)] #[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]

View File

@ -16,6 +16,7 @@ mod cursor;
mod duration; mod duration;
mod history_list; mod history_list;
mod interactive; mod interactive;
mod skim_impl;
pub use duration::{format_duration, format_duration_into}; pub use duration::{format_duration, format_duration_into};
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]

View File

@ -1,4 +1,4 @@
use std::time::Duration; use std::{sync::Arc, time::Duration};
use crate::tui::{ use crate::tui::{
buffer::Buffer, buffer::Buffer,
@ -8,10 +8,10 @@ use crate::tui::{
}; };
use atuin_client::history::History; use atuin_client::history::History;
use super::format_duration; use super::{format_duration, interactive::HistoryWrapper};
pub struct HistoryList<'a> { pub struct HistoryList<'a> {
history: &'a [History], history: &'a [Arc<HistoryWrapper>],
block: Option<Block<'a>>, block: Option<Block<'a>>,
} }
@ -77,7 +77,7 @@ impl<'a> StatefulWidget for HistoryList<'a> {
} }
impl<'a> HistoryList<'a> { impl<'a> HistoryList<'a> {
pub fn new(history: &'a [History]) -> Self { pub fn new(history: &'a [Arc<HistoryWrapper>]) -> Self {
Self { Self {
history, history,
block: None, block: None,

View File

@ -1,16 +1,10 @@
use std::{ use std::{
io::{stdout, Write}, io::{stdout, Write},
ops::Deref,
sync::Arc,
time::Duration, time::Duration,
}; };
use crate::tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Paragraph},
Frame, Terminal,
};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent}, event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
execute, terminal, execute, terminal,
@ -18,12 +12,12 @@ use crossterm::{
use eyre::Result; use eyre::Result;
use futures_util::FutureExt; use futures_util::FutureExt;
use semver::Version; use semver::Version;
use skim::SkimItem;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use atuin_client::{ use atuin_client::{
database::current_context,
database::Context,
database::Database, database::Database,
database::{current_context, Context},
history::History, history::History,
settings::{ExitMode, FilterMode, SearchMode, Settings}, settings::{ExitMode, FilterMode, SearchMode, Settings},
}; };
@ -32,18 +26,35 @@ use super::{
cursor::Cursor, cursor::Cursor,
history_list::{HistoryList, ListState, PREFIX_LENGTH}, history_list::{HistoryList, ListState, PREFIX_LENGTH},
}; };
use crate::VERSION; use crate::{
tui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Paragraph},
Frame, Terminal,
},
VERSION,
};
const RETURN_ORIGINAL: usize = usize::MAX; const RETURN_ORIGINAL: usize = usize::MAX;
const RETURN_QUERY: usize = usize::MAX - 1; const RETURN_QUERY: usize = usize::MAX - 1;
struct State { struct State {
history_count: i64, history_count: i64,
input: Cursor,
filter_mode: FilterMode,
results_state: ListState,
context: Context,
update_needed: Option<Version>, update_needed: Option<Version>,
results_state: ListState,
search: SearchState,
// only allocated if using skim
all_history: Vec<Arc<HistoryWrapper>>,
}
pub struct SearchState {
pub input: Cursor,
pub filter_mode: FilterMode,
pub context: Context,
} }
impl State { impl State {
@ -51,22 +62,48 @@ impl State {
&mut self, &mut self,
search_mode: SearchMode, search_mode: SearchMode,
db: &mut impl Database, db: &mut impl Database,
) -> Result<Vec<History>> { ) -> Result<Vec<Arc<HistoryWrapper>>> {
let i = self.input.as_str(); let i = self.search.input.as_str();
let results = if i.is_empty() { let results = if i.is_empty() {
db.list(self.filter_mode, &self.context, Some(200), true) db.list(
.await? self.search.filter_mode,
&self.search.context,
Some(200),
true,
)
.await?
.into_iter()
.map(|history| HistoryWrapper { history, count: 1 })
.map(Arc::new)
.collect::<Vec<_>>()
} else if search_mode == SearchMode::Skim {
if self.all_history.is_empty() {
self.all_history = db
.all_with_count()
.await
.unwrap()
.into_iter()
.map(|(history, count)| HistoryWrapper { history, count })
.map(Arc::new)
.collect::<Vec<_>>();
}
super::skim_impl::fuzzy_search(&self.search, &self.all_history).await
} else { } else {
db.search( db.search(
search_mode, search_mode,
self.filter_mode, self.search.filter_mode,
&self.context, &self.search.context,
i, i,
Some(200), Some(200),
None, None,
None, None,
) )
.await? .await?
.into_iter()
.map(|history| HistoryWrapper { history, count: 1 })
.map(Arc::new)
.collect::<Vec<_>>()
}; };
self.results_state.select(0); self.results_state.select(0);
@ -125,47 +162,51 @@ impl State {
return Some(self.results_state.selected() + c); return Some(self.results_state.selected() + c);
} }
KeyCode::Left if ctrl => self KeyCode::Left if ctrl => self
.search
.input .input
.prev_word(&settings.word_chars, settings.word_jump_mode), .prev_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Left => { KeyCode::Left => {
self.input.left(); self.search.input.left();
} }
KeyCode::Char('h') if ctrl => { KeyCode::Char('h') if ctrl => {
self.input.left(); self.search.input.left();
} }
KeyCode::Right if ctrl => self KeyCode::Right if ctrl => self
.search
.input .input
.next_word(&settings.word_chars, settings.word_jump_mode), .next_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Right => self.input.right(), KeyCode::Right => self.search.input.right(),
KeyCode::Char('l') if ctrl => self.input.right(), KeyCode::Char('l') if ctrl => self.search.input.right(),
KeyCode::Char('a') if ctrl => self.input.start(), KeyCode::Char('a') if ctrl => self.search.input.start(),
KeyCode::Home => self.input.start(), KeyCode::Home => self.search.input.start(),
KeyCode::Char('e') if ctrl => self.input.end(), KeyCode::Char('e') if ctrl => self.search.input.end(),
KeyCode::End => self.input.end(), KeyCode::End => self.search.input.end(),
KeyCode::Backspace if ctrl => self KeyCode::Backspace if ctrl => self
.search
.input .input
.remove_prev_word(&settings.word_chars, settings.word_jump_mode), .remove_prev_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Backspace => { KeyCode::Backspace => {
self.input.back(); self.search.input.back();
} }
KeyCode::Delete if ctrl => self KeyCode::Delete if ctrl => self
.search
.input .input
.remove_next_word(&settings.word_chars, settings.word_jump_mode), .remove_next_word(&settings.word_chars, settings.word_jump_mode),
KeyCode::Delete => { KeyCode::Delete => {
self.input.remove(); self.search.input.remove();
} }
KeyCode::Char('w') if ctrl => { KeyCode::Char('w') if ctrl => {
// remove the first batch of whitespace // remove the first batch of whitespace
while matches!(self.input.back(), Some(c) if c.is_whitespace()) {} while matches!(self.search.input.back(), Some(c) if c.is_whitespace()) {}
while self.input.left() { while self.search.input.left() {
if self.input.char().unwrap().is_whitespace() { if self.search.input.char().unwrap().is_whitespace() {
self.input.right(); // found whitespace, go back right self.search.input.right(); // found whitespace, go back right
break; break;
} }
self.input.remove(); self.search.input.remove();
} }
} }
KeyCode::Char('u') if ctrl => self.input.clear(), KeyCode::Char('u') if ctrl => self.search.input.clear(),
KeyCode::Char('r') if ctrl => { KeyCode::Char('r') if ctrl => {
pub static FILTER_MODES: [FilterMode; 4] = [ pub static FILTER_MODES: [FilterMode; 4] = [
FilterMode::Global, FilterMode::Global,
@ -173,9 +214,9 @@ impl State {
FilterMode::Session, FilterMode::Session,
FilterMode::Directory, FilterMode::Directory,
]; ];
let i = self.filter_mode as usize; let i = self.search.filter_mode as usize;
let i = (i + 1) % FILTER_MODES.len(); let i = (i + 1) % FILTER_MODES.len();
self.filter_mode = FILTER_MODES[i]; self.search.filter_mode = FILTER_MODES[i];
} }
KeyCode::Down if self.results_state.selected() == 0 => return Some(RETURN_ORIGINAL), KeyCode::Down if self.results_state.selected() == 0 => return Some(RETURN_ORIGINAL),
KeyCode::Down => { KeyCode::Down => {
@ -194,7 +235,7 @@ impl State {
let i = self.results_state.selected() + 1; let i = self.results_state.selected() + 1;
self.results_state.select(i.min(len - 1)); self.results_state.select(i.min(len - 1));
} }
KeyCode::Char(c) => self.input.insert(c), KeyCode::Char(c) => self.search.input.insert(c),
KeyCode::PageDown => { KeyCode::PageDown => {
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines; let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
let i = self.results_state.selected().saturating_sub(scroll_len); let i = self.results_state.selected().saturating_sub(scroll_len);
@ -216,7 +257,7 @@ impl State {
fn draw<T: Backend>( fn draw<T: Backend>(
&mut self, &mut self,
f: &mut Frame<'_, T>, f: &mut Frame<'_, T>,
results: &[History], results: &[Arc<HistoryWrapper>],
compact: bool, compact: bool,
show_preview: bool, show_preview: bool,
) { ) {
@ -284,7 +325,7 @@ impl State {
let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into()); let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into());
f.render_widget(preview, chunks[3]); f.render_widget(preview, chunks[3]);
let extra_width = UnicodeWidthStr::width(self.input.substring()); let extra_width = UnicodeWidthStr::width(self.search.input.substring());
let cursor_offset = if compact { 0 } else { 1 }; let cursor_offset = if compact { 0 } else { 1 };
f.set_cursor( f.set_cursor(
@ -332,7 +373,7 @@ impl State {
stats stats
} }
fn build_results_list(compact: bool, results: &[History]) -> HistoryList { fn build_results_list(compact: bool, results: &[Arc<HistoryWrapper>]) -> HistoryList {
let results_list = if compact { let results_list = if compact {
HistoryList::new(results) HistoryList::new(results)
} else { } else {
@ -348,8 +389,8 @@ impl State {
fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph { fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph {
let input = format!( let input = format!(
"[{:^14}] {}", "[{:^14}] {}",
self.filter_mode.as_str(), self.search.filter_mode.as_str(),
self.input.as_str(), self.search.input.as_str(),
); );
let input = if compact { let input = if compact {
Paragraph::new(input) Paragraph::new(input)
@ -366,7 +407,7 @@ impl State {
fn build_preview( fn build_preview(
&mut self, &mut self,
results: &[History], results: &[Arc<HistoryWrapper>],
compact: bool, compact: bool,
preview_width: u16, preview_width: u16,
chunk_width: usize, chunk_width: usize,
@ -438,6 +479,23 @@ impl Write for Stdout {
} }
} }
pub struct HistoryWrapper {
history: History,
pub count: i32,
}
impl Deref for HistoryWrapper {
type Target = History;
fn deref(&self) -> &Self::Target {
&self.history
}
}
impl SkimItem for HistoryWrapper {
fn text(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::Borrowed(self.history.command.as_str())
}
}
// this is a big blob of horrible! clean it up! // this is a big blob of horrible! clean it up!
// for now, it works. But it'd be great if it were more easily readable, and // for now, it works. But it'd be great if it were more easily readable, and
// modular. I'd like to add some more stats and stuff at some point // modular. I'd like to add some more stats and stuff at some point
@ -455,22 +513,28 @@ pub async fn history(
// Put the cursor at the end of the query by default // Put the cursor at the end of the query by default
input.end(); input.end();
let update_needed = settings.needs_update().fuse(); let settings2 = settings.clone();
let update_needed = tokio::spawn(async move { settings2.needs_update().await }).fuse();
tokio::pin!(update_needed); tokio::pin!(update_needed);
let context = current_context();
let mut app = State { let mut app = State {
history_count: db.history_count().await?, history_count: db.history_count().await?,
input,
results_state: ListState::default(), results_state: ListState::default(),
context: current_context(),
filter_mode: if settings.shell_up_key_binding {
settings
.filter_mode_shell_up_key_binding
.unwrap_or(settings.filter_mode)
} else {
settings.filter_mode
},
update_needed: None, update_needed: None,
search: SearchState {
input,
context,
filter_mode: if settings.shell_up_key_binding {
settings
.filter_mode_shell_up_key_binding
.unwrap_or(settings.filter_mode)
} else {
settings.filter_mode
},
},
all_history: Vec::new(),
}; };
let mut results = app.query_results(settings.search_mode, db).await?; let mut results = app.query_results(settings.search_mode, db).await?;
@ -485,8 +549,8 @@ pub async fn history(
}; };
terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?; terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?;
let initial_input = app.input.as_str().to_owned(); let initial_input = app.search.input.as_str().to_owned();
let initial_filter_mode = app.filter_mode; let initial_filter_mode = app.search.filter_mode;
let event_ready = tokio::task::spawn_blocking(|| event::poll(Duration::from_millis(250))); let event_ready = tokio::task::spawn_blocking(|| event::poll(Duration::from_millis(250)));
@ -504,23 +568,25 @@ pub async fn history(
} }
} }
update_needed = &mut update_needed => { update_needed = &mut update_needed => {
app.update_needed = update_needed; app.update_needed = update_needed?;
} }
} }
if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode { if initial_input != app.search.input.as_str()
|| initial_filter_mode != app.search.filter_mode
{
results = app.query_results(settings.search_mode, db).await?; results = app.query_results(settings.search_mode, db).await?;
} }
}; };
if index < results.len() { if index < results.len() {
// index is in bounds so we return that entry // index is in bounds so we return that entry
Ok(results.swap_remove(index).command) Ok(results.swap_remove(index).command.clone())
} else if index == RETURN_ORIGINAL { } else if index == RETURN_ORIGINAL {
Ok(String::new()) Ok(String::new())
} else { } else {
// Either: // Either:
// * index == RETURN_QUERY, in which case we should return the input // * index == RETURN_QUERY, in which case we should return the input
// * out of bounds -> usually implies no selected entry so we return the input // * out of bounds -> usually implies no selected entry so we return the input
Ok(app.input.into_inner()) Ok(app.search.input.into_inner())
} }
} }

View File

@ -0,0 +1,92 @@
use std::sync::Arc;
use atuin_client::settings::FilterMode;
use chrono::Utc;
use skim::{prelude::ExactOrFuzzyEngineFactory, MatchEngineFactory};
use tokio::task::yield_now;
use super::interactive::{HistoryWrapper, SearchState};
pub async fn fuzzy_search(
state: &SearchState,
all_history: &[Arc<HistoryWrapper>],
) -> Vec<Arc<HistoryWrapper>> {
let mut set = Vec::with_capacity(200);
let mut ranks = Vec::with_capacity(200);
let engine = ExactOrFuzzyEngineFactory::builder().fuzzy_algorithm(skim::FuzzyAlgorithm::SkimV2);
let query = state.input.as_str();
let engine = engine.create_engine(query);
let now = Utc::now();
for (i, item) in all_history.iter().enumerate() {
if i % 256 == 0 {
yield_now().await;
}
match state.filter_mode {
FilterMode::Global => {}
FilterMode::Host if item.hostname == state.context.hostname => {}
FilterMode::Session if item.session == state.context.session => {}
FilterMode::Directory if item.cwd == state.context.cwd => {}
_ => continue,
}
#[allow(clippy::cast_lossless, clippy::cast_precision_loss)]
if let Some(res) = engine.match_item(item.clone()) {
let [score, begin, _, _] = res.rank;
let mut duration = ((now - item.timestamp).num_seconds() as f64).log2();
if !duration.is_finite() || duration <= 1.0 {
duration = 1.0;
}
let count = (item.count as f64 + 16.0).log2();
let begin = (begin as f64 + 16.0).log2();
// reduce longer durations, raise higher counts, raise matches close to the start
let score = (score as f64) * count / duration / begin;
'insert: {
// algorithm:
// 1. find either the position that this command ranks
// 2. find the same command positioned better than our rank.
for i in 0..set.len() {
// do we out score the corrent position?
if ranks[i] > score {
ranks.insert(i, score);
set.insert(i, item.clone());
let mut j = i + 1;
while j < set.len() {
// remove duplicates that have a worse score
if set[j].command == item.command {
ranks.remove(j);
set.remove(j);
// break this while loop because there won't be any other
// duplicates.
break;
}
j += 1;
}
// keep it limited
if ranks.len() > 200 {
ranks.pop();
set.pop();
}
break 'insert;
}
// don't continue if this command has a better score already
if set[i].command == item.command {
break 'insert;
}
}
if set.len() < 200 {
ranks.push(score);
set.push(item.clone());
}
}
}
}
set
}