aboutsummaryrefslogtreecommitdiffstats
path: root/src/command/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/command/client')
-rw-r--r--src/command/client/history.rs298
-rw-r--r--src/command/client/import.rs152
-rw-r--r--src/command/client/search.rs189
-rw-r--r--src/command/client/search/cursor.rs333
-rw-r--r--src/command/client/search/duration.rs62
-rw-r--r--src/command/client/search/engines.rs46
-rw-r--r--src/command/client/search/engines/db.rs33
-rw-r--r--src/command/client/search/engines/skim.rs145
-rw-r--r--src/command/client/search/history_list.rs183
-rw-r--r--src/command/client/search/interactive.rs588
-rw-r--r--src/command/client/stats.rs181
-rw-r--r--src/command/client/sync.rs74
-rw-r--r--src/command/client/sync/login.rs147
-rw-r--r--src/command/client/sync/logout.rs19
-rw-r--r--src/command/client/sync/register.rs49
-rw-r--r--src/command/client/sync/status.rs35
16 files changed, 0 insertions, 2534 deletions
diff --git a/src/command/client/history.rs b/src/command/client/history.rs
deleted file mode 100644
index 76c796ef..00000000
--- a/src/command/client/history.rs
+++ /dev/null
@@ -1,298 +0,0 @@
-use std::{
- env,
- fmt::{self, Display},
- io::{StdoutLock, Write},
- time::Duration,
-};
-
-use atuin_common::utils;
-use clap::Subcommand;
-use eyre::Result;
-use runtime_format::{FormatKey, FormatKeyError, ParsedFmt};
-
-use atuin_client::{
- database::{current_context, Database},
- history::History,
- settings::Settings,
-};
-
-#[cfg(feature = "sync")]
-use atuin_client::sync;
-use log::debug;
-
-use super::search::format_duration;
-use super::search::format_duration_into;
-
-#[derive(Subcommand)]
-#[command(infer_subcommands = true)]
-pub enum Cmd {
- /// Begins a new command in the history
- Start { command: Vec<String> },
-
- /// Finishes a new command in the history (adds time, exit code)
- End {
- id: String,
- #[arg(long, short)]
- exit: i64,
- },
-
- /// List all items in history
- List {
- #[arg(long, short)]
- cwd: bool,
-
- #[arg(long, short)]
- session: bool,
-
- #[arg(long)]
- human: bool,
-
- /// Show only the text of the command
- #[arg(long)]
- cmd_only: bool,
-
- /// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
- /// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
- #[arg(long, short)]
- format: Option<String>,
- },
-
- /// Get the last command ran
- Last {
- #[arg(long)]
- human: bool,
-
- /// Show only the text of the command
- #[arg(long)]
- cmd_only: bool,
-
- /// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
- /// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
- #[arg(long, short)]
- format: Option<String>,
- },
-}
-
-#[derive(Clone, Copy, Debug)]
-pub enum ListMode {
- Human,
- CmdOnly,
- Regular,
-}
-
-impl ListMode {
- pub const fn from_flags(human: bool, cmd_only: bool) -> Self {
- if human {
- ListMode::Human
- } else if cmd_only {
- ListMode::CmdOnly
- } else {
- ListMode::Regular
- }
- }
-}
-
-#[allow(clippy::cast_sign_loss)]
-pub fn print_list(h: &[History], list_mode: ListMode, format: Option<&str>) {
- let w = std::io::stdout();
- let mut w = w.lock();
-
- match list_mode {
- ListMode::Human => print_human_list(&mut w, h, format),
- ListMode::CmdOnly => print_cmd_only(&mut w, h),
- ListMode::Regular => print_regular(&mut w, h, format),
- }
-
- w.flush().expect("failed to flush history");
-}
-
-/// type wrapper around `History` so we can implement traits
-struct FmtHistory<'a>(&'a History);
-
-/// defines how to format the history
-impl FormatKey for FmtHistory<'_> {
- #[allow(clippy::cast_sign_loss)]
- fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> {
- match key {
- "command" => f.write_str(self.0.command.trim())?,
- "directory" => f.write_str(self.0.cwd.trim())?,
- "exit" => f.write_str(&self.0.exit.to_string())?,
- "duration" => {
- let dur = Duration::from_nanos(std::cmp::max(self.0.duration, 0) as u64);
- format_duration_into(dur, f)?;
- }
- "time" => self.0.timestamp.format("%Y-%m-%d %H:%M:%S").fmt(f)?,
- "relativetime" => {
- let since = chrono::Utc::now() - self.0.timestamp;
- let time = format_duration(since.to_std().unwrap_or_default());
- f.write_str(&time)?;
- }
- "host" => f.write_str(
- self.0
- .hostname
- .split_once(':')
- .map_or(&self.0.hostname, |(host, _)| host),
- )?,
- "user" => f.write_str(self.0.hostname.split_once(':').map_or("", |(_, user)| user))?,
- _ => return Err(FormatKeyError::UnknownKey),
- }
- Ok(())
- }
-}
-
-fn print_list_with(w: &mut StdoutLock, h: &[History], format: &str) {
- let fmt = match ParsedFmt::new(format) {
- Ok(fmt) => fmt,
- Err(err) => {
- eprintln!("ERROR: History formatting failed with the following error: {err}");
- println!("If your formatting string contains curly braces (eg: {{var}}) you need to escape them this way: {{{{var}}.");
- std::process::exit(1)
- }
- };
-
- for h in h.iter().rev() {
- writeln!(w, "{}", fmt.with_args(&FmtHistory(h))).expect("failed to write history");
- }
-}
-
-pub fn print_human_list(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
- let format = format
- .unwrap_or("{time} · {duration}\t{command}")
- .replace("\\t", "\t");
- print_list_with(w, h, &format);
-}
-
-pub fn print_regular(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
- let format = format
- .unwrap_or("{time}\t{command}\t{duration}")
- .replace("\\t", "\t");
- print_list_with(w, h, &format);
-}
-
-pub fn print_cmd_only(w: &mut StdoutLock, h: &[History]) {
- for h in h.iter().rev() {
- writeln!(w, "{}", h.command.trim()).expect("failed to write history");
- }
-}
-
-impl Cmd {
- pub async fn run(&self, settings: &Settings, db: &mut impl Database) -> Result<()> {
- let context = current_context();
-
- match self {
- Self::Start { command: words } => {
- let command = words.join(" ");
-
- if command.starts_with(' ') || settings.history_filter.is_match(&command) {
- return Ok(());
- }
-
- // It's better for atuin to silently fail here and attempt to
- // store whatever is ran, than to throw an error to the terminal
- let cwd = utils::get_current_dir();
-
- let h = History::new(chrono::Utc::now(), command, cwd, -1, -1, None, None, None);
-
- // print the ID
- // we use this as the key for calling end
- println!("{}", h.id);
- db.save(&h).await?;
- Ok(())
- }
-
- Self::End { id, exit } => {
- if id.trim() == "" {
- return Ok(());
- }
-
- let mut h = db.load(id).await?;
-
- if h.duration > 0 {
- debug!("cannot end history - already has duration");
-
- // returning OK as this can occur if someone Ctrl-c a prompt
- return Ok(());
- }
-
- h.exit = *exit;
- h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos();
-
- db.update(&h).await?;
-
- if settings.should_sync()? {
- #[cfg(feature = "sync")]
- {
- debug!("running periodic background sync");
- sync::sync(settings, false, db).await?;
- }
- #[cfg(not(feature = "sync"))]
- debug!("not compiled with sync support");
- } else {
- debug!("sync disabled! not syncing");
- }
-
- Ok(())
- }
-
- Self::List {
- session,
- cwd,
- human,
- cmd_only,
- format,
- } => {
- let session = if *session {
- Some(env::var("ATUIN_SESSION")?)
- } else {
- None
- };
- let cwd = if *cwd {
- Some(utils::get_current_dir())
- } else {
- None
- };
-
- let history = match (session, cwd) {
- (None, None) => db.list(settings.filter_mode, &context, None, false).await?,
- (None, Some(cwd)) => {
- let query = format!("select * from history where cwd = '{cwd}';");
- db.query_history(&query).await?
- }
- (Some(session), None) => {
- let query = format!("select * from history where session = '{session}';");
- db.query_history(&query).await?
- }
- (Some(session), Some(cwd)) => {
- let query = format!(
- "select * from history where cwd = '{cwd}' and session = '{session}';",
- );
- db.query_history(&query).await?
- }
- };
-
- print_list(
- &history,
- ListMode::from_flags(*human, *cmd_only),
- format.as_deref(),
- );
-
- Ok(())
- }
-
- Self::Last {
- human,
- cmd_only,
- format,
- } => {
- let last = db.last().await?;
- print_list(
- &[last],
- ListMode::from_flags(*human, *cmd_only),
- format.as_deref(),
- );
-
- Ok(())
- }
- }
- }
-}
diff --git a/src/command/client/import.rs b/src/command/client/import.rs
deleted file mode 100644
index 7abc3d44..00000000
--- a/src/command/client/import.rs
+++ /dev/null
@@ -1,152 +0,0 @@
-use std::env;
-
-use async_trait::async_trait;
-use clap::Parser;
-use eyre::Result;
-use indicatif::ProgressBar;
-
-use atuin_client::{
- database::Database,
- history::History,
- import::{
- bash::Bash, fish::Fish, nu::Nu, nu_histdb::NuHistDb, resh::Resh, zsh::Zsh,
- zsh_histdb::ZshHistDb, Importer, Loader,
- },
-};
-
-#[derive(Parser)]
-#[command(infer_subcommands = true)]
-pub enum Cmd {
- /// Import history for the current shell
- Auto,
-
- /// Import history from the zsh history file
- Zsh,
- /// Import history from the zsh history file
- ZshHistDb,
- /// Import history from the bash history file
- Bash,
- /// Import history from the resh history file
- Resh,
- /// Import history from the fish history file
- Fish,
- /// Import history from the nu history file
- Nu,
- /// Import history from the nu history file
- NuHistDb,
-}
-
-const BATCH_SIZE: usize = 100;
-
-impl Cmd {
- pub async fn run<DB: Database>(&self, db: &mut DB) -> Result<()> {
- println!(" Atuin ");
- println!("======================");
- println!(" \u{1f30d} ");
- println!(" \u{1f418}\u{1f418}\u{1f418}\u{1f418} ");
- println!(" \u{1f422} ");
- println!("======================");
- println!("Importing history...");
-
- match self {
- Self::Auto => {
- if cfg!(windows) {
- println!("This feature does not work on windows. Please run atuin import <SHELL>. To view a list of shells, run atuin import.");
- return Ok(());
- }
-
- let shell = env::var("SHELL").unwrap_or_else(|_| String::from("NO_SHELL"));
- if shell.ends_with("/zsh") {
- if ZshHistDb::histpath().is_ok() {
- println!(
- "Detected Zsh-HistDb, using :{}",
- ZshHistDb::histpath().unwrap().to_str().unwrap()
- );
- import::<ZshHistDb, DB>(db).await
- } else {
- println!("Detected ZSH");
- import::<Zsh, DB>(db).await
- }
- } else if shell.ends_with("/fish") {
- println!("Detected Fish");
- import::<Fish, DB>(db).await
- } else if shell.ends_with("/bash") {
- println!("Detected Bash");
- import::<Bash, DB>(db).await
- } else if shell.ends_with("/nu") {
- if NuHistDb::histpath().is_ok() {
- println!(
- "Detected Nu-HistDb, using :{}",
- NuHistDb::histpath().unwrap().to_str().unwrap()
- );
- import::<NuHistDb, DB>(db).await
- } else {
- println!("Detected Nushell");
- import::<Nu, DB>(db).await
- }
- } else {
- println!("cannot import {shell} history");
- Ok(())
- }
- }
-
- Self::Zsh => import::<Zsh, DB>(db).await,
- Self::ZshHistDb => import::<ZshHistDb, DB>(db).await,
- Self::Bash => import::<Bash, DB>(db).await,
- Self::Resh => import::<Resh, DB>(db).await,
- Self::Fish => import::<Fish, DB>(db).await,
- Self::Nu => import::<Nu, DB>(db).await,
- Self::NuHistDb => import::<NuHistDb, DB>(db).await,
- }
- }
-}
-
-pub struct HistoryImporter<'db, DB: Database> {
- pb: ProgressBar,
- buf: Vec<History>,
- db: &'db mut DB,
-}
-
-impl<'db, DB: Database> HistoryImporter<'db, DB> {
- fn new(db: &'db mut DB, len: usize) -> Self {
- Self {
- pb: ProgressBar::new(len as u64),
- buf: Vec::with_capacity(BATCH_SIZE),
- db,
- }
- }
-
- async fn flush(self) -> Result<()> {
- if !self.buf.is_empty() {
- self.db.save_bulk(&self.buf).await?;
- }
- self.pb.finish();
- Ok(())
- }
-}
-
-#[async_trait]
-impl<'db, DB: Database> Loader for HistoryImporter<'db, DB> {
- async fn push(&mut self, hist: History) -> Result<()> {
- self.pb.inc(1);
- self.buf.push(hist);
- if self.buf.len() == self.buf.capacity() {
- self.db.save_bulk(&self.buf).await?;
- self.buf.clear();
- }
- Ok(())
- }
-}
-
-async fn import<I: Importer + Send, DB: Database>(db: &mut DB) -> Result<()> {
- println!("Importing history from {}", I::NAME);
-
- let mut importer = I::new().await?;
- let len = importer.entries().await.unwrap();
- let mut loader = HistoryImporter::new(db, len);
- importer.load(&mut loader).await?;
- loader.flush().await?;
-
- println!("Import complete!");
- Ok(())
-}
diff --git a/src/command/client/search.rs b/src/command/client/search.rs
deleted file mode 100644
index 356ae251..00000000
--- a/src/command/client/search.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-use atuin_common::utils;
-use clap::Parser;
-use eyre::Result;
-
-use atuin_client::{
- database::Database,
- database::{current_context, OptFilters},
- history::History,
- settings::{FilterMode, SearchMode, Settings},
-};
-
-use super::history::ListMode;
-
-mod cursor;
-mod duration;
-mod engines;
-mod history_list;
-mod interactive;
-pub use duration::{format_duration, format_duration_into};
-
-#[allow(clippy::struct_excessive_bools)]
-#[derive(Parser)]
-pub struct Cmd {
- /// Filter search result by directory
- #[arg(long, short)]
- cwd: Option<String>,
-
- /// Exclude directory from results
- #[arg(long = "exclude-cwd")]
- exclude_cwd: Option<String>,
-
- /// Filter search result by exit code
- #[arg(long, short)]
- exit: Option<i64>,
-
- /// Exclude results with this exit code
- #[arg(long = "exclude-exit")]
- exclude_exit: Option<i64>,
-
- /// Only include results added before this date
- #[arg(long, short)]
- before: Option<String>,
-
- /// Only include results after this date
- #[arg(long)]
- after: Option<String>,
-
- /// How many entries to return at most
- #[arg(long)]
- limit: Option<i64>,
-
- /// Offset from the start of the results
- #[arg(long)]
- offset: Option<i64>,
-
- /// Open interactive search UI
- #[arg(long, short)]
- interactive: bool,
-
- /// Allow overriding filter mode over config
- #[arg(long = "filter-mode")]
- filter_mode: Option<FilterMode>,
-
- /// Allow overriding search mode over config
- #[arg(long = "search-mode")]
- search_mode: Option<SearchMode>,
-
- /// Marker argument used to inform atuin that it was invoked from a shell up-key binding (hidden from help to avoid confusion)
- #[arg(long = "shell-up-key-binding", hide = true)]
- shell_up_key_binding: bool,
-
- /// Use human-readable formatting for time
- #[arg(long)]
- human: bool,
-
- query: Vec<String>,
-
- /// Show only the text of the command
- #[arg(long)]
- cmd_only: bool,
-
- /// Delete anything matching this query. Will not print out the match
- #[arg(long)]
- delete: bool,
-
- /// Reverse the order of results, oldest first
- #[arg(long, short)]
- reverse: bool,
-
- /// Available variables: {command}, {directory}, {duration}, {user}, {host}, {time}, {exit} and
- /// {relativetime}.
- /// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
- #[arg(long, short)]
- format: Option<String>,
-}
-
-impl Cmd {
- pub async fn run(self, mut db: impl Database, settings: &mut Settings) -> Result<()> {
- if self.search_mode.is_some() {
- settings.search_mode = self.search_mode.unwrap();
- }
- if self.filter_mode.is_some() {
- settings.filter_mode = self.filter_mode.unwrap();
- }
-
- settings.shell_up_key_binding = self.shell_up_key_binding;
-
- if self.interactive {
- let item = interactive::history(&self.query, settings, db).await?;
- eprintln!("{item}");
- } else {
- let list_mode = ListMode::from_flags(self.human, self.cmd_only);
-
- let opt_filter = OptFilters {
- exit: self.exit,
- exclude_exit: self.exclude_exit,
- cwd: self.cwd,
- exclude_cwd: self.exclude_cwd,
- before: self.before,
- after: self.after,
- limit: self.limit,
- offset: self.offset,
- reverse: self.reverse,
- };
-
- let mut entries =
- run_non_interactive(settings, opt_filter.clone(), &self.query, &mut db).await?;
-
- if entries.is_empty() {
- std::process::exit(1)
- }
-
- // if we aren't deleting, print it all
- if self.delete {
- // delete it
- // it only took me _years_ to add this
- // sorry
- while !entries.is_empty() {
- for entry in &entries {
- eprintln!("deleting {}", entry.id);
- db.delete(entry.clone()).await?;
- }
-
- entries =
- run_non_interactive(settings, opt_filter.clone(), &self.query, &mut db)
- .await?;
- }
- } else {
- super::history::print_list(&entries, list_mode, self.format.as_deref());
- }
- };
- Ok(())
- }
-}
-
-// This is supposed to more-or-less mirror the command line version, so ofc
-// it is going to have a lot of args
-#[allow(clippy::too_many_arguments)]
-async fn run_non_interactive(
- settings: &Settings,
- filter_options: OptFilters,
- query: &[String],
- db: &mut impl Database,
-) -> Result<Vec<History>> {
- let dir = if filter_options.cwd.as_deref() == Some(".") {
- Some(utils::get_current_dir())
- } else {
- filter_options.cwd
- };
-
- let context = current_context();
-
- let opt_filter = OptFilters {
- cwd: dir,
- ..filter_options
- };
-
- let results = db
- .search(
- settings.search_mode,
- settings.filter_mode,
- &context,
- query.join(" ").as_str(),
- opt_filter,
- )
- .await?;
-
- Ok(results)
-}
diff --git a/src/command/client/search/cursor.rs b/src/command/client/search/cursor.rs
deleted file mode 100644
index 2bce4f37..00000000
--- a/src/command/client/search/cursor.rs
+++ /dev/null
@@ -1,333 +0,0 @@
-use atuin_client::settings::WordJumpMode;
-
-pub struct Cursor {
- source: String,
- index: usize,
-}
-
-impl From<String> for Cursor {
- fn from(source: String) -> Self {
- Self { source, index: 0 }
- }
-}
-
-pub struct WordJumper<'a> {
- word_chars: &'a str,
- word_jump_mode: WordJumpMode,
-}
-
-impl WordJumper<'_> {
- fn is_word_boundary(&self, c: char, next_c: char) -> bool {
- (c.is_whitespace() && !next_c.is_whitespace())
- || (!c.is_whitespace() && next_c.is_whitespace())
- || (self.word_chars.contains(c) && !self.word_chars.contains(next_c))
- || (!self.word_chars.contains(c) && self.word_chars.contains(next_c))
- }
-
- fn emacs_get_next_word_pos(&self, source: &str, index: usize) -> usize {
- let index = (index + 1..source.len().saturating_sub(1))
- .find(|&i| self.word_chars.contains(source.chars().nth(i).unwrap()))
- .unwrap_or(source.len());
- (index + 1..source.len().saturating_sub(1))
- .find(|&i| !self.word_chars.contains(source.chars().nth(i).unwrap()))
- .unwrap_or(source.len())
- }
-
- fn emacs_get_prev_word_pos(&self, source: &str, index: usize) -> usize {
- let index = (1..index)
- .rev()
- .find(|&i| self.word_chars.contains(source.chars().nth(i).unwrap()))
- .unwrap_or(0);
- (1..index)
- .rev()
- .find(|&i| !self.word_chars.contains(source.chars().nth(i).unwrap()))
- .map_or(0, |i| i + 1)
- }
-
- fn subl_get_next_word_pos(&self, source: &str, index: usize) -> usize {
- let index = (index..source.len().saturating_sub(1)).find(|&i| {
- self.is_word_boundary(
- source.chars().nth(i).unwrap(),
- source.chars().nth(i + 1).unwrap(),
- )
- });
- if index.is_none() {
- return source.len();
- }
- (index.unwrap() + 1..source.len())
- .find(|&i| !source.chars().nth(i).unwrap().is_whitespace())
- .unwrap_or(source.len())
- }
-
- fn subl_get_prev_word_pos(&self, source: &str, index: usize) -> usize {
- let index = (1..index)
- .rev()
- .find(|&i| !source.chars().nth(i).unwrap().is_whitespace());
- if index.is_none() {
- return 0;
- }
- (1..index.unwrap())
- .rev()
- .find(|&i| {
- self.is_word_boundary(
- source.chars().nth(i - 1).unwrap(),
- source.chars().nth(i).unwrap(),
- )
- })
- .unwrap_or(0)
- }
-
- fn get_next_word_pos(&self, source: &str, index: usize) -> usize {
- match self.word_jump_mode {
- WordJumpMode::Emacs => self.emacs_get_next_word_pos(source, index),
- WordJumpMode::Subl => self.subl_get_next_word_pos(source, index),
- }
- }
-
- fn get_prev_word_pos(&self, source: &str, index: usize) -> usize {
- match self.word_jump_mode {
- WordJumpMode::Emacs => self.emacs_get_prev_word_pos(source, index),
- WordJumpMode::Subl => self.subl_get_prev_word_pos(source, index),
- }
- }
-}
-
-impl Cursor {
- pub fn as_str(&self) -> &str {
- self.source.as_str()
- }
-
- pub fn into_inner(self) -> String {
- self.source
- }
-
- /// Returns the string before the cursor
- pub fn substring(&self) -> &str {
- &self.source[..self.index]
- }
-
- /// Returns the currently selected [`char`]
- pub fn char(&self) -> Option<char> {
- self.source[self.index..].chars().next()
- }
-
- pub fn right(&mut self) {
- if self.index < self.source.len() {
- loop {
- self.index += 1;
- if self.source.is_char_boundary(self.index) {
- break;
- }
- }
- }
- }
-
- pub fn left(&mut self) -> bool {
- if self.index > 0 {
- loop {
- self.index -= 1;
- if self.source.is_char_boundary(self.index) {
- break true;
- }
- }
- } else {
- false
- }
- }
-
- pub fn next_word(&mut self, word_chars: &str, word_jump_mode: WordJumpMode) {
- let word_jumper = WordJumper {
- word_chars,
- word_jump_mode,
- };
- self.index = word_jumper.get_next_word_pos(&self.source, self.index);
- }
-
- pub fn prev_word(&mut self, word_chars: &str, word_jump_mode: WordJumpMode) {
- let word_jumper = WordJumper {
- word_chars,
- word_jump_mode,
- };
- self.index = word_jumper.get_prev_word_pos(&self.source, self.index);
- }
-
- pub fn insert(&mut self, c: char) {
- self.source.insert(self.index, c);
- self.index += c.len_utf8();
- }
-
- pub fn remove(&mut self) -> Option<char> {
- if self.index < self.source.len() {
- Some(self.source.remove(self.index))
- } else {
- None
- }
- }
-
- pub fn remove_next_word(&mut self, word_chars: &str, word_jump_mode: WordJumpMode) {
- let word_jumper = WordJumper {
- word_chars,
- word_jump_mode,
- };
- let next_index = word_jumper.get_next_word_pos(&self.source, self.index);
- self.source.replace_range(self.index..next_index, "");
- }
-
- pub fn remove_prev_word(&mut self, word_chars: &str, word_jump_mode: WordJumpMode) {
- let word_jumper = WordJumper {
- word_chars,
- word_jump_mode,
- };
- let next_index = word_jumper.get_prev_word_pos(&self.source, self.index);
- self.source.replace_range(next_index..self.index, "");
- self.index = next_index;
- }
-
- pub fn back(&mut self) -> Option<char> {
- if self.left() {
- self.remove()
- } else {
- None
- }
- }
-
- pub fn clear(&mut self) {
- self.source.clear();
- self.index = 0;
- }
-
- pub fn end(&mut self) {
- self.index = self.source.len();
- }
-
- pub fn start(&mut self) {
- self.index = 0;
- }
-}
-
-#[cfg(test)]
-mod cursor_tests {
- use super::Cursor;
- use super::*;
-
- static EMACS_WORD_JUMPER: WordJumper = WordJumper {
- word_chars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
- word_jump_mode: WordJumpMode::Emacs,
- };
-
- static SUBL_WORD_JUMPER: WordJumper = WordJumper {
- word_chars: "./\\()\"'-:,.;<>~!@#$%^&*|+=[]{}`~?",
- word_jump_mode: WordJumpMode::Subl,
- };
-
- #[test]
- fn right() {
- // ö is 2 bytes
- let mut c = Cursor::from(String::from("öaöböcödöeöfö"));
- let indices = [0, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18, 20, 20, 20, 20];
- for i in indices {
- assert_eq!(c.index, i);
- c.right();
- }
- }
-
- #[test]
- fn left() {
- // ö is 2 bytes
- let mut c = Cursor::from(String::from("öaöböcödöeöfö"));
- c.end();
- let indices = [20, 18, 17, 15, 14, 12, 11, 9, 8, 6, 5, 3, 2, 0, 0, 0, 0];
- for i in indices {
- assert_eq!(c.index, i);
- c.left();
- }
- }
-
- #[test]
- fn test_emacs_get_next_word_pos() {
- let s = String::from(" aaa ((()))bbb ((())) ");
- let indices = [(0, 6), (3, 6), (7, 18), (19, 30)];
- for (i_src, i_dest) in indices {
- assert_eq!(EMACS_WORD_JUMPER.get_next_word_pos(&s, i_src), i_dest);
- }
- assert_eq!(EMACS_WORD_JUMPER.get_next_word_pos("", 0), 0);
- }
-
- #[test]
- fn test_emacs_get_prev_word_pos() {
- let s = String::from(" aaa ((()))bbb ((())) ");
- let indices = [(30, 15), (29, 15), (15, 3), (3, 0)];
- for (i_src, i_dest) in indices {
- assert_eq!(EMACS_WORD_JUMPER.get_prev_word_pos(&s, i_src), i_dest);
- }
- assert_eq!(EMACS_WORD_JUMPER.get_prev_word_pos("", 0), 0);
- }
-
- #[test]
- fn test_subl_get_next_word_pos() {
- let s = String::from(" aaa ((()))bbb ((())) ");
- let indices = [(0, 3), (1, 3), (3, 9), (9, 15), (15, 21), (21, 30)];
- for (i_src, i_dest) in indices {
- assert_eq!(SUBL_WORD_JUMPER.get_next_word_pos(&s, i_src), i_dest);
- }
- assert_eq!(SUBL_WORD_JUMPER.get_next_word_pos("", 0), 0);
- }
-
- #[test]
- fn test_subl_get_prev_word_pos() {
- let s = String::from(" aaa ((()))bbb ((())) ");
- let indices = [(30, 21), (21, 15), (15, 9), (9, 3), (3, 0)];
- for (i_src, i_dest) in indices {
- assert_eq!(SUBL_WORD_JUMPER.get_prev_word_pos(&s, i_src), i_dest);
- }
- assert_eq!(SUBL_WORD_JUMPER.get_prev_word_pos("", 0), 0);
- }
-
- #[test]
- fn pop() {
- let mut s = String::from("öaöböcödöeöfö");
- let mut c = Cursor::from(s.clone());
- c.end();
- while !s.is_empty() {
- let c1 = s.pop();
- let c2 = c.back();
- assert_eq!(c1, c2);
- assert_eq!(s.as_str(), c.substring());
- }
- let c1 = s.pop();
- let c2 = c.back();
- assert_eq!(c1, c2);
- }
-
- #[test]
- fn back() {
- let mut c = Cursor::from(String::from("öaöböcödöeöfö"));
- // move to ^
- for _ in 0..4 {
- c.right();
- }
- assert_eq!(c.substring(), "öaöb");
- assert_eq!(c.back(), Some('b'));
- assert_eq!(c.back(), Some('ö'));
- assert_eq!(c.back(), Some('a'));
- assert_eq!(c.back(), Some('ö'));
- assert_eq!(c.back(), None);
- assert_eq!(c.as_str(), "öcödöeöfö");
- }
-
- #[test]
- fn insert() {
- let mut c = Cursor::from(String::from("öaöböcödöeöfö"));
- // move to ^
- for _ in 0..4 {
- c.right();
- }
- assert_eq!(c.substring(), "öaöb");
- c.insert('ö');
- c.insert('g');
- c.insert('ö');
- c.insert('h');
- assert_eq!(c.substring(), "öaöbögöh");
- assert_eq!(c.as_str(), "öaöbögöhöcödöeöfö");
- }
-}
diff --git a/src/command/client/search/duration.rs b/src/command/client/search/duration.rs
deleted file mode 100644
index 08dadb95..00000000
--- a/src/command/client/search/duration.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use core::fmt;
-use std::{ops::ControlFlow, time::Duration};
-
-#[allow(clippy::module_name_repetitions)]
-pub fn format_duration_into(dur: Duration, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
- if value > 0 {
- ControlFlow::Break((unit, value))
- } else {
- ControlFlow::Continue(())
- }
- }
-
- // impl taken and modified from
- // https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331
- // Copyright (c) 2016 The humantime Developers
- fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
- let secs = f.as_secs();
- let nanos = f.subsec_nanos();
-
- let years = secs / 31_557_600; // 365.25d
- let year_days = secs % 31_557_600;
- let months = year_days / 2_630_016; // 30.44d
- let month_days = year_days % 2_630_016;
- let days = month_days / 86400;
- let day_secs = month_days % 86400;
- let hours = day_secs / 3600;
- let minutes = day_secs % 3600 / 60;
- let seconds = day_secs % 60;
-
- let millis = nanos / 1_000_000;
-
- // a difference from our impl than the original is that
- // we only care about the most-significant segment of the duration.
- // If the item call returns `Break`, then the `?` will early-return.
- // This allows for a very consise impl
- item("y", years)?;
- item("mo", months)?;
- item("d", days)?;
- item("h", hours)?;
- item("m", minutes)?;
- item("s", seconds)?;
- item("ms", u64::from(millis))?;
- ControlFlow::Continue(())
- }
-
- match fmt(dur) {
- ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
- ControlFlow::Continue(()) => write!(f, "0s"),
- }
-}
-
-#[allow(clippy::module_name_repetitions)]
-pub fn format_duration(f: Duration) -> String {
- struct F(Duration);
- impl fmt::Display for F {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- format_duration_into(self.0, f)
- }
- }
- F(f).to_string()
-}
diff --git a/src/command/client/search/engines.rs b/src/command/client/search/engines.rs
deleted file mode 100644
index 878b1431..00000000
--- a/src/command/client/search/engines.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use async_trait::async_trait;
-use atuin_client::{
- database::{Context, Database},
- history::History,
- settings::{FilterMode, SearchMode},
-};
-use eyre::Result;
-
-use super::cursor::Cursor;
-
-pub mod db;
-pub mod skim;
-
-pub fn engine(search_mode: SearchMode) -> Box<dyn SearchEngine> {
- match search_mode {
- SearchMode::Skim => Box::new(skim::Search::new()) as Box<_>,
- mode => Box::new(db::Search(mode)) as Box<_>,
- }
-}
-
-pub struct SearchState {
- pub input: Cursor,
- pub filter_mode: FilterMode,
- pub context: Context,
-}
-
-#[async_trait]
-pub trait SearchEngine: Send + Sync + 'static {
- async fn full_query(
- &mut self,
- state: &SearchState,
- db: &mut dyn Database,
- ) -> Result<Vec<History>>;
-
- async fn query(&mut self, state: &SearchState, db: &mut dyn Database) -> Result<Vec<History>> {
- if state.input.as_str().is_empty() {
- Ok(db
- .list(state.filter_mode, &state.context, Some(200), true)
- .await?
- .into_iter()
- .collect::<Vec<_>>())
- } else {
- self.full_query(state, db).await
- }
- }
-}
diff --git a/src/command/client/search/engines/db.rs b/src/command/client/search/engines/db.rs
deleted file mode 100644
index b4f24561..00000000
--- a/src/command/client/search/engines/db.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use async_trait::async_trait;
-use atuin_client::{
- database::Database, database::OptFilters, history::History, settings::SearchMode,
-};
-use eyre::Result;
-
-use super::{SearchEngine, SearchState};
-
-pub struct Search(pub SearchMode);
-
-#[async_trait]
-impl SearchEngine for Search {
- async fn full_query(
- &mut self,
- state: &SearchState,
- db: &mut dyn Database,
- ) -> Result<Vec<History>> {
- Ok(db
- .search(
- self.0,
- state.filter_mode,
- &state.context,
- state.input.as_str(),
- OptFilters {
- limit: Some(200),
- ..Default::default()
- },
- )
- .await?
- .into_iter()
- .collect::<Vec<_>>())
- }
-}
diff --git a/src/command/client/search/engines/skim.rs b/src/command/client/search/engines/skim.rs
deleted file mode 100644
index 76049312..00000000
--- a/src/command/client/search/engines/skim.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-use std::path::Path;
-
-use async_trait::async_trait;
-use atuin_client::{database::Database, history::History, settings::FilterMode};
-use chrono::Utc;
-use eyre::Result;
-use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
-use tokio::task::yield_now;
-
-use super::{SearchEngine, SearchState};
-
-pub struct Search {
- all_history: Vec<(History, i32)>,
- engine: SkimMatcherV2,
-}
-
-impl Search {
- pub fn new() -> Self {
- Search {
- all_history: vec![],
- engine: SkimMatcherV2::default(),
- }
- }
-}
-
-#[async_trait]
-impl SearchEngine for Search {
- async fn full_query(
- &mut self,
- state: &SearchState,
- db: &mut dyn Database,
- ) -> Result<Vec<History>> {
- if self.all_history.is_empty() {
- self.all_history = db.all_with_count().await.unwrap();
- }
-
- Ok(fuzzy_search(&self.engine, state, &self.all_history).await)
- }
-}
-
-async fn fuzzy_search(
- engine: &SkimMatcherV2,
- state: &SearchState,
- all_history: &[(History, i32)],
-) -> Vec<History> {
- let mut set = Vec::with_capacity(200);
- let mut ranks = Vec::with_capacity(200);
- let query = state.input.as_str();
- let now = Utc::now();
-
- for (i, (history, count)) in all_history.iter().enumerate() {
- if i % 256 == 0 {
- yield_now().await;
- }
- match state.filter_mode {
- FilterMode::Global => {}
- FilterMode::Host if history.hostname == state.context.hostname => {}
- FilterMode::Session if history.session == state.context.session => {}
- FilterMode::Directory if history.cwd == state.context.cwd => {}
- _ => continue,
- }
- #[allow(clippy::cast_lossless, clippy::cast_precision_loss)]
- if let Some((score, indices)) = engine.fuzzy_indices(&history.command, query) {
- let begin = indices.first().copied().unwrap_or_default();
-
- let mut duration = ((now - history.timestamp).num_seconds() as f64).log2();
- if !duration.is_finite() || duration <= 1.0 {
- duration = 1.0;
- }
- // these + X.0 just make the log result a bit smoother.
- // log is very spiky towards 1-4, but I want a gradual decay.
- // eg:
- // log2(4) = 2, log2(5) = 2.3 (16% increase)
- // log2(8) = 3, log2(9) = 3.16 (5% increase)
- // log2(16) = 4, log2(17) = 4.08 (2% increase)
- let count = (*count as f64 + 8.0).log2();
- let begin = (begin as f64 + 16.0).log2();
- let path = path_dist(history.cwd.as_ref(), state.context.cwd.as_ref());
- let path = (path as f64 + 8.0).log2();
-
- // reduce longer durations, raise higher counts, raise matches close to the start
- let score = (-score as f64) * count / path / 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, history.clone());
- let mut j = i + 1;
- while j < set.len() {
- // remove duplicates that have a worse score
- if set[j].command == history.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 == history.command {
- break 'insert;
- }
- }
-
- if set.len() < 200 {
- ranks.push(score);
- set.push(history.clone());
- }
- }
- }
- }
-
- set
-}
-
-fn path_dist(a: &Path, b: &Path) -> usize {
- let mut a: Vec<_> = a.components().collect();
- let b: Vec<_> = b.components().collect();
-
- let mut dist = 0;
-
- // pop a until there's a common anscestor
- while !b.starts_with(&a) {
- dist += 1;
- a.pop();
- }
-
- b.len() - a.len() + dist
-}
diff --git a/src/command/client/search/history_list.rs b/src/command/client/search/history_list.rs
deleted file mode 100644
index eedab1a5..00000000
--- a/src/command/client/search/history_list.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-use std::time::Duration;
-
-use crate::ratatui::{
- buffer::Buffer,
- layout::Rect,
- style::{Color, Modifier, Style},
- widgets::{Block, StatefulWidget, Widget},
-};
-use atuin_client::history::History;
-
-use super::format_duration;
-
-pub struct HistoryList<'a> {
- history: &'a [History],
- block: Option<Block<'a>>,
-}
-
-#[derive(Default)]
-pub struct ListState {
- offset: usize,
- selected: usize,
- max_entries: usize,
-}
-
-impl ListState {
- pub fn selected(&self) -> usize {
- self.selected
- }
-
- pub fn max_entries(&self) -> usize {
- self.max_entries
- }
-
- pub fn select(&mut self, index: usize) {
- self.selected = index;
- }
-}
-
-impl<'a> StatefulWidget for HistoryList<'a> {
- type State = ListState;
-
- fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
- let list_area = self.block.take().map_or(area, |b| {
- let inner_area = b.inner(area);
- b.render(area, buf);
- inner_area
- });
-
- if list_area.width < 1 || list_area.height < 1 || self.history.is_empty() {
- return;
- }
- let list_height = list_area.height as usize;
-
- let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
- state.offset = start;
- state.max_entries = end - start;
-
- let mut s = DrawState {
- buf,
- list_area,
- x: 0,
- y: 0,
- state,
- };
-
- for item in self.history.iter().skip(state.offset).take(end - start) {
- s.index();
- s.duration(item);
- s.time(item);
- s.command(item);
-
- // reset line
- s.y += 1;
- s.x = 0;
- }
- }
-}
-
-impl<'a> HistoryList<'a> {
- pub fn new(history: &'a [History]) -> Self {
- Self {
- history,
- block: None,
- }
- }
-
- pub fn block(mut self, block: Block<'a>) -> Self {
- self.block = Some(block);
- self
- }
-
- fn get_items_bounds(&self, selected: usize, offset: usize, height: usize) -> (usize, usize) {
- let offset = offset.min(self.history.len().saturating_sub(1));
-
- let max_scroll_space = height.min(10);
- if offset + height < selected + max_scroll_space {
- let end = selected + max_scroll_space;
- (end - height, end)
- } else if selected < offset {
- (selected, selected + height)
- } else {
- (offset, offset + height)
- }
- }
-}
-
-struct DrawState<'a> {
- buf: &'a mut Buffer,
- list_area: Rect,
- x: u16,
- y: u16,
- state: &'a ListState,
-}
-
-// longest line prefix I could come up with
-#[allow(clippy::cast_possible_truncation)] // we know that this is <65536 length
-pub const PREFIX_LENGTH: u16 = " > 123ms 59s ago".len() as u16;
-
-impl DrawState<'_> {
- fn index(&mut self) {
- // these encode the slices of `" > "`, `" {n} "`, or `" "` in a compact form.
- // Yes, this is a hack, but it makes me feel happy
- static SLICES: &str = " > 1 2 3 4 5 6 7 8 9 ";
-
- let i = self.y as usize + self.state.offset;
- let i = i.checked_sub(self.state.selected);
- let i = i.unwrap_or(10).min(10) * 2;
- self.draw(&SLICES[i..i + 3], Style::default());
- }
-
- fn duration(&mut self, h: &History) {
- let status = Style::default().fg(if h.success() {
- Color::Green
- } else {
- Color::Red
- });
- let duration = Duration::from_nanos(u64::try_from(h.duration).unwrap_or(0));
- self.draw(&format_duration(duration), status);
- }
-
- #[allow(clippy::cast_possible_truncation)] // we know that time.len() will be <6
- fn time(&mut self, h: &History) {
- let style = Style::default().fg(Color::Blue);
-
- // Account for the chance that h.timestamp is "in the future"
- // This would mean that "since" is negative, and the unwrap here
- // would fail.
- // If the timestamp would otherwise be in the future, display
- // the time since as 0.
- let since = chrono::Utc::now() - h.timestamp;
- let time = format_duration(since.to_std().unwrap_or_default());
-
- // pad the time a little bit before we write. this aligns things nicely
- self.x = PREFIX_LENGTH - 4 - time.len() as u16;
-
- self.draw(&time, style);
- self.draw(" ago", style);
- }
-
- fn command(&mut self, h: &History) {
- let mut style = Style::default();
- if self.y as usize + self.state.offset == self.state.selected {
- style = style.fg(Color::Red).add_modifier(Modifier::BOLD);
- }
-
- for section in h.command.split_ascii_whitespace() {
- self.x += 1;
- if self.x > self.list_area.width {
- // Avoid attempting to draw a command section beyond the width
- // of the list
- return;
- }
- self.draw(section, style);
- }
- }
-
- fn draw(&mut self, s: &str, style: Style) {
- let cx = self.list_area.left() + self.x;
- let cy = self.list_area.bottom() - self.y - 1;
- let w = (self.list_area.width - self.x) as usize;
- self.x += self.buf.set_stringn(cx, cy, s, w, style).0 - cx;
- }
-}
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
deleted file mode 100644
index 300bc791..00000000
--- a/src/command/client/search/interactive.rs
+++ /dev/null
@@ -1,588 +0,0 @@
-use std::{
- io::{stdout, Write},
- time::Duration,
-};
-
-use crossterm::{
- event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
- execute, terminal,
-};
-use eyre::Result;
-use futures_util::FutureExt;
-use semver::Version;
-use unicode_width::UnicodeWidthStr;
-
-use atuin_client::{
- database::{current_context, Database},
- history::History,
- settings::{ExitMode, FilterMode, SearchMode, Settings},
-};
-
-use super::{
- cursor::Cursor,
- engines::{SearchEngine, SearchState},
- history_list::{HistoryList, ListState, PREFIX_LENGTH},
-};
-use crate::ratatui::{
- backend::{Backend, CrosstermBackend},
- layout::{Alignment, Constraint, Direction, Layout},
- style::{Color, Modifier, Style},
- text::{Span, Spans, Text},
- widgets::{Block, BorderType, Borders, Paragraph},
- Frame, Terminal, TerminalOptions, Viewport,
-};
-use crate::{command::client::search::engines, VERSION};
-
-const RETURN_ORIGINAL: usize = usize::MAX;
-const RETURN_QUERY: usize = usize::MAX - 1;
-
-struct State {
- history_count: i64,
- update_needed: Option<Version>,
- results_state: ListState,
- switched_search_mode: bool,
- search_mode: SearchMode,
-
- search: SearchState,
- engine: Box<dyn SearchEngine>,
-}
-
-impl State {
- async fn query_results(&mut self, db: &mut dyn Database) -> Result<Vec<History>> {
- let results = self.engine.query(&self.search, db).await?;
- self.results_state.select(0);
- Ok(results)
- }
-
- fn handle_input(&mut self, settings: &Settings, input: &Event, len: usize) -> Option<usize> {
- match input {
- Event::Key(k) => self.handle_key_input(settings, k, len),
- Event::Mouse(m) => self.handle_mouse_input(*m, len),
- Event::Paste(d) => self.handle_paste_input(d),
- _ => None,
- }
- }
-
- fn handle_mouse_input(&mut self, input: MouseEvent, len: usize) -> Option<usize> {
- match input.kind {
- event::MouseEventKind::ScrollDown => {
- let i = self.results_state.selected().saturating_sub(1);
- self.results_state.select(i);
- }
- event::MouseEventKind::ScrollUp => {
- let i = self.results_state.selected() + 1;
- self.results_state.select(i.min(len - 1));
- }
- _ => {}
- }
- None
- }
-
- fn handle_paste_input(&mut self, input: &str) -> Option<usize> {
- for i in input.chars() {
- self.search.input.insert(i);
- }
- None
- }
-
- #[allow(clippy::too_many_lines)]
- #[allow(clippy::cognitive_complexity)]
- fn handle_key_input(
- &mut self,
- settings: &Settings,
- input: &KeyEvent,
- len: usize,
- ) -> Option<usize> {
- if input.kind == event::KeyEventKind::Release {
- return None;
- }
-
- let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
- let alt = input.modifiers.contains(KeyModifiers::ALT);
- // reset the state, will be set to true later if user really did change it
- self.switched_search_mode = false;
- match input.code {
- KeyCode::Char('c' | 'd' | 'g') if ctrl => return Some(RETURN_ORIGINAL),
- KeyCode::Esc => {
- return Some(match settings.exit_mode {
- ExitMode::ReturnOriginal => RETURN_ORIGINAL,
- ExitMode::ReturnQuery => RETURN_QUERY,
- })
- }
- KeyCode::Enter => {
- return Some(self.results_state.selected());
- }
- KeyCode::Char(c @ '1'..='9') if alt => {
- let c = c.to_digit(10)? as usize;
- return Some(self.results_state.selected() + c);
- }
- KeyCode::Left if ctrl => self
- .search
- .input
- .prev_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Char('b') if alt => self
- .search
- .input
- .prev_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Left => {
- self.search.input.left();
- }
- KeyCode::Char('h') if ctrl => {
- self.search.input.left();
- }
- KeyCode::Char('b') if ctrl => {
- self.search.input.left();
- }
- KeyCode::Right if ctrl => self
- .search
- .input
- .next_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Char('f') if alt => self
- .search
- .input
- .next_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Right => self.search.input.right(),
- KeyCode::Char('l') if ctrl => self.search.input.right(),
- KeyCode::Char('f') if ctrl => self.search.input.right(),
- KeyCode::Char('a') if ctrl => self.search.input.start(),
- KeyCode::Home => self.search.input.start(),
- KeyCode::Char('e') if ctrl => self.search.input.end(),
- KeyCode::End => self.search.input.end(),
- KeyCode::Backspace if ctrl => self
- .search
- .input
- .remove_prev_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Backspace => {
- self.search.input.back();
- }
- KeyCode::Delete if ctrl => self
- .search
- .input
- .remove_next_word(&settings.word_chars, settings.word_jump_mode),
- KeyCode::Delete => {
- self.search.input.remove();
- }
- KeyCode::Char('w') if ctrl => {
- // remove the first batch of whitespace
- while matches!(self.search.input.back(), Some(c) if c.is_whitespace()) {}
- while self.search.input.left() {
- if self.search.input.char().unwrap().is_whitespace() {
- self.search.input.right(); // found whitespace, go back right
- break;
- }
- self.search.input.remove();
- }
- }
- KeyCode::Char('u') if ctrl => self.search.input.clear(),
- KeyCode::Char('r') if ctrl => {
- pub static FILTER_MODES: [FilterMode; 4] = [
- FilterMode::Global,
- FilterMode::Host,
- FilterMode::Session,
- FilterMode::Directory,
- ];
- let i = self.search.filter_mode as usize;
- let i = (i + 1) % FILTER_MODES.len();
- self.search.filter_mode = FILTER_MODES[i];
- }
- KeyCode::Char('s') if ctrl => {
- self.switched_search_mode = true;
- self.search_mode = self.search_mode.next(settings);
- self.engine = engines::engine(self.search_mode);
- }
- KeyCode::Down if self.results_state.selected() == 0 => {
- return Some(match settings.exit_mode {
- ExitMode::ReturnOriginal => RETURN_ORIGINAL,
- ExitMode::ReturnQuery => RETURN_QUERY,
- })
- }
- KeyCode::Down => {
- let i = self.results_state.selected().saturating_sub(1);
- self.results_state.select(i);
- }
- KeyCode::Char('n' | 'j') if ctrl => {
- let i = self.results_state.selected().saturating_sub(1);
- self.results_state.select(i);
- }
- KeyCode::Up => {
- let i = self.results_state.selected() + 1;
- self.results_state.select(i.min(len - 1));
- }
- KeyCode::Char('p' | 'k') if ctrl => {
- let i = self.results_state.selected() + 1;
- self.results_state.select(i.min(len - 1));
- }
- KeyCode::Char(c) => self.search.input.insert(c),
- KeyCode::PageDown => {
- let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
- let i = self.results_state.selected().saturating_sub(scroll_len);
- self.results_state.select(i);
- }
- KeyCode::PageUp => {
- let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
- let i = self.results_state.selected() + scroll_len;
- self.results_state.select(i.min(len - 1));
- }
- _ => {}
- };
-
- None
- }
-
- #[allow(clippy::cast_possible_truncation)]
- #[allow(clippy::bool_to_int_with_if)]
- fn draw<T: Backend>(
- &mut self,
- f: &mut Frame<'_, T>,
- results: &[History],
- compact: bool,
- show_preview: bool,
- ) {
- let border_size = if compact { 0 } else { 1 };
- let preview_width = f.size().width - 2;
- let preview_height = if show_preview {
- let longest_command = results
- .iter()
- .max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
- longest_command.map_or(0, |v| {
- std::cmp::min(
- 4,
- (v.command.len() as u16 + preview_width - 1 - border_size)
- / (preview_width - border_size),
- )
- }) + border_size * 2
- } else if compact {
- 0
- } else {
- 1
- };
- let show_help = !compact || f.size().height > 1;
- let chunks = Layout::default()
- .direction(Direction::Vertical)
- .margin(0)
- .horizontal_margin(1)
- .constraints(
- [
- Constraint::Length(if show_help { 1 } else { 0 }),
- Constraint::Min(1),
- Constraint::Length(1 + border_size),
- Constraint::Length(preview_height),
- ]
- .as_ref(),
- )
- .split(f.size());
-
- let header_chunks = Layout::default()
- .direction(Direction::Horizontal)
- .constraints(
- [
- Constraint::Ratio(1, 3),
- Constraint::Ratio(1, 3),
- Constraint::Ratio(1, 3),
- ]
- .as_ref(),
- )
- .split(chunks[0]);
-
- let title = self.build_title();
- f.render_widget(title, header_chunks[0]);
-
- let help = self.build_help();
- f.render_widget(help, header_chunks[1]);
-
- let stats = self.build_stats();
- f.render_widget(stats, header_chunks[2]);
-
- let results_list = Self::build_results_list(compact, results);
- f.render_stateful_widget(results_list, chunks[1], &mut self.results_state);
-
- let input = self.build_input(compact, chunks[2].width.into());
- f.render_widget(input, chunks[2]);
-
- let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into());
- f.render_widget(preview, chunks[3]);
-
- let extra_width = UnicodeWidthStr::width(self.search.input.substring());
-
- let cursor_offset = if compact { 0 } else { 1 };
- f.set_cursor(
- // Put cursor past the end of the input text
- chunks[2].x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
- chunks[2].y + cursor_offset,
- );
- }
-
- fn build_title(&mut self) -> Paragraph {
- let title = if self.update_needed.is_some() {
- let version = self.update_needed.clone().unwrap();
-
- Paragraph::new(Text::from(Span::styled(
- format!(" Atuin v{VERSION} - UPDATE AVAILABLE {version}"),
- Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
- )))
- } else {
- Paragraph::new(Text::from(Span::styled(
- format!(" Atuin v{VERSION}"),
- Style::default().add_modifier(Modifier::BOLD),
- )))
- };
- title
- }
-
- #[allow(clippy::unused_self)]
- fn build_help(&mut self) -> Paragraph {
- let help = Paragraph::new(Text::from(Spans::from(vec![
- Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
- Span::raw(" to exit"),
- ])))
- .style(Style::default().fg(Color::DarkGray))
- .alignment(Alignment::Center);
- help
- }
-
- fn build_stats(&mut self) -> Paragraph {
- let stats = Paragraph::new(Text::from(Span::raw(format!(
- "history count: {}",
- self.history_count,
- ))))
- .style(Style::default().fg(Color::DarkGray))
- .alignment(Alignment::Right);
- stats
- }
-
- fn build_results_list(compact: bool, results: &[History]) -> HistoryList {
- let results_list = if compact {
- HistoryList::new(results)
- } else {
- HistoryList::new(results).block(
- Block::default()
- .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded),
- )
- };
- results_list
- }
-
- fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph {
- /// Max width of the UI box showing current mode
- const MAX_WIDTH: usize = 14;
- let (pref, mode) = if self.switched_search_mode {
- (" SRCH:", self.search_mode.as_str())
- } else {
- ("", self.search.filter_mode.as_str())
- };
- let mode_width = MAX_WIDTH - pref.len();
- // sanity check to ensure we don't exceed the layout limits
- debug_assert!(mode_width >= mode.len(), "mode name '{mode}' is too long!");
- let input = format!("[{pref}{mode:^mode_width$}] {}", self.search.input.as_str(),);
- let input = if compact {
- Paragraph::new(input)
- } else {
- Paragraph::new(input).block(
- Block::default()
- .borders(Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded)
- .title(format!("{:─>width$}", "", width = chunk_width - 2)),
- )
- };
- input
- }
-
- fn build_preview(
- &mut self,
- results: &[History],
- compact: bool,
- preview_width: u16,
- chunk_width: usize,
- ) -> Paragraph {
- let selected = self.results_state.selected();
- let command = if results.is_empty() {
- String::new()
- } else {
- use itertools::Itertools as _;
- let s = &results[selected].command;
- s.char_indices()
- .step_by(preview_width.into())
- .map(|(i, _)| i)
- .chain(Some(s.len()))
- .tuple_windows()
- .map(|(a, b)| &s[a..b])
- .join("\n")
- };
- let preview = if compact {
- Paragraph::new(command).style(Style::default().fg(Color::DarkGray))
- } else {
- Paragraph::new(command).block(
- Block::default()
- .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded)
- .title(format!("{:─>width$}", "", width = chunk_width - 2)),
- )
- };
- preview
- }
-}
-
-struct Stdout {
- stdout: std::io::Stdout,
- inline_mode: bool,
-}
-
-impl Stdout {
- pub fn new(inline_mode: bool) -> std::io::Result<Self> {
- terminal::enable_raw_mode()?;
- let mut stdout = stdout();
- if !inline_mode {
- execute!(stdout, terminal::EnterAlternateScreen)?;
- }
- execute!(
- stdout,
- event::EnableMouseCapture,
- event::EnableBracketedPaste,
- )?;
- Ok(Self {
- stdout,
- inline_mode,
- })
- }
-}
-
-impl Drop for Stdout {
- fn drop(&mut self) {
- if !self.inline_mode {
- execute!(self.stdout, terminal::LeaveAlternateScreen).unwrap();
- }
- execute!(
- self.stdout,
- event::DisableMouseCapture,
- event::DisableBracketedPaste,
- )
- .unwrap();
- terminal::disable_raw_mode().unwrap();
- }
-}
-
-impl Write for Stdout {
- fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
- self.stdout.write(buf)
- }
-
- fn flush(&mut self) -> std::io::Result<()> {
- self.stdout.flush()
- }
-}
-
-// 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
-// modular. I'd like to add some more stats and stuff at some point
-#[allow(clippy::cast_possible_truncation)]
-pub async fn history(
- query: &[String],
- settings: &Settings,
- mut db: impl Database,
-) -> Result<String> {
- let stdout = Stdout::new(settings.inline_height > 0)?;
- let backend = CrosstermBackend::new(stdout);
- let mut terminal = Terminal::with_options(
- backend,
- TerminalOptions {
- viewport: if settings.inline_height > 0 {
- Viewport::Inline(settings.inline_height)
- } else {
- Viewport::Fullscreen
- },
- },
- )?;
-
- let mut input = Cursor::from(query.join(" "));
- // Put the cursor at the end of the query by default
- input.end();
-
- let settings2 = settings.clone();
- let update_needed = tokio::spawn(async move { settings2.needs_update().await }).fuse();
- tokio::pin!(update_needed);
-
- let context = current_context();
-
- let history_count = db.history_count().await?;
-
- let mut app = State {
- history_count,
- results_state: ListState::default(),
- update_needed: None,
- switched_search_mode: false,
- search_mode: settings.search_mode,
- 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
- },
- },
- engine: engines::engine(settings.search_mode),
- };
-
- let mut results = app.query_results(&mut db).await?;
-
- let index = 'render: loop {
- let compact = match settings.style {
- atuin_client::settings::Style::Auto => {
- terminal.size().map(|size| size.height < 14).unwrap_or(true)
- }
- atuin_client::settings::Style::Compact => true,
- atuin_client::settings::Style::Full => false,
- };
- terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?;
-
- let initial_input = app.search.input.as_str().to_owned();
- let initial_filter_mode = app.search.filter_mode;
- let initial_search_mode = app.search_mode;
-
- let event_ready = tokio::task::spawn_blocking(|| event::poll(Duration::from_millis(250)));
-
- tokio::select! {
- event_ready = event_ready => {
- if event_ready?? {
- loop {
- if let Some(i) = app.handle_input(settings, &event::read()?, results.len()) {
- break 'render i;
- }
- if !event::poll(Duration::ZERO)? {
- break;
- }
- }
- }
- }
- update_needed = &mut update_needed => {
- app.update_needed = update_needed?;
- }
- }
-
- if initial_input != app.search.input.as_str()
- || initial_filter_mode != app.search.filter_mode
- || initial_search_mode != app.search_mode
- {
- results = app.query_results(&mut db).await?;
- }
- };
-
- if settings.inline_height > 0 {
- terminal.clear()?;
- }
-
- if index < results.len() {
- // index is in bounds so we return that entry
- Ok(results.swap_remove(index).command)
- } else if index == RETURN_ORIGINAL {
- Ok(String::new())
- } else {
- // Either:
- // * index == RETURN_QUERY, in which case we should return the input
- // * out of bounds -> usually implies no selected entry so we return the input
- Ok(app.search.input.into_inner())
- }
-}
diff --git a/src/command/client/stats.rs b/src/command/client/stats.rs
deleted file mode 100644
index 5134f22f..00000000
--- a/src/command/client/stats.rs
+++ /dev/null
@@ -1,181 +0,0 @@
-use std::collections::{HashMap, HashSet};
-
-use chrono::{prelude::*, Duration};
-use clap::Parser;
-use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor};
-use eyre::{bail, Result};
-use interim::parse_date_string;
-
-use atuin_client::{
- database::{current_context, Database},
- history::History,
- settings::{FilterMode, Settings},
-};
-
-#[derive(Parser)]
-#[command(infer_subcommands = true)]
-pub struct Cmd {
- /// compute statistics for the specified period, leave blank for statistics since the beginning
- period: Vec<String>,
-
- /// How many top commands to list
- #[arg(long, short, default_value = "10")]
- count: usize,
-}
-
-fn compute_stats(history: &[History], count: usize) -> Result<()> {
- let mut commands = HashSet::<&str>::with_capacity(history.len());
- let mut prefixes = HashMap::<&str, usize>::with_capacity(history.len());
- for i in history {
- // just in case it somehow has a leading tab or space or something (legacy atuin didn't ignore space prefixes)
- let command = i.command.trim();
- commands.insert(command);
- *prefixes.entry(interesting_command(command)).or_default() += 1;
- }
-
- let unique = commands.len();
- let mut top = prefixes.into_iter().collect::<Vec<_>>();
- top.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
- top.truncate(count);
- if top.is_empty() {
- bail!("No commands found");
- }
-
- let max = top.iter().map(|x| x.1).max().unwrap();
- let num_pad = max.ilog10() as usize + 1;
-
- for (command, count) in top {
- let gray = SetForegroundColor(Color::Grey);
- let bold = SetAttribute(crossterm::style::Attribute::Bold);
-
- let in_ten = 10 * count / max;
- print!("[");
- print!("{}", SetForegroundColor(Color::Red));
- for i in 0..in_ten {
- if i == 2 {
- print!("{}", SetForegroundColor(Color::Yellow));
- }
- if i == 5 {
- print!("{}", SetForegroundColor(Color::Green));
- }
- print!("▮");
- }
- for _ in in_ten..10 {
- print!(" ");
- }
-
- println!("{ResetColor}] {gray}{count:num_pad$}{ResetColor} {bold}{command}{ResetColor}");
- }
- println!("Total commands: {}", history.len());
- println!("Unique commands: {unique}");
-
- Ok(())
-}
-
-impl Cmd {
- pub async fn run(&self, db: &mut impl Database, settings: &Settings) -> Result<()> {
- let context = current_context();
- let words = if self.period.is_empty() {
- String::from("all")
- } else {
- self.period.join(" ")
- };
- let history = if words.as_str() == "all" {
- db.list(FilterMode::Global, &context, None, false).await?
- } else if words.trim() == "today" {
- let start = Local::now().date().and_hms(0, 0, 0);
- let end = start + Duration::days(1);
- db.range(start.into(), end.into()).await?
- } else if words.trim() == "month" {
- let end = Local::now().date().and_hms(0, 0, 0);
- let start = end - Duration::days(31);
- db.range(start.into(), end.into()).await?
- } else if words.trim() == "week" {
- let end = Local::now().date().and_hms(0, 0, 0);
- let start = end - Duration::days(7);
- db.range(start.into(), end.into()).await?
- } else if words.trim() == "year" {
- let end = Local::now().date().and_hms(0, 0, 0);
- let start = end - Duration::days(365);
- db.range(start.into(), end.into()).await?
- } else {
- let start = parse_date_string(&words, Local::now(), settings.dialect.into())?;
- let end = start + Duration::days(1);
- db.range(start.into(), end.into()).await?
- };
- compute_stats(&history, self.count)?;
- Ok(())
- }
-}
-
-// TODO: make this configurable?
-static COMMON_COMMAND_PREFIX: &[&str] = &["sudo"];
-static COMMON_SUBCOMMAND_PREFIX: &[&str] = &["cargo", "go", "git", "npm", "yarn", "pnpm"];
-
-fn first_non_whitespace(s: &str) -> Option<usize> {
- s.char_indices()
- // find the first non whitespace char
- .find(|(_, c)| !c.is_ascii_whitespace())
- // return the index of that char
- .map(|(i, _)| i)
-}
-
-fn first_whitespace(s: &str) -> usize {
- s.char_indices()
- // find the first whitespace char
- .find(|(_, c)| c.is_ascii_whitespace())
- // return the index of that char, (or the max length of the string)
- .map_or(s.len(), |(i, _)| i)
-}
-
-fn interesting_command(mut command: &str) -> &str {
- // compute command prefix
- // we loop here because we might be working with a common command prefix (eg sudo) that we want to trim off
- let (i, prefix) = loop {
- let i = first_whitespace(command);
- let prefix = &command[..i];
-
- // is it a common prefix
- if COMMON_COMMAND_PREFIX.contains(&prefix) {
- command = command[i..].trim_start();
- if command.is_empty() {
- // no commands following, just use the prefix
- return prefix;
- }
- } else {
- break (i, prefix);
- }
- };
-
- // compute subcommand
- let subcommand_indices = command
- // after the end of the command prefix
- .get(i..)
- // find the first non whitespace character (start of subcommand)
- .and_then(first_non_whitespace)
- // then find the end of that subcommand
- .map(|j| i + j + first_whitespace(&command[i + j..]));
-
- match subcommand_indices {
- // if there is a subcommand and it's a common one, then count the full prefix + subcommand
- Some(end) if COMMON_SUBCOMMAND_PREFIX.contains(&prefix) => &command[..end],
- // otherwise just count the main command
- _ => prefix,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::interesting_command;
-
- #[test]
- fn interesting_commands() {
- assert_eq!(interesting_command("cargo"), "cargo");
- assert_eq!(interesting_command("cargo build foo bar"), "cargo build");
- assert_eq!(
- interesting_command("sudo cargo build foo bar"),
- "cargo build"
- );
- assert_eq!(interesting_command("sudo"), "sudo");
- }
-}
diff --git a/src/command/client/sync.rs b/src/command/client/sync.rs
deleted file mode 100644
index 419177a5..00000000
--- a/src/command/client/sync.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-use clap::Subcommand;
-use eyre::{Result, WrapErr};
-
-use atuin_client::{database::Database, settings::Settings};
-
-mod login;
-mod logout;
-mod register;
-mod status;
-
-#[derive(Subcommand)]
-#[command(infer_subcommands = true)]
-pub enum Cmd {
- /// Sync with the configured server
- Sync {
- /// Force re-download everything
- #[arg(long, short)]
- force: bool,
- },
-
- /// Login to the configured server
- Login(login::Cmd),
-
- /// Log out
- Logout,
-
- /// Register with the configured server
- Register(register::Cmd),
-
- /// Print the encryption key for transfer to another machine
- Key {
- /// Switch to base64 output of the key
- #[arg(long)]
- base64: bool,
- },
-
- Status,
-}
-
-impl Cmd {
- pub async fn run(self, settings: Settings, db: &mut impl Database) -> Result<()> {
- match self {
- Self::Sync { force } => run(&settings, force, db).await,
- Self::Login(l) => l.run(&settings).await,
- Self::Logout => logout::run(&settings),
- Self::Register(r) => r.run(&settings).await,
- Self::Status => status::run(&settings, db).await,
- Self::Key { base64 } => {
- use atuin_client::encryption::{encode_key, load_key};
- let key = load_key(&settings).wrap_err("could not load encryption key")?;
-
- if base64 {
- let encode = encode_key(key).wrap_err("could not encode encryption key")?;
- println!("{encode}");
- } else {
- let mnemonic = bip39::Mnemonic::from_entropy(&key.0, bip39::Language::English)
- .map_err(|_| eyre::eyre!("invalid key"))?;
- println!("{mnemonic}");
- }
- Ok(())
- }
- }
- }
-}
-
-async fn run(settings: &Settings, force: bool, db: &mut impl Database) -> Result<()> {
- atuin_client::sync::sync(settings, force, db).await?;
- println!(
- "Sync complete! {} items in database, force: {}",
- db.history_count().await?,
- force
- );
- Ok(())
-}
diff --git a/src/command/client/sync/login.rs b/src/command/client/sync/login.rs
deleted file mode 100644
index 6aa2d847..00000000
--- a/src/command/client/sync/login.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-use std::{io, path::PathBuf};
-
-use clap::Parser;
-use eyre::{bail, Context, ContextCompat, Result};
-use tokio::{fs::File, io::AsyncWriteExt};
-
-use atuin_client::{
- api_client,
- encryption::{decode_key, encode_key, new_key, Key},
- settings::Settings,
-};
-use atuin_common::api::LoginRequest;
-use rpassword::prompt_password;
-
-#[derive(Parser)]
-pub struct Cmd {
- #[clap(long, short)]
- pub username: Option<String>,
-
- #[clap(long, short)]
- pub password: Option<String>,
-
- /// The encryption key for your account
- #[clap(long, short)]
- pub key: Option<String>,
-}
-
-fn get_input() -> Result<String> {
- let mut input = String::new();
- io::stdin().read_line(&mut input)?;
- Ok(input.trim_end_matches(&['\r', '\n'][..]).to_string())
-}
-
-impl Cmd {
- pub async fn run(&self, settings: &Settings) -> Result<()> {
- let session_path = settings.session_path.as_str();
-
- if PathBuf::from(session_path).exists() {
- println!(
- "You are already logged in! Please run 'atuin logout' if you wish to login again"
- );
-
- return Ok(());
- }
-
- let username = or_user_input(&self.username, "username");
- let key = or_user_input(&self.key, "encryption key [blank to use existing key file]");
- let password = self.password.clone().unwrap_or_else(read_user_password);
-
- let key_path = settings.key_path.as_str();
- if key.is_empty() {
- if PathBuf::from(key_path).exists() {
- let bytes = fs_err::read_to_string(key_path)
- .context("existing key file couldn't be read")?;
- if decode_key(bytes).is_err() {
- bail!("the key in existing key file was invalid");
- }
- } else {
- println!("No key file exists, creating a new");
- let _key = new_key(settings)?;
- }
- } else {
- // try parse the key as a mnemonic...
- let key = match bip39::Mnemonic::from_phrase(&key, bip39::Language::English) {
- Ok(mnemonic) => encode_key(
- Key::from_slice(mnemonic.entropy())
- .context("key was not the correct length")?,
- )?,
- Err(err) => {
- if let Some(err) = err.downcast_ref::<bip39::ErrorKind>() {
- match err {
- // assume they copied in the base64 key
- bip39::ErrorKind::InvalidWord => key,
- bip39::ErrorKind::InvalidChecksum => {
- bail!("key mnemonic was not valid")
- }
- bip39::ErrorKind::InvalidKeysize(_)
- | bip39::ErrorKind::InvalidWordLength(_)
- | bip39::ErrorKind::InvalidEntropyLength(_, _) => {
- bail!("key was not the correct length")
- }
- }
- } else {
- // unknown error. assume they copied the base64 key
- key
- }
- }
- };
-
- if decode_key(key.clone()).is_err() {
- bail!("the specified key was invalid");
- }
-
- let mut file = File::create(key_path).await?;
- file.write_all(key.as_bytes()).await?;
- }
-
- let session = api_client::login(
- settings.sync_address.as_str(),
- LoginRequest { username, password },
- )
- .await?;
-
- let session_path = settings.session_path.as_str();
- let mut file = File::create(session_path).await?;
- file.write_all(session.session.as_bytes()).await?;
-
- println!("Logged in!");
-
- Ok(())
- }
-}
-
-pub(super) fn or_user_input(value: &'_ Option<String>, name: &'static str) -> String {
- value.clone().unwrap_or_else(|| read_user_input(name))
-}
-
-pub(super) fn read_user_password() -> String {
- let password = prompt_password("Please enter password: ");
- password.expect("Failed to read from input")
-}
-
-fn read_user_input(name: &'static str) -> String {
- eprint!("Please enter {name}: ");
- get_input().expect("Failed to read from input")
-}
-
-#[cfg(test)]
-mod tests {
- use atuin_client::encryption::Key;
-
- #[test]
- fn mnemonic_round_trip() {
- let key = Key {
- 0: [
- 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3, 8, 4, 6, 2, 6, 4, 3, 3, 8, 3,
- 2, 7, 9, 5,
- ],
- };
- let phrase = bip39::Mnemonic::from_entropy(&key.0, bip39::Language::English)
- .unwrap()
- .into_phrase();
- let mnemonic = bip39::Mnemonic::from_phrase(&phrase, bip39::Language::English).unwrap();
- assert_eq!(mnemonic.entropy(), &key.0);
- assert_eq!(phrase, "adapt amused able anxiety mother adapt beef gaze amount else seat alcohol cage lottery avoid scare alcohol cactus school avoid coral adjust catch pink");
- }
-}
diff --git a/src/command/client/sync/logout.rs b/src/command/client/sync/logout.rs
deleted file mode 100644
index 90b49d6d..00000000
--- a/src/command/client/sync/logout.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use std::path::PathBuf;
-
-use eyre::{Context, Result};
-use fs_err::remove_file;
-
-use atuin_client::settings::Settings;
-
-pub fn run(settings: &Settings) -> Result<()> {
- let session_path = settings.session_path.as_str();
-
- if PathBuf::from(session_path).exists() {
- remove_file(session_path).context("Failed to remove session file")?;
- println!("You have logged out!");
- } else {
- println!("You are not logged in");
- }
-
- Ok(())
-}
diff --git a/src/command/client/sync/register.rs b/src/command/client/sync/register.rs
deleted file mode 100644
index 6b51fac8..00000000
--- a/src/command/client/sync/register.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use clap::Parser;
-use eyre::Result;
-use tokio::{fs::File, io::AsyncWriteExt};
-
-use atuin_client::{api_client, settings::Settings};
-
-#[derive(Parser)]
-pub struct Cmd {
- #[clap(long, short)]
- pub username: Option<String>,
-
- #[clap(long, short)]
- pub password: Option<String>,
-
- #[clap(long, short)]
- pub email: Option<String>,
-}
-
-impl Cmd {
- pub async fn run(self, settings: &Settings) -> Result<()> {
- run(settings, &self.username, &self.email, &self.password).await
- }
-}
-
-pub async fn run(
- settings: &Settings,
- username: &Option<String>,
- email: &Option<String>,
- password: &Option<String>,
-) -> Result<()> {
- use super::login::or_user_input;
- let username = or_user_input(username, "username");
- let email = or_user_input(email, "email");
- let password = password
- .clone()
- .unwrap_or_else(super::login::read_user_password);
-
- let session =
- api_client::register(settings.sync_address.as_str(), &username, &email, &password).await?;
-
- let path = settings.session_path.as_str();
- let mut file = File::create(path).await?;
- file.write_all(session.session.as_bytes()).await?;
-
- // Create a new key, and save it to disk
- let _key = atuin_client::encryption::new_key(settings)?;
-
- Ok(())
-}
diff --git a/src/command/client/sync/status.rs b/src/command/client/sync/status.rs
deleted file mode 100644
index b3e73e8e..00000000
--- a/src/command/client/sync/status.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use atuin_client::{
- api_client, database::Database, encryption::load_encoded_key, settings::Settings,
-};
-use colored::Colorize;
-use eyre::Result;
-
-pub async fn run(settings: &Settings, db: &impl Database) -> Result<()> {
- let client = api_client::Client::new(
- &settings.sync_address,
- &settings.session_token,
- load_encoded_key(settings)?,
- )?;
-
- let status = client.status().await?;
- let last_sync = Settings::last_sync()?;
- let local_count = db.history_count().await?;
-
- println!("{}", "[Local]".green());
-
- if settings.auto_sync {
- println!("Sync frequency: {}", settings.sync_frequency);
- println!("Last sync: {last_sync}");
- }
-
- println!("History count: {local_count}\n");
-
- if settings.auto_sync {
- println!("{}", "[Remote]".green());
- println!("Address: {}", settings.sync_address);
- println!("Username: {}", status.username);
- println!("History count: {}", status.count);
- }
-
- Ok(())
-}