diff options
Diffstat (limited to 'crates/turtle/src/command/client/search.rs')
| -rw-r--r-- | crates/turtle/src/command/client/search.rs | 366 |
1 files changed, 0 insertions, 366 deletions
diff --git a/crates/turtle/src/command/client/search.rs b/crates/turtle/src/command/client/search.rs deleted file mode 100644 index 359864cb..00000000 --- a/crates/turtle/src/command/client/search.rs +++ /dev/null @@ -1,366 +0,0 @@ -use std::fs::File; -use std::io::{IsTerminal as _, Write, stderr, stdout}; - -use crate::atuin_client::database::ClientSqlite; -use crate::atuin_common::utils::{self, Escapable as _}; -use clap::Parser; -use eyre::Result; - -use crate::atuin_client::{ - database::{OptFilters, current_context}, - encryption, - history::{History, store::HistoryStore}, - record::sqlite_store::SqliteStore, - settings::{FilterMode, KeymapMode, SearchMode, Settings, Timezone}, -}; - -use super::history::ListMode; - -mod cursor; -mod duration; -mod engines; -mod history_list; -mod inspector; -mod interactive; -pub(crate) mod keybindings; - -pub(crate) use duration::format_duration_into; - -#[expect(clippy::struct_excessive_bools, clippy::struct_field_names)] -#[derive(Parser, Debug)] -pub(crate) 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, - - /// Notify the keymap at the shell's side - #[arg(long = "keymap-mode", default_value = "auto")] - keymap_mode: KeymapMode, - - /// Use human-readable formatting for time - #[arg(long)] - human: bool, - - #[arg(allow_hyphen_values = true)] - query: Option<Vec<String>>, - - /// Show only the text of the command - #[arg(long)] - cmd_only: bool, - - /// Terminate the output with a null, for better multiline handling - #[arg(long)] - print0: bool, - - /// Delete anything matching this query. Will not print out the match - #[arg(long)] - delete: bool, - - /// Delete EVERYTHING! - #[arg(long)] - delete_it_all: bool, - - /// Reverse the order of results, oldest first - #[arg(long, short)] - reverse: bool, - - /// Display the command time in another timezone other than the configured default. - /// - /// This option takes one of the following kinds of values: - /// - the special value "local" (or "l") which refers to the system time zone - /// - an offset from UTC (e.g. "+9", "-2:30") - #[arg(long, visible_alias = "tz")] - #[arg(allow_hyphen_values = true)] - // Clippy warns about `Option<Option<T>>`, but we suppress it because we need - // this distinction for proper argument handling. - #[expect(clippy::option_option)] - timezone: Option<Option<Timezone>>, - - /// Available variables: {command}, {directory}, {duration}, {user}, {host}, {time}, {exit} and - /// {relativetime}. - /// Example: --format "{time} - [{duration}] - {directory}$\t{command}" - #[arg(long, short)] - format: Option<String>, - - /// Set the maximum number of lines Atuin's interface should take up. - #[arg(long = "inline-height")] - inline_height: Option<u16>, - - /// Filter by author. Supports $all-user (non-agents), $all-agent, or literal names. - /// Can be specified multiple times. - #[arg(long)] - author: Option<Vec<String>>, - - /// Include duplicate commands in the output (non-interactive only) - #[arg(long)] - include_duplicates: bool, - - /// File name to write the result to (hidden from help as this is meant to be used from a script) - #[arg(long = "result-file", hide = true)] - result_file: Option<String>, -} - -impl Cmd { - /// Returns true if this search command will run in interactive (TUI) mode - pub(crate) fn is_interactive(&self) -> bool { - self.interactive - } - - // clippy: please write this instead - // clippy: now it has too many lines - // me: I'll do it later OKAY - #[expect(clippy::too_many_lines)] - pub(crate) async fn run( - self, - db: ClientSqlite, - settings: &mut Settings, - store: SqliteStore, - ) -> Result<()> { - let query = self.query.unwrap_or_else(|| { - std::env::var("ATUIN_QUERY").map_or_else( - |_| vec![], - |query| query.split(' ').map(ToString::to_string).collect(), - ) - }); - - if (self.delete_it_all || self.delete) && self.limit.is_some() { - // Because of how deletion is implemented, it will always delete all matches - // and disregard the limit option. It is also not clear what deletion with a - // limit would even mean. Deleting the LIMIT most recent entries that match - // the search query would make sense, but that wouldn't match what's displayed - // when running the equivalent search, but deleting those entries that are - // displayed with the search would leave any duplicates of those lines which may - // or may not have been intended to be deleted. - eprintln!("\"--limit\" is not compatible with deletion."); - return Ok(()); - } - - if self.delete && query.is_empty() { - eprintln!( - "Please specify a query to match the items you wish to delete. If you wish to delete all history, pass --delete-it-all" - ); - return Ok(()); - } - - if self.delete_it_all && !query.is_empty() { - eprintln!( - "--delete-it-all will delete ALL of your history! It does not require a query." - ); - return Ok(()); - } - - if let Some(search_mode) = self.search_mode { - settings.search_mode = search_mode; - } - if let Some(filter_mode) = self.filter_mode { - settings.filter_mode = Some(filter_mode); - } - if let Some(inline_height) = self.inline_height { - settings.inline_height = inline_height; - } - - settings.shell_up_key_binding = self.shell_up_key_binding; - - // `keymap_mode` specified in config.toml overrides the `--keymap-mode` - // option specified in the keybindings. - settings.keymap_mode = match settings.keymap_mode { - KeymapMode::Auto => self.keymap_mode, - value => value, - }; - settings.keymap_mode_shell = self.keymap_mode; - - let encryption_key: [u8; 32] = encryption::load_key(settings)?.into(); - - let host_id = Settings::host_id().await?; - let history_store = HistoryStore::new(store.clone(), host_id, encryption_key); - - if self.interactive { - let item = interactive::history(&query, settings, db, &history_store).await?; - - if let Some(result_file) = self.result_file { - let mut file = File::create(result_file)?; - write!(file, "{item}")?; - } else if !stdout().is_terminal() { - // stdout is not a terminal - likely command substitution like VAR=$(atuin search -i) - // Write to stdout so it gets captured. This requires some care on Windows, as the current - // console code page or `[Console]::OutputEncoding` on PowerShell may be different from UTF-8. - println!("{item}"); - } else if stderr().is_terminal() { - eprintln!("{}", item.escape_control()); - } else { - eprintln!("{item}"); - } - } else { - 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, - include_duplicates: self.include_duplicates, - }; - - let mut entries = - run_non_interactive(settings, opt_filter.clone(), &query, &db).await?; - - if entries.is_empty() { - std::process::exit(1) - } - - // if we aren't deleting, print it all - if self.delete || self.delete_it_all { - // delete it - // it only took me _years_ to add this - // sorry - while !entries.is_empty() { - for entry in &entries { - eprintln!("deleting {}", entry.id); - } - - let ids = history_store.delete_entries(entries).await?; - history_store.incremental_build(&db, &ids).await?; - - entries = - run_non_interactive(settings, opt_filter.clone(), &query, &db).await?; - } - } else { - let format = match self.format { - None => Some(settings.history_format.as_str()), - _ => self.format.as_deref(), - }; - let tz = match self.timezone { - Some(Some(tz)) => tz, // User provided a value - Some(None) | None => settings.timezone, // No value was provided - }; - - super::history::print_list( - &entries, - ListMode::from_flags(self.human, self.cmd_only), - format, - self.print0, - true, - tz, - ); - } - } - Ok(()) - } -} - -// This is supposed to more-or-less mirror the command line version, so ofc -// it is going to have a lot of args -async fn run_non_interactive( - settings: &Settings, - filter_options: OptFilters, - query: &[String], - db: &ClientSqlite, -) -> 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().await?; - - let opt_filter = OptFilters { - cwd: dir.clone(), - ..filter_options - }; - - let filter_mode = settings.default_filter_mode(context.git_root.is_some()); - - let results = db - .search( - settings.search_mode, - filter_mode, - &context, - query.join(" ").as_str(), - opt_filter, - ) - .await?; - - Ok(results) -} - -#[cfg(test)] -mod tests { - use super::Cmd; - use clap::Parser; - - #[test] - fn search_for_triple_dash() { - // Issue #3028: searching for `---` should not be treated as a CLI flag - let cmd = Cmd::try_parse_from(["search", "---"]); - assert!(cmd.is_ok(), "Failed to parse '---' as a query: {cmd:?}"); - let cmd = cmd.unwrap(); - assert_eq!(cmd.query, Some(vec!["---".to_string()])); - } - - #[test] - fn search_for_double_dash_value() { - // Searching for strings starting with -- should also work - let cmd = Cmd::try_parse_from(["search", "--", "--foo"]); - assert!(cmd.is_ok()); - let cmd = cmd.unwrap(); - assert_eq!(cmd.query, Some(vec!["--foo".to_string()])); - } - - #[test] - fn search_author_cli_flag() { - let cmd = - Cmd::try_parse_from(["search", "--author", "codex", "--author", "ellie"]).unwrap(); - assert_eq!( - cmd.author, - Some(vec!["codex".to_string(), "ellie".to_string()]) - ); - } -} |
