From ad81d1f54fd7007ff5c260bdc6c5e8a31e7b1b52 Mon Sep 17 00:00:00 2001 From: atuin-bot <152089506+atuin-bot@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:47:11 -0700 Subject: [PATCH] chore(rustfmt): update Rustfmt config (#3945) Update `.rustfmt.toml` to be more opinionated. Formatting now requires nightly Rust; use `cargo +nightly fmt`. --- .depot/workflows/rust.yml | 6 +- .devcontainer/Dockerfile | 1 + .github/workflows/rust.yml | 6 +- .rustfmt.toml | 19 +- AGENTS.md | 4 +- CONTRIBUTING.md | 4 +- crates/atuin-ai/src/commands/inline.rs | 91 +- crates/atuin-ai/src/context.rs | 7 +- crates/atuin-ai/src/context_window.rs | 74 +- crates/atuin-ai/src/diff.rs | 24 +- crates/atuin-ai/src/event_serde.rs | 45 +- crates/atuin-ai/src/file_tracker.rs | 68 +- crates/atuin-ai/src/fsm/effects.rs | 16 +- crates/atuin-ai/src/fsm/mod.rs | 70 +- crates/atuin-ai/src/fsm/tests.rs | 189 +--- crates/atuin-ai/src/fsm/tools.rs | 14 +- crates/atuin-ai/src/history_format.rs | 20 +- crates/atuin-ai/src/mcp.rs | 28 +- crates/atuin-ai/src/models.rs | 9 +- crates/atuin-ai/src/permissions/check.rs | 8 +- crates/atuin-ai/src/permissions/rule.rs | 18 +- crates/atuin-ai/src/permissions/shell.rs | 13 +- crates/atuin-ai/src/permissions/walker.rs | 5 +- crates/atuin-ai/src/permissions/writer.rs | 7 +- crates/atuin-ai/src/session.rs | 63 +- crates/atuin-ai/src/shell.rs | 3 +- crates/atuin-ai/src/skills/frontmatter.rs | 20 +- crates/atuin-ai/src/skills/mod.rs | 50 +- crates/atuin-ai/src/skills/walker.rs | 13 +- crates/atuin-ai/src/snapshots.rs | 55 +- crates/atuin-ai/src/store.rs | 107 +- crates/atuin-ai/src/stream.rs | 17 +- crates/atuin-ai/src/tools/descriptor.rs | 5 +- crates/atuin-ai/src/tools/mod.rs | 210 ++-- crates/atuin-ai/src/tui/app.rs | 349 ++----- crates/atuin-ai/src/tui/persist.rs | 5 +- crates/atuin-ai/src/tui/select.rs | 4 +- crates/atuin-ai/src/tui/state.rs | 12 +- crates/atuin-ai/src/tui/tips.rs | 19 +- crates/atuin-ai/src/tui/tools_exec.rs | 21 +- crates/atuin-ai/src/tui/view/input.rs | 17 +- crates/atuin-ai/src/tui/view/mod.rs | 181 ++-- crates/atuin-ai/src/tui/view/trunc.rs | 33 +- crates/atuin-ai/src/tui/view/turn.rs | 100 +- crates/atuin-ai/src/usage.rs | 13 +- .../atuin-ai/src/user_context/interpolate.rs | 22 +- crates/atuin-ai/src/user_context/mod.rs | 12 +- crates/atuin-client/benches/history_store.rs | 17 +- crates/atuin-client/benches/ordering.rs | 5 +- .../benches/record/sqlite_store.rs | 23 +- crates/atuin-client/src/api_client.rs | 89 +- crates/atuin-client/src/auth.rs | 55 +- crates/atuin-client/src/database.rs | 431 +++----- crates/atuin-client/src/distro.rs | 20 +- crates/atuin-client/src/history.rs | 22 +- crates/atuin-client/src/history/store.rs | 77 +- crates/atuin-client/src/hub.rs | 20 +- crates/atuin-client/src/import/bash.rs | 59 +- crates/atuin-client/src/import/fish.rs | 10 +- crates/atuin-client/src/import/mod.rs | 10 +- crates/atuin-client/src/import/nu.rs | 5 +- crates/atuin-client/src/import/nu_histdb.rs | 25 +- crates/atuin-client/src/import/powershell.rs | 25 +- crates/atuin-client/src/import/replxx.rs | 30 +- crates/atuin-client/src/import/resh.rs | 27 +- crates/atuin-client/src/import/xonsh.rs | 9 +- .../atuin-client/src/import/xonsh_sqlite.rs | 22 +- crates/atuin-client/src/import/zsh.rs | 57 +- crates/atuin-client/src/import/zsh_histdb.rs | 42 +- crates/atuin-client/src/login.rs | 9 +- crates/atuin-client/src/logs.rs | 6 +- crates/atuin-client/src/meta.rs | 36 +- crates/atuin-client/src/ordering.rs | 8 +- crates/atuin-client/src/packfile/mod.rs | 3 +- crates/atuin-client/src/packfile/packer.rs | 33 +- crates/atuin-client/src/packfile/record.rs | 3 +- crates/atuin-client/src/packfile/sync.rs | 64 +- crates/atuin-client/src/plugin.rs | 10 +- .../atuin-client/src/record/sqlite_store.rs | 100 +- crates/atuin-client/src/record/sync.rs | 405 +++----- crates/atuin-client/src/register.rs | 5 +- crates/atuin-client/src/secrets.rs | 9 +- crates/atuin-client/src/settings.rs | 151 ++- crates/atuin-client/src/settings/daemon.rs | 77 +- crates/atuin-client/src/settings/shells.rs | 3 +- crates/atuin-client/src/settings/watcher.rs | 24 +- crates/atuin-client/src/theme.rs | 209 ++-- crates/atuin-common/src/ansi.rs | 10 +- crates/atuin-common/src/docs.rs | 3 +- .../atuin-common/src/encryption/paseto_v4.rs | 33 +- crates/atuin-common/src/filter.rs | 45 +- crates/atuin-common/src/logs.rs | 3 +- crates/atuin-common/src/os/unix.rs | 6 +- crates/atuin-common/src/path/display_rich.rs | 45 +- crates/atuin-common/src/rmp/decode.rs | 17 +- crates/atuin-common/src/rmp/encode.rs | 22 +- crates/atuin-common/src/shell.rs | 15 +- crates/atuin-common/src/slice.rs | 16 +- crates/atuin-common/src/string.rs | 1 - crates/atuin-common/src/string/align.rs | 10 +- crates/atuin-common/src/string/ellipsis.rs | 44 +- .../string/escape_non_printable_posix_ext.rs | 6 +- crates/atuin-common/src/string/non_nul_str.rs | 3 +- crates/atuin-common/src/time/duration.rs | 47 +- .../atuin-common/src/time/offset_date_time.rs | 31 +- crates/atuin-common/src/time/utc_offset.rs | 10 +- crates/atuin-common/src/url.rs | 64 +- crates/atuin-common/src/utils.rs | 15 +- crates/atuin-daemon/build.rs | 3 +- crates/atuin-daemon/src/client.rs | 45 +- crates/atuin-daemon/src/components/history.rs | 58 +- crates/atuin-daemon/src/components/search.rs | 66 +- .../atuin-daemon/src/components/semantic.rs | 131 +-- crates/atuin-daemon/src/components/sync.rs | 34 +- crates/atuin-daemon/src/control/service.rs | 15 +- crates/atuin-daemon/src/daemon.rs | 22 +- crates/atuin-daemon/src/lib.rs | 17 +- crates/atuin-daemon/src/search/index.rs | 166 +--- crates/atuin-daemon/src/server.rs | 16 +- crates/atuin-daemon/tests/lifecycle.rs | 24 +- crates/atuin-daemon/tests/search.rs | 57 +- crates/atuin-domain/src/api.rs | 4 +- crates/atuin-domain/src/caps/axum.rs | 96 +- crates/atuin-domain/src/caps/client.rs | 49 +- crates/atuin-domain/src/caps/middleware.rs | 66 +- crates/atuin-domain/src/caps/mod.rs | 16 +- crates/atuin-domain/src/caps/server.rs | 30 +- crates/atuin-domain/src/record/mod.rs | 70 +- crates/atuin-dotfiles/src/shell.rs | 23 +- crates/atuin-dotfiles/src/shell/bash.rs | 3 +- crates/atuin-dotfiles/src/shell/fish.rs | 3 +- crates/atuin-dotfiles/src/shell/powershell.rs | 20 +- crates/atuin-dotfiles/src/shell/xonsh.rs | 3 +- crates/atuin-dotfiles/src/shell/zsh.rs | 3 +- crates/atuin-dotfiles/src/store.rs | 89 +- crates/atuin-dotfiles/src/store/var.rs | 71 +- crates/atuin-history/benches/smart_sort.rs | 8 +- crates/atuin-history/src/stats.rs | 30 +- crates/atuin-kv/src/database.rs | 36 +- crates/atuin-kv/src/store.rs | 25 +- crates/atuin-kv/src/store/record.rs | 8 +- crates/atuin-lab-share/src/crypto.rs | 68 +- crates/atuin-lab-share/src/error.rs | 35 +- crates/atuin-lab-share/src/lib.rs | 92 +- crates/atuin-lab-share/src/lifecycle.rs | 39 +- crates/atuin-lab-share/src/protocol.rs | 48 +- crates/atuin-lab-share/src/proxy_tap.rs | 103 +- crates/atuin-lab-share/src/query.rs | 5 +- crates/atuin-lab-share/src/render.rs | 18 +- crates/atuin-lab-share/src/session/mod.rs | 111 +-- crates/atuin-lab-share/src/session/screen.rs | 22 +- crates/atuin-lab-share/src/subshell.rs | 6 +- crates/atuin-lab-share/src/transport.rs | 181 +--- .../matcher/benches/fuzzy_match.rs | 27 +- crates/atuin-nucleo/matcher/src/chars.rs | 11 +- crates/atuin-nucleo/matcher/src/debug.rs | 3 +- crates/atuin-nucleo/matcher/src/exact.rs | 5 +- .../atuin-nucleo/matcher/src/fuzzy_greedy.rs | 8 +- .../atuin-nucleo/matcher/src/fuzzy_optimal.rs | 35 +- crates/atuin-nucleo/matcher/src/lib.rs | 17 +- crates/atuin-nucleo/matcher/src/matrix.rs | 16 +- crates/atuin-nucleo/matcher/src/pattern.rs | 2 +- .../atuin-nucleo/matcher/src/pattern/tests.rs | 33 +- crates/atuin-nucleo/matcher/src/prefilter.rs | 2 +- crates/atuin-nucleo/matcher/src/tests.rs | 925 +++++++----------- crates/atuin-nucleo/matcher/src/utf32_str.rs | 30 +- crates/atuin-nucleo/src/boxcar.rs | 34 +- crates/atuin-nucleo/src/lib.rs | 11 +- crates/atuin-nucleo/src/par_sort.rs | 8 +- crates/atuin-nucleo/src/pattern.rs | 16 +- crates/atuin-nucleo/src/tests.rs | 28 +- crates/atuin-nucleo/src/worker.rs | 35 +- crates/atuin-pty-proxy/src/capture.rs | 62 +- crates/atuin-pty-proxy/src/debug.rs | 3 +- crates/atuin-pty-proxy/src/osc133.rs | 114 +-- crates/atuin-pty-proxy/src/protocol.rs | 10 +- crates/atuin-pty-proxy/src/pty_proxy.rs | 20 +- crates/atuin-pty-proxy/src/runtime.rs | 23 +- crates/atuin-pty-proxy/src/screen.rs | 53 +- crates/atuin-pty-proxy/tests/fd_pressure.rs | 32 +- crates/atuin-pty-proxy/tests/subscriber.rs | 51 +- crates/atuin-scripts/src/database.rs | 56 +- crates/atuin-scripts/src/execution.rs | 31 +- crates/atuin-scripts/src/store.rs | 13 +- crates/atuin-scripts/src/store/record.rs | 45 +- crates/atuin-scripts/src/store/script.rs | 24 +- crates/atuin-search-bench/benches/search.rs | 25 +- crates/atuin-search-bench/src/corpus.rs | 49 +- crates/atuin-server-database/src/lib.rs | 3 +- crates/atuin-server-postgres/src/lib.rs | 69 +- crates/atuin-server-postgres/src/wrappers.rs | 3 +- crates/atuin-server-sqlite/src/lib.rs | 23 +- crates/atuin-server-sqlite/src/wrappers.rs | 3 +- crates/atuin-server/src/bin/main.rs | 16 +- crates/atuin-server/src/handlers/health.rs | 9 +- crates/atuin-server/src/handlers/mod.rs | 10 +- crates/atuin-server/src/handlers/user.rs | 40 +- crates/atuin-server/src/handlers/v0/me.rs | 3 +- crates/atuin-server/src/handlers/v0/record.rs | 27 +- crates/atuin-server/src/handlers/v0/store.rs | 11 +- crates/atuin-server/src/lib.rs | 29 +- crates/atuin-server/src/metrics.rs | 19 +- crates/atuin-server/src/router.rs | 97 +- crates/atuin-server/src/settings.rs | 19 +- crates/atuin-server/src/trace.rs | 6 +- crates/atuin-server/tests/common/mod.rs | 10 +- crates/atuin-server/tests/sync.rs | 43 +- crates/atuin-server/tests/users.rs | 14 +- crates/atuin/src/command/client.rs | 23 +- crates/atuin/src/command/client/account.rs | 5 +- .../command/client/account/change_password.rs | 7 +- .../src/command/client/account/delete.rs | 10 +- .../atuin/src/command/client/account/link.rs | 3 +- .../atuin/src/command/client/account/login.rs | 49 +- .../src/command/client/account/register.rs | 51 +- crates/atuin/src/command/client/config.rs | 54 +- crates/atuin/src/command/client/daemon.rs | 44 +- crates/atuin/src/command/client/doctor.rs | 56 +- crates/atuin/src/command/client/dotfiles.rs | 4 +- .../src/command/client/dotfiles/alias.rs | 38 +- .../atuin/src/command/client/dotfiles/var.rs | 28 +- crates/atuin/src/command/client/history.rs | 183 ++-- crates/atuin/src/command/client/hook.rs | 54 +- crates/atuin/src/command/client/hook/event.rs | 26 +- crates/atuin/src/command/client/hook/wire.rs | 3 +- crates/atuin/src/command/client/import.rs | 24 +- crates/atuin/src/command/client/info.rs | 63 +- crates/atuin/src/command/client/init.rs | 18 +- crates/atuin/src/command/client/init/bash.rs | 16 +- crates/atuin/src/command/client/init/fish.rs | 6 +- .../src/command/client/init/powershell.rs | 10 +- crates/atuin/src/command/client/init/xonsh.rs | 18 +- crates/atuin/src/command/client/init/zsh.rs | 6 +- crates/atuin/src/command/client/kv.rs | 12 +- crates/atuin/src/command/client/lab.rs | 49 +- crates/atuin/src/command/client/scripts.rs | 60 +- crates/atuin/src/command/client/search.rs | 64 +- .../atuin/src/command/client/search/cursor.rs | 17 +- .../src/command/client/search/engines.rs | 34 +- .../command/client/search/engines/daemon.rs | 14 +- .../src/command/client/search/engines/db.rs | 32 +- .../src/command/client/search/history_list.rs | 93 +- .../src/command/client/search/inspector.rs | 101 +- .../src/command/client/search/interactive.rs | 381 +++----- .../client/search/keybindings/actions.rs | 3 +- .../client/search/keybindings/conditions.rs | 22 +- .../client/search/keybindings/defaults.rs | 160 ++- .../command/client/search/keybindings/key.rs | 24 +- .../client/search/keybindings/keymap.rs | 19 +- .../atuin/src/command/client/search/syntax.rs | 48 +- crates/atuin/src/command/client/setup.rs | 22 +- crates/atuin/src/command/client/stats.rs | 23 +- crates/atuin/src/command/client/store.rs | 5 +- crates/atuin/src/command/client/store/pull.rs | 16 +- .../atuin/src/command/client/store/purge.rs | 6 +- crates/atuin/src/command/client/store/push.rs | 14 +- .../atuin/src/command/client/store/rebuild.rs | 17 +- .../atuin/src/command/client/store/rekey.rs | 6 +- .../atuin/src/command/client/store/verify.rs | 6 +- crates/atuin/src/command/client/sync.rs | 16 +- .../atuin/src/command/client/sync/status.rs | 6 +- crates/atuin/src/command/client/update.rs | 3 +- crates/atuin/src/command/client/wrapped.rs | 60 +- crates/atuin/src/command/external.rs | 19 +- crates/atuin/src/command/gen_completions.rs | 7 +- crates/atuin/src/command/mod.rs | 6 +- crates/atuin/src/logs.rs | 30 +- crates/atuin/src/main.rs | 3 +- crates/atuin/src/print_error.rs | 11 +- crates/atuin/src/sync.rs | 16 +- crates/tests-database/src/helpers.rs | 3 +- crates/tests-database/tests/db_story.rs | 45 +- 272 files changed, 3939 insertions(+), 7437 deletions(-) diff --git a/.depot/workflows/rust.yml b/.depot/workflows/rust.yml index 550995d7b..245cf7fcf 100644 --- a/.depot/workflows/rust.yml +++ b/.depot/workflows/rust.yml @@ -178,10 +178,10 @@ jobs: runs-on: depot-ubuntu-24.04 steps: - uses: actions/checkout@v6 - - name: Install latest rust + - name: Install nightly rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.94.0 + toolchain: nightly components: rustfmt - name: Format - run: cargo fmt -- --check + run: cargo +nightly fmt -- --check diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 18f35969f..9ee861397 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # --- rust conveniences --- RUN rustup component add rust-analyzer clippy rustfmt && \ + rustup toolchain install nightly --profile minimal --component rustfmt && \ cargo install cargo-nextest --locked || true # Use mold by default. This has to live in $CARGO_HOME (/usr/local/cargo in this diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b06d22c5f..82e2d0fa6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -266,14 +266,14 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Install latest rust + - name: Install nightly rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.97.0 + toolchain: nightly components: rustfmt - name: Format - run: cargo fmt -- --check + run: cargo +nightly fmt -- --check deny: runs-on: depot-ubuntu-24.04 diff --git a/.rustfmt.toml b/.rustfmt.toml index ec1e17ff6..029d0cf4e 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,17 @@ +comment_width = 100 +#error_on_line_overflow = true +#error_on_unformatted = true +format_macro_bodies = true +format_macro_matchers = true +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Module" +max_width = 100 +newline_style = "Unix" +overflow_delimited_expr = true reorder_imports = true -# uncomment once stable -#imports_granularity = "crate" -#group_imports = "StdExternalCrate" +struct_lit_width = 18 +style_edition = "2024" +unstable_features = true +use_field_init_shorthand = true +use_small_heuristics = "Off" diff --git a/AGENTS.md b/AGENTS.md index 10b04b93e..eba243104 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ atuin-server-sqlite SQLite implementation (sqlx) - `#![deny(unsafe_code)]` on client/common, `#![forbid(unsafe_code)]` on server. - Clippy: `pedantic` + `nursery` on main crate. CI enforces `-D warnings -D clippy::redundant_clone`, on both the default targets and `--tests`. - Rustdoc: CI runs `cargo doc --document-private-items --no-deps --workspace` with `RUSTDOCFLAGS=-D warnings`. Broken intra-doc links fail the build. -- Format: `cargo fmt`. Only non-default: `reorder_imports = true`. +- Format: `cargo +nightly fmt`. `.rustfmt.toml` uses nightly-only options, so formatting requires the nightly toolchain even though the project builds on stable 1.97.0. - IDs: UUIDv7 (time-ordered), newtype wrappers (`HistoryId`, `RecordId`, `HostId`). - Serialization: MessagePack for encrypted payloads, JSON for API, TOML for config. - Storage traits: `Database` (client), `Store` (record store), `Database` (server) -- all `async_trait`. @@ -80,6 +80,6 @@ cargo build cargo test cargo clippy -- -D warnings -D clippy::redundant_clone cargo clippy --tests -- -D warnings -D clippy::redundant_clone -cargo fmt --check +cargo +nightly fmt --check RUSTDOCFLAGS="-D warnings" cargo doc --document-private-items --no-deps --workspace ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48755f9cf..f58beb7e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,11 +41,13 @@ These variable exports can be added in a local `.envrc` file, read by [direnv](h ## Editor setup (optional) -`cargo fmt` is the source of truth and CI enforces it. We also have a: +`cargo +nightly fmt` is the source of truth and CI enforces it. A nightly version of Rust is required for formatting because Atuin's `.rustfmt.toml` uses nightly-only options. - [`.editorconfig`](https://editorconfig.org/) which sets sane defaults for `.rs` files. - [`.nvim.lua`](https://github.com/atuinsh/atuin/blob/main/.nvim.lua) which configures neovim. Note you need `vim.o.exrc = true` and `nvim >= 0.11+`. You can disable it if you prefer, please see the docs at the top of that file. +If you format via rust-analyzer (LSP integration in your editor), make sure it uses nightly Rustfmt by setting [`rust-analyzer.rustfmt.overrideCommand`](https://rust-analyzer.github.io/book/configuration.html#rustfmt.overrideCommand) to `["rustfmt", "+nightly"]`. + ## PRs It can speed up the review cycle if you consent to maintainers pushing to your branch. This will only be in the case of small fixes or adjustments, and not anything large. If you feel OK with this, please check the box on the template! diff --git a/crates/atuin-ai/src/commands/inline.rs b/crates/atuin-ai/src/commands/inline.rs index 18ae0126e..24d084a2b 100644 --- a/crates/atuin-ai/src/commands/inline.rs +++ b/crates/atuin-ai/src/commands/inline.rs @@ -1,11 +1,12 @@ +use atuin_client::database::{Database, Sqlite}; +use eyre::{Context as _, Result, bail}; +use tracing::{debug, info}; + use crate::context::{AppContext, ClientContext}; use crate::fsm::AgentFsm; use crate::session::{LocalSessionService, SessionManager, SessionService}; use crate::tui::app::{AiApp, ExitOutcome, IoContext}; use crate::tui::state::ConversationEvent; -use atuin_client::database::{Database, Sqlite}; -use eyre::{Context as _, Result, bail}; -use tracing::{debug, info}; pub(crate) async fn run( initial_command: Option, @@ -107,11 +108,13 @@ async fn ensure_hub_session(settings: &atuin_client::settings::Settings) -> Resu println!("Atuin AI requires authenticating with Atuin Hub."); if will_sync { println!( - "Once logged in, your shell history will be synchronized via Atuin Hub if auto_sync is enabled or when manually syncing." + "Once logged in, your shell history will be synchronized via Atuin Hub if auto_sync \ + is enabled or when manually syncing." ); } println!( - "If you have an existing Atuin sync account, you can log in with your existing credentials." + "If you have an existing Atuin sync account, you can log in with your existing \ + credentials." ); println!("Press enter to begin (or esc to cancel)."); if !wait_for_login_confirmation()? { @@ -186,20 +189,14 @@ async fn run_inline_tui( } }; - let cwd = std::env::current_dir() - .ok() - .map(|p| p.to_string_lossy().into_owned()); - let git_root_str = ctx - .git_root - .as_ref() - .map(|p| p.to_string_lossy().into_owned()); + let cwd = std::env::current_dir().ok().map(|p| p.to_string_lossy().into_owned()); + let git_root_str = ctx.git_root.as_ref().map(|p| p.to_string_lossy().into_owned()); let session_window_mins = settings.ai.session_continue_minutes.max(0); // treat negative values as 0 to avoid confusion let max_age_secs: i64 = session_window_mins * 60; - let resumable = service - .find_resumable(cwd.as_deref(), git_root_str.as_deref(), max_age_secs) - .await?; + let resumable = + service.find_resumable(cwd.as_deref(), git_root_str.as_deref(), max_age_secs).await?; // ─── Build FSM ─────────────────────────────────────────────── let (session_mgr, mut fsm, file_tracker, edit_permissions) = if let Some(stored) = resumable { @@ -211,8 +208,10 @@ async fn run_inline_tui( if has_api_content { events.push(ConversationEvent::SystemContext { - content: "[Note: The user has started a new invocation of Atuin AI. Prior messages from this session are from an earlier invocation.]".to_string(), - }); + content: "[Note: The user has started a new invocation of Atuin AI. Prior \ + messages from this session are from an earlier invocation.]" + .to_string(), + }); let view_start = events.len(); let last_time = last_event_ts.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)); @@ -225,9 +224,8 @@ async fn run_inline_tui( Default::default() }; - let ep = if let Ok(Some(json)) = mgr - .get_metadata(crate::edit_permissions::METADATA_KEY) - .await + let ep = if let Ok(Some(json)) = + mgr.get_metadata(crate::edit_permissions::METADATA_KEY).await && let Ok(cache) = crate::edit_permissions::EditPermissionCache::from_json(&json) { cache @@ -264,26 +262,16 @@ async fn run_inline_tui( // `ai.model` is read once at startup, so /model in another running // session doesn't retarget this one mid-conversation. - fsm.ctx.model = settings - .ai - .model - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(String::from); + fsm.ctx.model = + settings.ai.model.as_deref().map(str::trim).filter(|s| !s.is_empty()).map(String::from); // ─── Snapshot store ───────────────────────────────────────── - let snapshot_dir = atuin_common::utils::data_dir() - .join("ai") - .join("snapshots") - .join(session_mgr.session_id()); + let snapshot_dir = + atuin_common::utils::data_dir().join("ai").join("snapshots").join(session_mgr.session_id()); let snapshot_store = crate::snapshots::SnapshotStore::open(snapshot_dir).ok(); // ─── Discover skills ─────────────────────────────────────── - let project_root = ctx - .git_root - .clone() - .or_else(|| std::env::current_dir().ok()); + let project_root = ctx.git_root.clone().or_else(|| std::env::current_dir().ok()); let skill_registry = crate::skills::SkillRegistry::discover(project_root.as_deref()).await; // ─── Resume notice (frozen at startup) ────────────────────── @@ -291,7 +279,8 @@ async fn run_inline_tui( Some(t) => { let human = chrono_humanize::HumanTime::from(t - chrono::Utc::now()); format!( - " Continuing previous session (last active {human}) - type /new to start a new session" + " Continuing previous session (last active {human}) - type /new to start a new \ + session" ) } None => " Continuing previous session - type /new to start a new session".to_string(), @@ -301,10 +290,8 @@ async fn run_inline_tui( let mut slash_registry = crate::tui::slash::SlashCommandRegistry::default(); let mut skill_names = std::collections::HashSet::new(); for skill in skill_registry.all() { - slash_registry.register(crate::tui::slash::SlashCommand::new( - &skill.name, - &skill.description, - )); + slash_registry + .register(crate::tui::slash::SlashCommand::new(&skill.name, &skill.description)); skill_names.insert(skill.name.clone()); } @@ -338,9 +325,8 @@ async fn run_inline_tui( ); let options = eye_declare::RunOptions::default().keyboard(eye_declare::KeyboardProtocol::Enhanced); - let outcome = eye_declare::driver_tokio::run_with(app, options) - .await - .context("failed running AI TUI")?; + let outcome = + eye_declare::driver_tokio::run_with(app, options).await.context("failed running AI TUI")?; // The app (and with it the last persist sender) dropped when the run // loop returned; wait for the worker to drain its queue so the final @@ -361,11 +347,8 @@ enum SetupChoice { } fn prompt_ai_setup() -> Result { - use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyEventKind}, - terminal, - }; + use crossterm::event::{self, Event, KeyCode, KeyEventKind}; + use crossterm::{cursor, terminal}; let options = ["Enable Atuin AI", "Disable ? Keybind", "Cancel"]; let mut selected: usize = 0; @@ -433,10 +416,8 @@ fn render_setup_options( options: &[&str], selected: usize, ) -> Result<()> { - use crossterm::{ - style::Stylize, - terminal::{Clear, ClearType}, - }; + use crossterm::style::Stylize; + use crossterm::terminal::{Clear, ClearType}; for (i, option) in options.iter().enumerate() { if i == selected { @@ -477,10 +458,8 @@ async fn set_ai_enabled(enabled: bool) -> Result<()> { } fn wait_for_login_confirmation() -> Result { - use crossterm::{ - event::{self, Event, KeyCode}, - terminal::{disable_raw_mode, enable_raw_mode}, - }; + use crossterm::event::{self, Event, KeyCode}; + use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; enable_raw_mode().context("failed enabling raw mode for login prompt")?; struct Guard; diff --git a/crates/atuin-ai/src/context.rs b/crates/atuin-ai/src/context.rs index b1564fe9e..85d859276 100644 --- a/crates/atuin-ai/src/context.rs +++ b/crates/atuin-ai/src/context.rs @@ -58,12 +58,7 @@ impl AppContext { } caps.push("client_v1_load_skill".to_string()); if let Ok(extra) = std::env::var("ATUIN_AI__ADDITIONAL_CAPS") { - caps.extend( - extra - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()), - ); + caps.extend(extra.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty())); } caps } diff --git a/crates/atuin-ai/src/context_window.rs b/crates/atuin-ai/src/context_window.rs index c2f13b172..2a777f357 100644 --- a/crates/atuin-ai/src/context_window.rs +++ b/crates/atuin-ai/src/context_window.rs @@ -49,10 +49,8 @@ impl ContextWindowBuilder { // This is safe because the combining logic (Text + ToolCall merging) // only operates within a single assistant response, which never // spans turn boundaries. - let turn_messages: Vec> = turns - .iter() - .map(|range| events_to_messages(&events[range.clone()])) - .collect(); + let turn_messages: Vec> = + turns.iter().map(|range| events_to_messages(&events[range.clone()])).collect(); let turn_chars: Vec = turn_messages.iter().map(|m| estimate_chars(m)).collect(); let total_chars: usize = turn_chars.iter().sum(); @@ -154,9 +152,10 @@ fn estimate_chars(messages: &[serde_json::Value]) -> usize { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + fn user(content: &str) -> ConversationEvent { ConversationEvent::UserMessage { content: content.to_string(), @@ -342,10 +341,7 @@ mod tests { let messages = builder.build(&events); // Should have prefix (turn 1) + marker + last turn (turn 3) - assert!( - messages.len() >= 3, - "should have at least prefix + marker + tail" - ); + assert!(messages.len() >= 3, "should have at least prefix + marker + tail"); // First message should be from turn 1 assert_eq!(messages[0]["content"], "first"); @@ -393,9 +389,7 @@ mod tests { // Verify turn 2 (the tool call turn) was dropped let has_tool_use = messages.iter().any(|m| { - m["content"] - .as_array() - .is_some_and(|arr| arr.iter().any(|b| b["type"] == "tool_use")) + m["content"].as_array().is_some_and(|arr| arr.iter().any(|b| b["type"] == "tool_use")) }); assert!(!has_tool_use, "tool call turn should have been truncated"); @@ -427,10 +421,7 @@ mod tests { let marker_cost = estimate_chars(std::slice::from_ref(&truncation_marker())); let budget = total - turn2_chars + marker_cost + 5; - assert!( - budget < total, - "budget must be less than total for truncation to trigger" - ); + assert!(budget < total, "budget must be less than total for truncation to trigger"); let builder = ContextWindowBuilder::new(budget); let messages = builder.build(&events); @@ -438,43 +429,13 @@ mod tests { // Should have: prefix (t1: 2 msgs) + marker (1 msg) + t3 (2 msgs) + t4 (2 msgs) = 7 // (turn 2 dropped) assert_eq!(messages.len(), 7); - assert!( - messages[0]["content"] - .as_str() - .unwrap() - .starts_with("turn-1-user-") - ); - assert!( - messages[1]["content"] - .as_str() - .unwrap() - .starts_with("turn-1-response-") - ); + assert!(messages[0]["content"].as_str().unwrap().starts_with("turn-1-user-")); + assert!(messages[1]["content"].as_str().unwrap().starts_with("turn-1-response-")); assert!(messages[2]["content"].as_str().unwrap().contains("omitted")); - assert!( - messages[3]["content"] - .as_str() - .unwrap() - .starts_with("turn-3-user-") - ); - assert!( - messages[4]["content"] - .as_str() - .unwrap() - .starts_with("turn-3-response-") - ); - assert!( - messages[5]["content"] - .as_str() - .unwrap() - .starts_with("turn-4-user-") - ); - assert!( - messages[6]["content"] - .as_str() - .unwrap() - .starts_with("turn-4-response-") - ); + assert!(messages[3]["content"].as_str().unwrap().starts_with("turn-3-user-")); + assert!(messages[4]["content"].as_str().unwrap().starts_with("turn-3-response-")); + assert!(messages[5]["content"].as_str().unwrap().starts_with("turn-4-user-")); + assert!(messages[6]["content"].as_str().unwrap().starts_with("turn-4-response-")); } #[test] @@ -488,9 +449,7 @@ mod tests { // No truncation marker assert_eq!(messages.len(), 4); assert!( - !messages - .iter() - .any(|m| m["content"].as_str().is_some_and(|s| s.contains("omitted"))) + !messages.iter().any(|m| m["content"].as_str().is_some_and(|s| s.contains("omitted"))) ); } @@ -548,9 +507,6 @@ mod tests { ); // Turn 2 was dropped entirely, so no tool IDs should be present - assert!( - !tool_use_ids.contains(&"tc1"), - "dropped turn's tool_use should not appear" - ); + assert!(!tool_use_ids.contains(&"tc1"), "dropped turn's tool_use should not appear"); } } diff --git a/crates/atuin-ai/src/diff.rs b/crates/atuin-ai/src/diff.rs index 888adeec6..17f4ab76a 100644 --- a/crates/atuin-ai/src/diff.rs +++ b/crates/atuin-ai/src/diff.rs @@ -68,10 +68,7 @@ impl EditPreview { merged_groups.push(current_group); // Build structured hunks from merged groups - let hunks = merged_groups - .into_iter() - .map(|group| build_hunk(&group, &input)) - .collect(); + let hunks = merged_groups.into_iter().map(|group| build_hunk(&group, &input)).collect(); EditPreview { hunks } } @@ -121,11 +118,7 @@ impl WritePreview { pub fn from_content(content: &str) -> Self { let all_lines: Vec<&str> = content.lines().collect(); let total_lines = all_lines.len(); - let lines = all_lines - .into_iter() - .take(WRITE_PREVIEW_LINES) - .map(String::from) - .collect(); + let lines = all_lines.into_iter().take(WRITE_PREVIEW_LINES).map(String::from).collect(); WritePreview { lines, total_lines } } @@ -199,9 +192,10 @@ fn token_text(input: &InternedInput<&str>, is_before: bool, idx: u32) -> String #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[test] fn no_changes_produces_empty_preview() { let preview = EditPreview::compute("hello\nworld\n", "hello\nworld\n"); @@ -308,14 +302,8 @@ mod tests { assert_eq!(preview.hunks.len(), 1); let hunk = &preview.hunks[0]; - assert!( - hunk.lines - .contains(&DiffLine::Removed("key1 = old1".into())) - ); - assert!( - hunk.lines - .contains(&DiffLine::Removed("key2 = old2".into())) - ); + assert!(hunk.lines.contains(&DiffLine::Removed("key1 = old1".into()))); + assert!(hunk.lines.contains(&DiffLine::Removed("key2 = old2".into()))); assert!(hunk.lines.contains(&DiffLine::Added("key1 = new1".into()))); assert!(hunk.lines.contains(&DiffLine::Added("key2 = new2".into()))); } diff --git a/crates/atuin-ai/src/event_serde.rs b/crates/atuin-ai/src/event_serde.rs index cd6ec905e..899e622b2 100644 --- a/crates/atuin-ai/src/event_serde.rs +++ b/crates/atuin-ai/src/event_serde.rs @@ -13,14 +13,12 @@ use crate::tui::ConversationEvent; /// suitable for database storage. pub(crate) fn serialize_event(event: &ConversationEvent) -> (String, String) { match event { - ConversationEvent::UserMessage { content } => ( - "user_message".to_string(), - serde_json::json!({ "content": content }).to_string(), - ), - ConversationEvent::Text { content } => ( - "text".to_string(), - serde_json::json!({ "content": content }).to_string(), - ), + ConversationEvent::UserMessage { content } => { + ("user_message".to_string(), serde_json::json!({ "content": content }).to_string()) + } + ConversationEvent::Text { content } => { + ("text".to_string(), serde_json::json!({ "content": content }).to_string()) + } ConversationEvent::ToolCall { id, name, input } => ( "tool_call".to_string(), serde_json::json!({ @@ -60,10 +58,9 @@ pub(crate) fn serialize_event(event: &ConversationEvent) -> (String, String) { }) .to_string(), ), - ConversationEvent::SystemContext { content } => ( - "system_context".to_string(), - serde_json::json!({ "content": content }).to_string(), - ), + ConversationEvent::SystemContext { content } => { + ("system_context".to_string(), serde_json::json!({ "content": content }).to_string()) + } ConversationEvent::SkillInvocation { name, arguments, @@ -109,16 +106,19 @@ pub(crate) fn deserialize_event(event_type: &str, event_data: &str) -> Result Ok(ConversationEvent::OutOfBandOutput { name: json_string(&data, "name")?, command: data .get("command") - .and_then(|v| if v.is_null() { None } else { v.as_str() }) + .and_then(|v| { + if v.is_null() { + None + } else { + v.as_str() + } + }) .map(String::from), content: json_string(&data, "content")?, }), @@ -129,7 +129,13 @@ pub(crate) fn deserialize_event(event_type: &str, event_data: &str) -> Result Result { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + fn round_trip(event: &ConversationEvent) -> ConversationEvent { let (event_type, event_data) = serialize_event(event); deserialize_event(&event_type, &event_data).unwrap() diff --git a/crates/atuin-ai/src/file_tracker.rs b/crates/atuin-ai/src/file_tracker.rs index 321ad06e3..af46d11b1 100644 --- a/crates/atuin-ai/src/file_tracker.rs +++ b/crates/atuin-ai/src/file_tracker.rs @@ -52,13 +52,10 @@ impl FileReadTracker { let content_hash = hash_content(content); let mtime_ms = system_time_to_ms(mtime); - self.reads.insert( - path, - FileReadState { - content_hash, - mtime_ms, - }, - ); + self.reads.insert(path, FileReadState { + content_hash, + mtime_ms, + }); } /// Check whether a file is fresh (unchanged since last read). @@ -100,13 +97,10 @@ impl FileReadTracker { let content_hash = hash_content(new_content); let mtime_ms = system_time_to_ms(new_mtime); - self.reads.insert( - path.to_path_buf(), - FileReadState { - content_hash, - mtime_ms, - }, - ); + self.reads.insert(path.to_path_buf(), FileReadState { + content_hash, + mtime_ms, + }); } /// Serialize to JSON for session metadata persistence. @@ -121,9 +115,7 @@ impl FileReadTracker { } fn system_time_to_ms(t: SystemTime) -> i64 { - t.duration_since(SystemTime::UNIX_EPOCH) - .map(|d| d.as_millis() as i64) - .unwrap_or(0) + t.duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0) } fn hash_content(content: &[u8]) -> u64 { @@ -132,11 +124,13 @@ fn hash_content(content: &[u8]) -> u64 { #[cfg(test)] mod tests { - use super::*; - use rstest::*; use std::io::Write; + + use rstest::*; use tempfile::NamedTempFile; + use super::*; + #[fixture] fn recorded() -> (FileReadTracker, NamedTempFile) { let mut tracker = FileReadTracker::default(); @@ -154,20 +148,14 @@ mod tests { #[rstest] fn record_and_check_fresh(#[from(recorded)] (tracker, tmp): (FileReadTracker, NamedTempFile)) { - assert!(matches!( - tracker.check_freshness(tmp.path()).unwrap(), - FreshnessCheck::Fresh - )); + assert!(matches!(tracker.check_freshness(tmp.path()).unwrap(), FreshnessCheck::Fresh)); } #[test] fn check_not_read() { let tracker = FileReadTracker::default(); let path = PathBuf::from("/nonexistent/file.txt"); - assert!(matches!( - tracker.check_freshness(&path).unwrap(), - FreshnessCheck::NotRead - )); + assert!(matches!(tracker.check_freshness(&path).unwrap(), FreshnessCheck::NotRead)); } #[rstest] @@ -180,10 +168,7 @@ mod tests { // Modify the file std::fs::write(tmp.path(), "modified").unwrap(); - assert!(matches!( - tracker.check_freshness(tmp.path()).unwrap(), - FreshnessCheck::Stale - )); + assert!(matches!(tracker.check_freshness(tmp.path()).unwrap(), FreshnessCheck::Stale)); } #[rstest] @@ -196,29 +181,20 @@ mod tests { let new_mtime = std::fs::metadata(tmp.path()).unwrap().modified().unwrap(); tracker.update_after_edit(tmp.path(), new_content, new_mtime); - assert!(matches!( - tracker.check_freshness(tmp.path()).unwrap(), - FreshnessCheck::Fresh - )); + assert!(matches!(tracker.check_freshness(tmp.path()).unwrap(), FreshnessCheck::Fresh)); } #[test] fn roundtrip_json() { let mut tracker = FileReadTracker::default(); - tracker.reads.insert( - PathBuf::from("/some/file.toml"), - FileReadState { - content_hash: 12345, - mtime_ms: 1700000000000, - }, - ); + tracker.reads.insert(PathBuf::from("/some/file.toml"), FileReadState { + content_hash: 12345, + mtime_ms: 1700000000000, + }); let json = tracker.to_json().unwrap(); let restored = FileReadTracker::from_json(&json).unwrap(); assert_eq!(restored.reads.len(), 1); - assert_eq!( - restored.reads[&PathBuf::from("/some/file.toml")].content_hash, - 12345 - ); + assert_eq!(restored.reads[&PathBuf::from("/some/file.toml")].content_hash, 12345); } } diff --git a/crates/atuin-ai/src/fsm/effects.rs b/crates/atuin-ai/src/fsm/effects.rs index 3566b1aa9..37eabc35d 100644 --- a/crates/atuin-ai/src/fsm/effects.rs +++ b/crates/atuin-ai/src/fsm/effects.rs @@ -44,7 +44,9 @@ pub(crate) enum Effect { tool: ClientToolCall, }, /// Kill a running tool (send interrupt to shell command). - AbortTool { tool_id: String }, + AbortTool { + tool_id: String, + }, /// Look up the command string behind an atuin_output call's history id /// in the local history db, for display (the model only passes a UUID). /// Resolves via `Event::OutputCommandResolved`. @@ -70,9 +72,13 @@ pub(crate) enum Effect { disposition: RuleDisposition, }, /// Cache a session-scoped file permission grant. - CacheSessionGrant { path: PathBuf }, + CacheSessionGrant { + path: PathBuf, + }, /// Persist the selected model alias to `ai.model` in config.toml. - SaveModelSelection { alias: String }, + SaveModelSelection { + alias: String, + }, /// Archive current session and start fresh (IO only — state already updated by FSM). ArchiveSession, @@ -100,7 +106,9 @@ pub(crate) enum TimeoutKind { /// Dangerous command confirmation dialog auto-dismiss. Confirmation, /// Shell tool execution timeout — abort the tool if it's still running. - ToolExecution { tool_id: String }, + ToolExecution { + tool_id: String, + }, } /// What to do when exiting the TUI. diff --git a/crates/atuin-ai/src/fsm/mod.rs b/crates/atuin-ai/src/fsm/mod.rs index 28f2b8aea..f4642af61 100644 --- a/crates/atuin-ai/src/fsm/mod.rs +++ b/crates/atuin-ai/src/fsm/mod.rs @@ -15,15 +15,14 @@ mod tests; use std::collections::HashMap; +use effects::{Effect, ExitAction, PermissionTarget, TimeoutKind}; +use events::{Event, PermissionChoice, PermissionResponse}; use serde_json::Value; +use tools::{ToolManager, ToolState}; use crate::context_window::ContextWindowBuilder; use crate::tui::state::ConversationEvent; -use effects::{Effect, ExitAction, PermissionTarget, TimeoutKind}; -use events::{Event, PermissionChoice, PermissionResponse}; -use tools::{ToolManager, ToolState}; - // ============================================================================ // State // ============================================================================ @@ -45,7 +44,9 @@ pub(crate) enum AgentState { }, /// A conversation turn is in progress. - Turn { stream: StreamPhase }, + Turn { + stream: StreamPhase, + }, /// Unrecoverable error. User can retry or exit. Error(String), @@ -57,7 +58,9 @@ pub(crate) enum StreamPhase { /// Request sent, awaiting first stream frame. Connecting, /// Actively receiving streamed response. - Streaming { status: Option }, + Streaming { + status: Option, + }, /// Stream connection has ended (Done received). Done, } @@ -246,9 +249,7 @@ impl AgentFsm { // This event fires from the stream response headers, rather than having to wait until the end // of a turn when StreamDone arrives, which can be lost for cancelled turns. if let Event::SessionIdReceived(session_id) = &event { - self.ctx - .session_id - .get_or_insert_with(|| session_id.clone()); + self.ctx.session_id.get_or_insert_with(|| session_id.clone()); } match (&self.state, event) { @@ -329,10 +330,7 @@ impl AgentFsm { } (AgentState::Idle { .. }, Event::ConfirmationTimeout { timeout_id }) => { - if self - .state_confirmation() - .is_some_and(|c| c.timeout_id == timeout_id) - { + if self.state_confirmation().is_some_and(|c| c.timeout_id == timeout_id) { self.state = AgentState::Idle { confirmation: None }; } vec![] @@ -599,9 +597,7 @@ impl AgentFsm { // Clear any pending execution timeout for this tool self.ctx.tool_timeout_ids.retain(|_, tid| tid != id); } - ids.into_iter() - .map(|tool_id| Effect::AbortTool { tool_id }) - .collect() + ids.into_iter().map(|tool_id| Effect::AbortTool { tool_id }).collect() } ( @@ -647,7 +643,9 @@ impl AgentFsm { // Add context so the LLM knows what happened if !pending.is_empty() { self.ctx.events.push(ConversationEvent::SystemContext { - content: "The user cancelled the previous generation. Tool calls that were in progress have been aborted.".to_string(), + content: "The user cancelled the previous generation. Tool calls that \ + were in progress have been aborted." + .to_string(), }); } @@ -766,9 +764,7 @@ impl AgentFsm { fn start_turn(&mut self, msg: String) -> Vec { // A message submitted while the picker was loading dismisses it. self.ctx.model_picker = None; - self.ctx - .events - .push(ConversationEvent::UserMessage { content: msg }); + self.ctx.events.push(ConversationEvent::UserMessage { content: msg }); // Don't clear tools — completed tools persist for rendering history. // Tools are only cleared on /new (session reset). self.ctx.current_response.clear(); @@ -795,9 +791,7 @@ impl AgentFsm { let text = std::mem::take(&mut self.ctx.current_response); let trimmed = text.trim_start().to_string(); if !trimmed.is_empty() { - self.ctx - .events - .push(ConversationEvent::Text { content: trimmed }); + self.ctx.events.push(ConversationEvent::Text { content: trimmed }); } } @@ -853,7 +847,8 @@ impl AgentFsm { self.ctx.events.push(ConversationEvent::ToolResult { tool_use_id: id.clone(), content: format!( - "Tool not enabled: capability '{required_cap}' was not advertised by this client" + "Tool not enabled: capability '{required_cap}' was not advertised by this \ + client" ), is_error: true, remote: false, @@ -990,9 +985,7 @@ impl AgentFsm { PermissionChoice::AlwaysAllow => { tracked.state = ToolState::Executing; let tool = tracked.tool.clone(); - let scope = tool - .resolved_file_path() - .map(|p| p.to_string_lossy().to_string()); + let scope = tool.resolved_file_path().map(|p| p.to_string_lossy().to_string()); let rule = crate::permissions::rule::Rule { tool: tool.rule_name().to_string(), scope, @@ -1141,9 +1134,7 @@ impl AgentFsm { if let crate::tools::ClientToolCall::Shell(ref shell) = tool { let timeout_id = self.ctx.next_timeout_id(); - self.ctx - .tool_timeout_ids - .insert(timeout_id, tool_id.clone()); + self.ctx.tool_timeout_ids.insert(timeout_id, tool_id.clone()); effects.push(Effect::ScheduleTimeout { timeout_id, duration: std::time::Duration::from_secs(shell.timeout_secs), @@ -1158,12 +1149,9 @@ impl AgentFsm { /// If so, either continue the conversation or go Idle. fn check_turn_completion(&mut self) -> Vec { // Stream must be done - if !matches!( - self.state, - AgentState::Turn { - stream: StreamPhase::Done - } - ) { + if !matches!(self.state, AgentState::Turn { + stream: StreamPhase::Done + }) { return vec![]; } @@ -1212,10 +1200,7 @@ impl AgentFsm { /// Get the most recent suggested command from the conversation. /// Get the most recent command from the current invocation only. fn current_command(&self) -> Option { - self.current_invocation_events() - .rev() - .find_map(|e| e.as_command()) - .map(|s| s.to_string()) + self.current_invocation_events().rev().find_map(|e| e.as_command()).map(|s| s.to_string()) } /// Check if the most recent command is dangerous. @@ -1226,10 +1211,7 @@ impl AgentFsm { if let ConversationEvent::ToolCall { name, input, .. } = e && name == "suggest_command" { - let danger = input - .get("danger") - .and_then(|v| v.as_str()) - .unwrap_or("low"); + let danger = input.get("danger").and_then(|v| v.as_str()).unwrap_or("low"); Some(danger == "high" || danger == "medium" || danger == "med") } else { None diff --git a/crates/atuin-ai/src/fsm/tests.rs b/crates/atuin-ai/src/fsm/tests.rs index 374c75851..626264222 100644 --- a/crates/atuin-ai/src/fsm/tests.rs +++ b/crates/atuin-ai/src/fsm/tests.rs @@ -1,20 +1,16 @@ //! Pure FSM transition tests. No IO, no async. -use serde_json::json; - +use effects::{Effect, ExitAction}; +use events::{Event, PermissionChoice, PermissionResponse}; use rstest::{fixture, rstest}; +use serde_json::json; use super::tools::{InterruptReason, ToolPreviewData}; use super::*; -use effects::{Effect, ExitAction}; -use events::{Event, PermissionChoice, PermissionResponse}; #[fixture] fn new_fsm() -> AgentFsm { - AgentFsm::new( - vec!["client_v1_read_file".to_string()], - "test-inv".to_string(), - ) + AgentFsm::new(vec!["client_v1_read_file".to_string()], "test-inv".to_string()) } // ============================================================================ @@ -25,12 +21,9 @@ fn new_fsm() -> AgentFsm { fn user_submit_starts_turn(#[from(new_fsm)] mut fsm: AgentFsm) { let effects = fsm.handle(Event::UserSubmit("hello".into())); - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Connecting - } - )); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Connecting + })); assert_eq!(effects.len(), 1); assert!(matches!(effects[0], Effect::StartStream { .. })); // User message was pushed to events @@ -46,12 +39,9 @@ fn stream_started_transitions_to_streaming(#[from(new_fsm)] mut fsm: AgentFsm) { let effects = fsm.handle(Event::StreamStarted); - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Streaming { status: None } - } - )); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Streaming { status: None } + })); assert!(effects.is_empty()); } @@ -167,10 +157,7 @@ fn stream_tool_call_tracks_tool_and_emits_check_permission(#[from(new_fsm)] mut #[rstest] fn atuin_output_call_emits_command_lookup_and_stores_result() { - let mut fsm = AgentFsm::new( - vec!["client_v1_atuin_output".to_string()], - "test-inv".to_string(), - ); + let mut fsm = AgentFsm::new(vec!["client_v1_atuin_output".to_string()], "test-inv".to_string()); fsm.handle(Event::UserSubmit("show output".into())); fsm.handle(Event::StreamStarted); @@ -255,17 +242,10 @@ fn tool_done_after_stream_done_continues_conversation(#[from(new_fsm)] mut fsm: }); // Turn complete → continuation - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Connecting - } - )); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::StartStream { .. })) - ); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Connecting + })); + assert!(effects.iter().any(|e| matches!(e, Effect::StartStream { .. }))); } #[rstest] @@ -290,12 +270,9 @@ fn continuation_turn_without_new_tools_goes_idle(#[from(new_fsm)] mut fsm: Agent outcome: crate::tools::ToolOutcome::Success("contents".into()), preview: None, }); - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Connecting - } - )); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Connecting + })); // Continuation stream: text only, no new tools fsm.handle(Event::StreamStarted); @@ -307,11 +284,7 @@ fn continuation_turn_without_new_tools_goes_idle(#[from(new_fsm)] mut fsm: Agent // Should go Idle, NOT start another continuation assert_eq!(fsm.state, AgentState::Idle { confirmation: None }); assert!(effects.iter().any(|e| matches!(e, Effect::Persist))); - assert!( - !effects - .iter() - .any(|e| matches!(e, Effect::StartStream { .. })) - ); + assert!(!effects.iter().any(|e| matches!(e, Effect::StartStream { .. }))); } #[rstest] @@ -336,12 +309,9 @@ fn tool_done_before_stream_done_stays_in_turn(#[from(new_fsm)] mut fsm: AgentFsm }); // Still in Turn — stream phase is Streaming, not Done - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Streaming { .. } - } - )); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Streaming { .. } + })); assert!(effects.is_empty()); } @@ -418,11 +388,7 @@ fn cancel_during_turn_with_pending_tools(#[from(new_fsm)] mut fsm: AgentFsm) { let effects = fsm.handle(Event::Cancel); assert_eq!(fsm.state, AgentState::Idle { confirmation: None }); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::AbortTool { .. })) - ); + assert!(effects.iter().any(|e| matches!(e, Effect::AbortTool { .. }))); // Error ToolResult injected assert!(fsm.ctx.events.iter().any(|e| matches!( e, @@ -479,17 +445,10 @@ fn dangerous_command_enters_confirmation(#[from(new_fsm)] mut fsm: AgentFsm) { let effects = fsm.handle(Event::ExecuteCommand); - assert!(matches!( - fsm.state, - AgentState::Idle { - confirmation: Some(_) - } - )); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::ScheduleTimeout { .. })) - ); + assert!(matches!(fsm.state, AgentState::Idle { + confirmation: Some(_) + })); + assert!(effects.iter().any(|e| matches!(e, Effect::ScheduleTimeout { .. }))); } #[rstest] @@ -551,17 +510,10 @@ fn retry_from_error_starts_new_stream(#[from(new_fsm)] mut fsm: AgentFsm) { let effects = fsm.handle(Event::Retry); - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Connecting - } - )); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::StartStream { .. })) - ); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Connecting + })); + assert!(effects.iter().any(|e| matches!(e, Effect::StartStream { .. }))); } // ============================================================================ @@ -592,17 +544,10 @@ fn permission_deny_completes_turn_and_continues(#[from(new_fsm)] mut fsm: AgentF // Turn should complete since all tools resolved and stream is done // → continuation needed (there was a tool result to send back) - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Connecting - } - )); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::StartStream { .. })) - ); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Connecting + })); + assert!(effects.iter().any(|e| matches!(e, Effect::StartStream { .. }))); // Error result was injected assert!(fsm.ctx.events.iter().any(|e| matches!( e, @@ -642,17 +587,10 @@ fn unknown_tool_call_gets_error_result(#[from(new_fsm)] mut fsm: AgentFsm) { // turn like a completed tool: the continuation starts immediately so // the model sees the error and can retry. assert!(fsm.ctx.tools.get("t1").is_none()); - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Connecting - } - )); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::StartStream { .. })) - ); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Connecting + })); + assert!(effects.iter().any(|e| matches!(e, Effect::StartStream { .. }))); } #[rstest] @@ -679,12 +617,9 @@ fn unknown_tool_alongside_real_tool_waits_for_both(#[from(new_fsm)] mut fsm: Age // Still waiting on the real tool — the pre-answered unknown call must // not complete the turn early. - assert!(matches!( - fsm.state, - AgentState::Turn { - stream: StreamPhase::Done - } - )); + assert!(matches!(fsm.state, AgentState::Turn { + stream: StreamPhase::Done + })); let effects = fsm.handle(Event::ToolExecutionDone { tool_id: "t1".into(), @@ -693,11 +628,7 @@ fn unknown_tool_alongside_real_tool_waits_for_both(#[from(new_fsm)] mut fsm: Age }); // Both results now ride the same continuation. - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::StartStream { .. })) - ); + assert!(effects.iter().any(|e| matches!(e, Effect::StartStream { .. }))); } // ============================================================================ @@ -707,10 +638,7 @@ fn unknown_tool_alongside_real_tool_waits_for_both(#[from(new_fsm)] mut fsm: Age #[fixture] fn fsm_with_shell() -> AgentFsm { AgentFsm::new( - vec![ - "client_v1_read_file".to_string(), - "client_v1_execute_shell_command".to_string(), - ], + vec!["client_v1_read_file".to_string(), "client_v1_execute_shell_command".to_string()], "test-inv".to_string(), ) } @@ -740,11 +668,7 @@ fn shell_tool_schedules_execution_timeout(#[from(fsm_with_shell)] mut fsm: Agent }); // Should have ExecuteTool + ScheduleTimeout - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::ExecuteTool { .. })) - ); + assert!(effects.iter().any(|e| matches!(e, Effect::ExecuteTool { .. }))); assert!(effects.iter().any(|e| matches!( e, Effect::ScheduleTimeout { kind: effects::TimeoutKind::ToolExecution { tool_id }, .. } @@ -768,16 +692,8 @@ fn read_tool_does_not_schedule_timeout(#[from(new_fsm)] mut fsm: AgentFsm) { response: PermissionResponse::Allowed, }); - assert!( - effects - .iter() - .any(|e| matches!(e, Effect::ExecuteTool { .. })) - ); - assert!( - !effects - .iter() - .any(|e| matches!(e, Effect::ScheduleTimeout { .. })) - ); + assert!(effects.iter().any(|e| matches!(e, Effect::ExecuteTool { .. }))); + assert!(!effects.iter().any(|e| matches!(e, Effect::ScheduleTimeout { .. }))); assert!(fsm.ctx.tool_timeout_ids.is_empty()); } @@ -885,9 +801,7 @@ fn timeout_respects_llm_specified_duration(#[from(fsm_with_shell)] mut fsm: Agen response: PermissionResponse::Allowed, }); - let timeout_effect = effects - .iter() - .find(|e| matches!(e, Effect::ScheduleTimeout { .. })); + let timeout_effect = effects.iter().find(|e| matches!(e, Effect::ScheduleTimeout { .. })); assert!(matches!( timeout_effect, Some(Effect::ScheduleTimeout { duration, .. }) if *duration == std::time::Duration::from_secs(120) @@ -918,12 +832,7 @@ fn cancel_clears_timeout_mappings(#[from(fsm_with_shell)] mut fsm: AgentFsm) { "[Timed out after 60s]", Some("[Interrupted by user]") )] -#[case::user_interrupt( - Event::InterruptTools, - InterruptReason::User, - "[Interrupted by user]", - None -)] +#[case::user_interrupt(Event::InterruptTools, InterruptReason::User, "[Interrupted by user]", None)] fn interrupt_propagates_reason_to_preview_and_llm( #[from(fsm_with_shell)] mut fsm: AgentFsm, #[case] trigger: Event, diff --git a/crates/atuin-ai/src/fsm/tools.rs b/crates/atuin-ai/src/fsm/tools.rs index 6e9076dfb..eecaf048d 100644 --- a/crates/atuin-ai/src/fsm/tools.rs +++ b/crates/atuin-ai/src/fsm/tools.rs @@ -142,25 +142,17 @@ impl ToolManager { /// synchronously at receipt (unknown tool, missing capability) and never /// enter the execution lifecycle. pub fn all_resolved(&self, tool_ids: &[String]) -> bool { - tool_ids - .iter() - .all(|id| self.get(id).is_none_or(|t| t.is_resolved())) + tool_ids.iter().all(|id| self.get(id).is_none_or(|t| t.is_resolved())) } /// Find the first tool awaiting user permission. pub fn awaiting_permission(&self) -> Option<&TrackedTool> { - self.tools - .iter() - .find(|t| t.state == ToolState::AwaitingPermission) + self.tools.iter().find(|t| t.state == ToolState::AwaitingPermission) } /// Get IDs of all non-resolved tools (for cancel). pub fn pending_ids(&self) -> Vec { - self.tools - .iter() - .filter(|t| !t.is_resolved()) - .map(|t| t.id.clone()) - .collect() + self.tools.iter().filter(|t| !t.is_resolved()).map(|t| t.id.clone()).collect() } /// Get IDs of all currently executing tools (for interrupt/abort). diff --git a/crates/atuin-ai/src/history_format.rs b/crates/atuin-ai/src/history_format.rs index 8193d9f10..da92a618c 100644 --- a/crates/atuin-ai/src/history_format.rs +++ b/crates/atuin-ai/src/history_format.rs @@ -53,12 +53,7 @@ fn format_attribution(history: &History) -> String { } fn format_timestamp(history: &History, local_offset: UtcOffset) -> String { - history - .timestamp - .to_offset(local_offset) - .display() - .ymd_hms() - .to_string() + history.timestamp.to_offset(local_offset).display().ymd_hms().to_string() } fn format_duration(nanos: i64) -> String { @@ -66,12 +61,7 @@ fn format_duration(nanos: i64) -> String { return String::new(); } - format!( - ", {}", - std::time::Duration::saturating_from_nanos_i64(nanos) - .display() - .stopwatch() - ) + format!(", {}", std::time::Duration::saturating_from_nanos_i64(nanos).display().stopwatch()) } #[cfg(test)] @@ -103,7 +93,8 @@ mod tests { fn formats_last_command() { assert_eq!( format_last_command(&history(1_234_000_000), UtcOffset::UTC), - "History ID: 018f011c-9a0a-7000-8000-000000000001 - `cargo test`\n[1970-01-01 00:00:00] (in `/repo`, exit 2), 1.234s" + "History ID: 018f011c-9a0a-7000-8000-000000000001 - `cargo test`\n[1970-01-01 \ + 00:00:00] (in `/repo`, exit 2), 1.234s" ); } @@ -111,7 +102,8 @@ mod tests { fn formats_history_search_result() { assert_eq!( format_history_search_result(3, &history(0), UtcOffset::UTC), - "## #3. (History ID: 018f011c-9a0a-7000-8000-000000000001):\n`cargo test`\n[1970-01-01 00:00:00] (in `/repo`, exit 2)\n" + "## #3. (History ID: 018f011c-9a0a-7000-8000-000000000001):\n`cargo \ + test`\n[1970-01-01 00:00:00] (in `/repo`, exit 2)\n" ); } diff --git a/crates/atuin-ai/src/mcp.rs b/crates/atuin-ai/src/mcp.rs index 3607cb01d..52a0ae72e 100644 --- a/crates/atuin-ai/src/mcp.rs +++ b/crates/atuin-ai/src/mcp.rs @@ -59,10 +59,7 @@ impl ServerHandler for AtuinMcp { .await } name => { - return Err(ErrorData::invalid_params( - format!("unknown tool: {name}"), - None, - )); + return Err(ErrorData::invalid_params(format!("unknown tool: {name}"), None)); } }; @@ -180,21 +177,19 @@ fn tool_definitions() -> Vec { vec![ Tool::new( "atuin_history", - "Search the user's shell command history, recorded by Atuin. \ - Fuzzy-matches the query against past commands and returns the most relevant \ - entries, each with a history ID, timestamp, working directory, exit code, \ - and duration. Commands run by AI agents are annotated with the agent's name \ - and stated intent. Pass a history ID to atuin_output to see what a command \ - printed.", + "Search the user's shell command history, recorded by Atuin. Fuzzy-matches the query \ + against past commands and returns the most relevant entries, each with a history ID, \ + timestamp, working directory, exit code, and duration. Commands run by AI agents are \ + annotated with the agent's name and stated intent. Pass a history ID to atuin_output \ + to see what a command printed.", history_schema, ) .annotate(ToolAnnotations::new().read_only(true)), Tool::new( "atuin_output", - "Fetch the captured terminal output of a previously executed \ - command, identified by a history ID from atuin_history results. Output \ - capture requires the Atuin daemon; output is only available for recent \ - commands captured while the daemon was running.", + "Fetch the captured terminal output of a previously executed command, identified by a \ + history ID from atuin_history results. Output capture requires the Atuin daemon; \ + output is only available for recent commands captured while the daemon was running.", output_schema, ) .annotate(ToolAnnotations::new().read_only(true)), @@ -212,10 +207,7 @@ mod tests { assert_eq!(names, ["atuin_history", "atuin_output"]); for tool in &tools { - assert_eq!( - tool.annotations.as_ref().unwrap().read_only_hint, - Some(true) - ); + assert_eq!(tool.annotations.as_ref().unwrap().read_only_hint, Some(true)); assert!(tool.input_schema.contains_key("required")); } } diff --git a/crates/atuin-ai/src/models.rs b/crates/atuin-ai/src/models.rs index b77710f5c..0051b7408 100644 --- a/crates/atuin-ai/src/models.rs +++ b/crates/atuin-ai/src/models.rs @@ -44,10 +44,7 @@ pub(crate) async fn fetch_models(endpoint: &reqwest::Url, token: &str) -> Result eyre::bail!("model list request failed ({status})"); } - response - .json::() - .await - .context("failed to parse model list") + response.json::().await.context("failed to parse model list") } /// Persist the chosen alias to `ai.model` in config.toml so it becomes the @@ -55,9 +52,7 @@ pub(crate) async fn fetch_models(endpoint: &reqwest::Url, token: &str) -> Result /// read at startup. pub(crate) async fn save_model_selection(alias: &str) -> Result<()> { let config_file = atuin_client::settings::Settings::get_config_path()?; - let config_str = tokio::fs::read_to_string(&config_file) - .await - .unwrap_or_default(); + let config_str = tokio::fs::read_to_string(&config_file).await.unwrap_or_default(); let mut doc = config_str.parse::()?; if !doc.contains_key("ai") { diff --git a/crates/atuin-ai/src/permissions/check.rs b/crates/atuin-ai/src/permissions/check.rs index bb1eae0c8..78c448e45 100644 --- a/crates/atuin-ai/src/permissions/check.rs +++ b/crates/atuin-ai/src/permissions/check.rs @@ -1,6 +1,7 @@ use eyre::Result; -use crate::{permissions::file::RuleFile, tools::PermissibleToolCall}; +use crate::permissions::file::RuleFile; +use crate::tools::PermissibleToolCall; pub(crate) struct PermissionRequest<'t> { call: &'t (dyn PermissibleToolCall + Send + Sync), @@ -58,10 +59,7 @@ impl PermissionChecker { } if request.call.all_covered_by(&file.content.permissions.allow) { - tracing::debug!( - "Permission 'ALLOW' by rules in file: {}", - file.path.display() - ); + tracing::debug!("Permission 'ALLOW' by rules in file: {}", file.path.display()); return Ok(PermissionResponse::Allowed); } } diff --git a/crates/atuin-ai/src/permissions/rule.rs b/crates/atuin-ai/src/permissions/rule.rs index 2133d64d9..7570f5d7c 100644 --- a/crates/atuin-ai/src/permissions/rule.rs +++ b/crates/atuin-ai/src/permissions/rule.rs @@ -50,9 +50,7 @@ impl TryFrom<&str> for Rule { fn try_from(value: &str) -> Result { let value = value.trim(); let re = RULE_RE.get_or_init(|| Regex::new(r"^(\w+)(?:\((.*)\))?$").unwrap()); - let caps = re - .captures(value) - .ok_or(RuleError::InvalidRule(value.to_string()))?; + let caps = re.captures(value).ok_or(RuleError::InvalidRule(value.to_string()))?; let tool = caps.get(1).unwrap().as_str().to_string(); let scope = caps.get(2).map(|m| m.as_str().to_string()); Ok(Rule { tool, scope }) @@ -61,9 +59,10 @@ impl TryFrom<&str> for Rule { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::bare_tool("Read", "Read", None)] #[case::wildcard_scope("Read(*)", "Read", Some("*"))] @@ -71,13 +70,10 @@ mod tests { #[case::shell_with_space("Shell(git commit *)", "Shell", Some("git commit *"))] #[case::nested_parens("Shell(echo ())", "Shell", Some("echo ()"))] fn parses_valid_rule(#[case] input: &str, #[case] tool: &str, #[case] scope: Option<&str>) { - assert_eq!( - Rule::try_from(input).unwrap(), - Rule { - tool: tool.to_string(), - scope: scope.map(String::from), - } - ); + assert_eq!(Rule::try_from(input).unwrap(), Rule { + tool: tool.to_string(), + scope: scope.map(String::from), + }); } #[rstest] diff --git a/crates/atuin-ai/src/permissions/shell.rs b/crates/atuin-ai/src/permissions/shell.rs index fc80b14db..8231dab7a 100644 --- a/crates/atuin-ai/src/permissions/shell.rs +++ b/crates/atuin-ai/src/permissions/shell.rs @@ -58,9 +58,10 @@ pub(crate) fn parse_shell_command(code: &str, shell: ShellKind) -> ParsedShellCo #[cfg(feature = "tree-sitter")] mod ts { - use super::{ParsedShellCommand, ShellCommand, parse_fallback}; use tree_sitter::{Parser, Tree}; + use super::{ParsedShellCommand, ShellCommand, parse_fallback}; + fn bash_parser() -> Parser { let mut parser = Parser::new(); parser @@ -177,9 +178,7 @@ mod ts { fn fish_parser() -> Parser { let mut parser = Parser::new(); - parser - .set_language(&tree_sitter_fish::language()) - .expect("failed to set fish language"); + parser.set_language(&tree_sitter_fish::language()).expect("failed to set fish language"); parser } @@ -456,9 +455,10 @@ fn find_subslice(haystack: &[&str], needle: &[&str]) -> Option { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + fn names(cmds: &[ShellCommand]) -> Vec<&str> { cmds.iter().map(|c| c.name.as_str()).collect() } @@ -564,9 +564,10 @@ mod tests { #[cfg(all(test, feature = "tree-sitter"))] mod adversarial { - use super::*; use rstest::rstest; + use super::*; + fn cmd_names(cmds: &[ShellCommand]) -> Vec<&str> { cmds.iter().map(|c| c.name.as_str()).collect() } diff --git a/crates/atuin-ai/src/permissions/walker.rs b/crates/atuin-ai/src/permissions/walker.rs index 3bda01c3c..e038b1157 100644 --- a/crates/atuin-ai/src/permissions/walker.rs +++ b/crates/atuin-ai/src/permissions/walker.rs @@ -112,10 +112,7 @@ async fn load_permissions_file(file_path: &Path) -> Result> { let content: RuleFileContent = toml::from_str(&raw)?; // Use the file's parent as the rule file path (for logging/debugging) - let path = file_path - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(|| file_path.to_path_buf()); + let path = file_path.parent().map(Path::to_path_buf).unwrap_or_else(|| file_path.to_path_buf()); Ok(Some(RuleFile { path, content })) } diff --git a/crates/atuin-ai/src/permissions/writer.rs b/crates/atuin-ai/src/permissions/writer.rs index 9049963ad..0602e3f26 100644 --- a/crates/atuin-ai/src/permissions/writer.rs +++ b/crates/atuin-ai/src/permissions/writer.rs @@ -89,9 +89,10 @@ pub(crate) fn global_permissions_path() -> std::path::PathBuf { #[cfg(test)] mod tests { - use super::*; use rstest::*; + use super::*; + #[fixture] fn perm_file() -> (tempfile::TempDir, std::path::PathBuf) { let dir = tempfile::tempdir().unwrap(); @@ -152,9 +153,7 @@ allow = ["AtuinHistory"] tool: "AtuinHistory".to_string(), scope: None, }; - write_rule(&file, &rule, RuleDisposition::Allow) - .await - .unwrap(); + write_rule(&file, &rule, RuleDisposition::Allow).await.unwrap(); let content = tokio::fs::read_to_string(&file).await.unwrap(); // Should appear exactly once diff --git a/crates/atuin-ai/src/session.rs b/crates/atuin-ai/src/session.rs index e9b2c02fd..bdfb56fcb 100644 --- a/crates/atuin-ai/src/session.rs +++ b/crates/atuin-ai/src/session.rs @@ -99,9 +99,7 @@ impl SessionService for LocalSessionService { git_root: Option<&str>, max_age_secs: i64, ) -> Result> { - self.store - .find_resumable_session(directory, git_root, max_age_secs) - .await + self.store.find_resumable_session(directory, git_root, max_age_secs).await } async fn load_events(&self, session_id: &str) -> Result> { @@ -118,14 +116,7 @@ impl SessionService for LocalSessionService { event_data: &str, ) -> Result<()> { self.store - .append_event( - session_id, - event_id, - parent_id, - invocation_id, - event_type, - event_data, - ) + .append_event(session_id, event_id, parent_id, invocation_id, event_type, event_data) .await } @@ -134,9 +125,7 @@ impl SessionService for LocalSessionService { session_id: &str, server_session_id: &str, ) -> Result<()> { - self.store - .update_server_session_id(session_id, server_session_id) - .await + self.store.update_server_session_id(session_id, server_session_id).await } async fn archive(&self, session_id: &str) -> Result<()> { @@ -215,13 +204,7 @@ impl SessionManager { pub async fn resume( service: Box, stored: &StoredSession, - ) -> Result<( - Self, - Vec, - Option, - Option, - String, - )> { + ) -> Result<(Self, Vec, Option, Option, String)> { let invocation_id = atuin_common::utils::uuid_v7().to_string(); let stored_events = service.load_events(&stored.id).await?; @@ -229,10 +212,7 @@ impl SessionManager { let mut last_event_id = None; let mut last_event_ts = None; for se in &stored_events { - events.push(event_serde::deserialize_event( - &se.event_type, - &se.event_data, - )?); + events.push(event_serde::deserialize_event(&se.event_type, &se.event_data)?); last_event_id = Some(se.id.clone()); last_event_ts = Some(se.created_at); } @@ -248,13 +228,7 @@ impl SessionManager { persisted_to_db: true, }; - Ok(( - manager, - events, - stored.server_session_id.clone(), - last_event_ts, - invocation_id, - )) + Ok((manager, events, stored.server_session_id.clone(), last_event_ts, invocation_id)) } /// Ensure the session row exists in the database. @@ -302,9 +276,7 @@ impl SessionManager { /// Persist the server session ID if it has changed. pub async fn persist_server_session_id(&mut self, server_session_id: &str) -> Result<()> { self.ensure_persisted().await?; - self.service - .update_server_session_id(&self.session_id, server_session_id) - .await + self.service.update_server_session_id(&self.session_id, server_session_id).await } /// Archive the current session (for `/new` command). @@ -352,9 +324,7 @@ impl SessionManager { /// Write a metadata value for the current session. pub async fn set_metadata(&mut self, key: &str, value: &str) -> Result<()> { self.ensure_persisted().await?; - self.service - .set_metadata(&self.session_id, key, value) - .await + self.service.set_metadata(&self.session_id, key, value).await } /// Write the usage cache for a user key. Not tied to the current @@ -366,16 +336,13 @@ impl SessionManager { #[cfg(test)] mod tests { - use super::*; use rstest::*; + use super::*; + #[fixture] async fn service() -> Box { - Box::new( - LocalSessionService::open("sqlite::memory:", 2.0) - .await - .unwrap(), - ) + Box::new(LocalSessionService::open("sqlite::memory:", 2.0).await.unwrap()) } #[rstest] @@ -406,9 +373,7 @@ mod tests { let svc = service.await; let session_id = atuin_common::utils::uuid_v7().to_string(); - svc.create_session(&session_id, Some("/project"), Some("/project")) - .await - .unwrap(); + svc.create_session(&session_id, Some("/project"), Some("/project")).await.unwrap(); let events = vec![ ConversationEvent::UserMessage { @@ -436,9 +401,7 @@ mod tests { parent = Some(eid); } - svc.update_server_session_id(&session_id, "srv-abc") - .await - .unwrap(); + svc.update_server_session_id(&session_id, "srv-abc").await.unwrap(); // Now find and resume the session with a fresh service connection let stored = svc diff --git a/crates/atuin-ai/src/shell.rs b/crates/atuin-ai/src/shell.rs index faa1db89b..3856e2193 100644 --- a/crates/atuin-ai/src/shell.rs +++ b/crates/atuin-ai/src/shell.rs @@ -165,9 +165,10 @@ bind "?" _atuin_ai_question_mark #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::zsh( ZSH_INIT, diff --git a/crates/atuin-ai/src/skills/frontmatter.rs b/crates/atuin-ai/src/skills/frontmatter.rs index 14e73b928..2318e977c 100644 --- a/crates/atuin-ai/src/skills/frontmatter.rs +++ b/crates/atuin-ai/src/skills/frontmatter.rs @@ -61,11 +61,8 @@ fn split_frontmatter(content: &str) -> Option<(&str, String)> { let after_open = after_open.strip_prefix('\n').unwrap_or(after_open); // Find the closing `---` - let close_pos = after_open - .lines() - .enumerate() - .find(|(_, line)| line.trim() == "---") - .map(|(i, _)| { + let close_pos = + after_open.lines().enumerate().find(|(_, line)| line.trim() == "---").map(|(i, _)| { after_open .lines() .take(i) @@ -76,10 +73,7 @@ fn split_frontmatter(content: &str) -> Option<(&str, String)> { let yaml_str = &after_open[..close_pos]; let rest = &after_open[close_pos..]; // Skip the closing `---` line - let body = rest - .strip_prefix("---") - .unwrap_or(rest) - .trim_start_matches(|c: char| c != '\n'); + let body = rest.strip_prefix("---").unwrap_or(rest).trim_start_matches(|c: char| c != '\n'); let body = body.strip_prefix('\n').unwrap_or(body); Some((yaml_str, body.to_string())) @@ -113,9 +107,10 @@ fn extract_fields(doc: &yaml_rust2::Yaml) -> Frontmatter { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::basic( "\ @@ -189,10 +184,7 @@ Body. let parsed = parse(content); assert_eq!(parsed.frontmatter.name.as_deref(), expected_name); assert_eq!(parsed.frontmatter.description.as_deref(), expected_desc); - assert_eq!( - parsed.frontmatter.disable_model_invocation, - expected_disable - ); + assert_eq!(parsed.frontmatter.disable_model_invocation, expected_disable); if let Some(b) = expected_body_trimmed { assert_eq!(parsed.body.trim(), b); } diff --git a/crates/atuin-ai/src/skills/mod.rs b/crates/atuin-ai/src/skills/mod.rs index 0ee78ccdf..a756aea87 100644 --- a/crates/atuin-ai/src/skills/mod.rs +++ b/crates/atuin-ai/src/skills/mod.rs @@ -9,9 +9,8 @@ pub(crate) mod walker; use std::path::Path; -use atuin_common::string::EllipsizeExt as _; -use atuin_common::string::Measure; use atuin_common::string::ellipsis::{Indicator, Pos}; +use atuin_common::string::{EllipsizeExt as _, Measure}; use eyre::{Result, eyre}; use crate::user_context::interpolate; @@ -78,10 +77,8 @@ impl SkillRegistry { continue; } - let description = fm - .description - .or_else(|| first_paragraph(&parsed.body)) - .unwrap_or_default(); + let description = + fm.description.or_else(|| first_paragraph(&parsed.body)).unwrap_or_default(); skills.push(SkillDescriptor { name, @@ -124,11 +121,8 @@ impl SkillRegistry { } pub fn server_skills_with_budget(&self, budget: usize) -> (Vec, Option) { - let eligible: Vec<&SkillDescriptor> = self - .skills - .iter() - .filter(|s| !s.disable_model_invocation) - .collect(); + let eligible: Vec<&SkillDescriptor> = + self.skills.iter().filter(|s| !s.disable_model_invocation).collect(); let mut summaries = Vec::new(); let mut used = 0; @@ -139,11 +133,7 @@ impl SkillRegistry { // packed into a byte-bounded LLM prompt budget. let truncated_desc = skill .description - .ellipsize( - Measure::Bytes(MAX_DESCRIPTION_LEN), - Pos::End, - Indicator::ASCII, - ) + .ellipsize(Measure::Bytes(MAX_DESCRIPTION_LEN), Pos::End, Indicator::ASCII) .to_string(); let entry_size = skill.name.len() + truncated_desc.len() + PER_ENTRY_OVERHEAD; @@ -179,9 +169,7 @@ impl SkillRegistry { /// shell interpolation runs. If `$ARGUMENTS` does not appear in the body /// and arguments were provided, they are appended as `ARGUMENTS: `. pub async fn load(&self, name: &str, shell: &str, arguments: Option<&str>) -> Result { - let skill = self - .get(name) - .ok_or_else(|| eyre!("Unknown skill: {name}"))?; + let skill = self.get(name).ok_or_else(|| eyre!("Unknown skill: {name}"))?; let content = tokio::fs::read_to_string(&skill.source_path).await?; let parsed = frontmatter::parse(&content); @@ -237,21 +225,23 @@ fn first_paragraph(body: &str) -> Option { return None; } - let para: String = trimmed - .lines() - .take_while(|line| !line.trim().is_empty()) - .collect::>() - .join(" "); + let para: String = + trimmed.lines().take_while(|line| !line.trim().is_empty()).collect::>().join(" "); let para = para.trim().to_string(); - if para.is_empty() { None } else { Some(para) } + if para.is_empty() { + None + } else { + Some(para) + } } #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + /// Build a `SkillDescriptor` from its distinguishing fields, deriving the /// source path from the name. fn descriptor(name: &str, description: &str, disabled: bool) -> SkillDescriptor { @@ -272,10 +262,7 @@ mod tests { } #[rstest] - #[case( - "Hello world\nSecond line\n\nNew paragraph", - Some("Hello world Second line") - )] + #[case("Hello world\nSecond line\n\nNew paragraph", Some("Hello world Second line"))] #[case("", None)] #[case("\n\n", None)] #[case("Single line", Some("Single line"))] @@ -397,7 +384,8 @@ mod tests { std::fs::create_dir_all(&skill_dir2).unwrap(); std::fs::write( skill_dir2.join("SKILL.md"), - "---\nname: release\ndescription: >\n Multi-line\n description here.\n---\n\nRelease steps.\n", + "---\nname: release\ndescription: >\n Multi-line\n description \ + here.\n---\n\nRelease steps.\n", ) .unwrap(); diff --git a/crates/atuin-ai/src/skills/walker.rs b/crates/atuin-ai/src/skills/walker.rs index b93845f91..b6ecd20dd 100644 --- a/crates/atuin-ai/src/skills/walker.rs +++ b/crates/atuin-ai/src/skills/walker.rs @@ -74,11 +74,8 @@ async fn scan_dir(dir: &Path, is_project: bool, out: &mut Vec) { // Check for SKILL.md directly in this directory let skill_path = path.join(SKILL_FILENAME); if skill_path.is_file() { - let dir_name = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); + let dir_name = + path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown").to_string(); match tokio::fs::read_to_string(&skill_path).await { Ok(content) => { @@ -168,11 +165,7 @@ mod tests { #[tokio::test] async fn missing_directories_handled() { - let files = discover( - Some(Path::new("/does/not/exist")), - Path::new("/also/missing"), - ) - .await; + let files = discover(Some(Path::new("/does/not/exist")), Path::new("/also/missing")).await; assert!(files.is_empty()); } } diff --git a/crates/atuin-ai/src/snapshots.rs b/crates/atuin-ai/src/snapshots.rs index 8fe573409..d2db56b49 100644 --- a/crates/atuin-ai/src/snapshots.rs +++ b/crates/atuin-ai/src/snapshots.rs @@ -129,9 +129,7 @@ pub(crate) fn sanitize_path(path: &Path) -> String { &s } }); - s.replace('%', "%25") - .replace('/', "%2F") - .replace('\\', "%5C") + s.replace('%', "%25").replace('/', "%2F").replace('\\', "%5C") } /// Write a file atomically using temp-file-then-rename. @@ -140,9 +138,7 @@ pub(crate) fn sanitize_path(path: &Path) -> String { /// content, fsyncs, then renames into place. Preserves permissions from /// the original file if it exists. pub(crate) fn atomic_write_file(target: &Path, content: &[u8]) -> Result<()> { - let dir = target - .parent() - .ok_or_else(|| eyre!("target path has no parent directory"))?; + let dir = target.parent().ok_or_else(|| eyre!("target path has no parent directory"))?; fs_err::create_dir_all(dir)?; let mut tmp = tempfile::NamedTempFile::new_in(dir)?; @@ -154,13 +150,8 @@ pub(crate) fn atomic_write_file(target: &Path, content: &[u8]) -> Result<()> { std::fs::set_permissions(tmp.path(), meta.permissions())?; } - tmp.persist(target).map_err(|e| { - eyre!( - "failed to persist atomic write to {}: {}", - target.display(), - e - ) - })?; + tmp.persist(target) + .map_err(|e| eyre!("failed to persist atomic write to {}: {}", target.display(), e))?; Ok(()) } @@ -178,9 +169,10 @@ fn format_iso8601(dt: OffsetDateTime) -> String { #[cfg(test)] mod tests { - use super::*; use rstest::*; + use super::*; + // ── sanitize_path ────────────────────────────────────────── #[rstest] @@ -299,9 +291,7 @@ mod tests { ), ) { let file_path = Path::new("/Users/me/.config/foo.toml"); - let created = store - .ensure_snapshot(file_path, b"[key]\nval = 1\n") - .unwrap(); + let created = store.ensure_snapshot(file_path, b"[key]\nval = 1\n").unwrap(); assert!(created); assert!(store.has_snapshot(file_path)); @@ -309,10 +299,7 @@ mod tests { // Snapshot file on disk let expected_file = session_dir.join("Users%2Fme%2F.config%2Ffoo.toml"); assert!(expected_file.exists()); - assert_eq!( - std::fs::read_to_string(&expected_file).unwrap(), - "[key]\nval = 1\n" - ); + assert_eq!(std::fs::read_to_string(&expected_file).unwrap(), "[key]\nval = 1\n"); // Manifest on disk let manifest_path = session_dir.join("manifest.json"); @@ -322,10 +309,7 @@ mod tests { let files = manifest["files"].as_object().unwrap(); assert_eq!(files.len(), 1); let entry = &files["Users%2Fme%2F.config%2Ffoo.toml"]; - assert_eq!( - entry["original_path"].as_str().unwrap(), - "/Users/me/.config/foo.toml" - ); + assert_eq!(entry["original_path"].as_str().unwrap(), "/Users/me/.config/foo.toml"); assert_eq!(entry["size_bytes"].as_u64().unwrap(), 14); } @@ -346,10 +330,7 @@ mod tests { // Original content preserved, not overwritten let snapshot_file = session_dir.join("etc%2Fhosts"); - assert_eq!( - std::fs::read_to_string(snapshot_file).unwrap(), - "first content" - ); + assert_eq!(std::fs::read_to_string(snapshot_file).unwrap(), "first content"); } #[test] @@ -360,9 +341,7 @@ mod tests { // First store: create a snapshot { let mut store = SnapshotStore::open(session_dir.clone()).unwrap(); - store - .ensure_snapshot(Path::new("/etc/hosts"), b"127.0.0.1") - .unwrap(); + store.ensure_snapshot(Path::new("/etc/hosts"), b"127.0.0.1").unwrap(); } // Second store (simulates new CLI invocation): should see existing snapshot @@ -370,9 +349,7 @@ mod tests { let mut store = SnapshotStore::open(session_dir).unwrap(); assert!(store.has_snapshot(Path::new("/etc/hosts"))); - let created = store - .ensure_snapshot(Path::new("/etc/hosts"), b"new content") - .unwrap(); + let created = store.ensure_snapshot(Path::new("/etc/hosts"), b"new content").unwrap(); assert!(!created); } } @@ -385,12 +362,8 @@ mod tests { SnapshotStore, ), ) { - store - .ensure_snapshot(Path::new("/etc/hosts"), b"hosts content") - .unwrap(); - store - .ensure_snapshot(Path::new("/Users/me/.bashrc"), b"bashrc content") - .unwrap(); + store.ensure_snapshot(Path::new("/etc/hosts"), b"hosts content").unwrap(); + store.ensure_snapshot(Path::new("/Users/me/.bashrc"), b"bashrc content").unwrap(); assert!(store.has_snapshot(Path::new("/etc/hosts"))); assert!(store.has_snapshot(Path::new("/Users/me/.bashrc"))); diff --git a/crates/atuin-ai/src/store.rs b/crates/atuin-ai/src/store.rs index 09e46bfdb..31beaa2aa 100644 --- a/crates/atuin-ai/src/store.rs +++ b/crates/atuin-ai/src/store.rs @@ -6,7 +6,8 @@ use eyre::{Result, eyre}; use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions}; use time::OffsetDateTime; -use crate::{session::CachedUsageSnapshot, usage::UsageSnapshot}; +use crate::session::CachedUsageSnapshot; +use crate::usage::UsageSnapshot; // Database row mappings — all columns are kept even if not yet read in // non-test code, since they're part of the schema and used in tests. @@ -36,16 +37,8 @@ pub(crate) struct StoredEvent { } /// Row type returned by session queries (avoids clippy::type_complexity). -type SessionRow = ( - String, - Option, - Option, - Option, - Option, - i64, - i64, - Option, -); +type SessionRow = + (String, Option, Option, Option, Option, i64, i64, Option); /// Row type returned by event queries. type EventRow = (String, String, Option, String, String, String, i64); @@ -225,7 +218,8 @@ impl AiSessionStore { let mut tx = self.pool.begin().await?; sqlx::query( - "INSERT INTO session_events (id, session_id, parent_id, invocation_id, event_type, event_data, created_at) + "INSERT INTO session_events (id, session_id, parent_id, invocation_id, event_type, \ + event_data, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", ) .bind(event_id) @@ -376,9 +370,10 @@ impl AiSessionStore { #[cfg(test)] mod tests { + use rstest::*; + use super::*; use crate::usage::UsageSnapshot; - use rstest::*; #[fixture] async fn store() -> AiSessionStore { @@ -388,10 +383,7 @@ mod tests { #[fixture] async fn store_with_s1(#[future] store: AiSessionStore) -> AiSessionStore { let store = store.await; - store - .create_session("s1", Some("/tmp"), None) - .await - .unwrap(); + store.create_session("s1", Some("/tmp"), None).await.unwrap(); store } @@ -426,25 +418,11 @@ mod tests { let store = store_with_s1.await; store - .append_event( - "s1", - "e1", - None, - "inv1", - "user_message", - r#"{"content":"hello"}"#, - ) + .append_event("s1", "e1", None, "inv1", "user_message", r#"{"content":"hello"}"#) .await .unwrap(); store - .append_event( - "s1", - "e2", - Some("e1"), - "inv1", - "text", - r#"{"content":"hi there"}"#, - ) + .append_event("s1", "e2", Some("e1"), "inv1", "text", r#"{"content":"hi there"}"#) .await .unwrap(); @@ -464,15 +442,10 @@ mod tests { #[tokio::test] async fn test_find_resumable_session(#[future] store: AiSessionStore) { let store = store.await; - store - .create_session("s1", Some("/home/user/project"), None) - .await - .unwrap(); + store.create_session("s1", Some("/home/user/project"), None).await.unwrap(); - let found = store - .find_resumable_session(Some("/home/user/project"), None, 3600) - .await - .unwrap(); + let found = + store.find_resumable_session(Some("/home/user/project"), None, 3600).await.unwrap(); assert!(found.is_some()); assert_eq!(found.unwrap().id, "s1"); } @@ -482,11 +455,7 @@ mod tests { async fn test_find_resumable_by_git_root(#[future] store: AiSessionStore) { let store = store.await; store - .create_session( - "s1", - Some("/home/user/project/sub"), - Some("/home/user/project"), - ) + .create_session("s1", Some("/home/user/project/sub"), Some("/home/user/project")) .await .unwrap(); @@ -502,16 +471,10 @@ mod tests { #[tokio::test] async fn test_find_resumable_skips_archived(#[future] store: AiSessionStore) { let store = store.await; - store - .create_session("s1", Some("/tmp"), None) - .await - .unwrap(); + store.create_session("s1", Some("/tmp"), None).await.unwrap(); store.archive_session("s1").await.unwrap(); - let found = store - .find_resumable_session(Some("/tmp"), None, 3600) - .await - .unwrap(); + let found = store.find_resumable_session(Some("/tmp"), None, 3600).await.unwrap(); assert!(found.is_none()); } @@ -519,15 +482,9 @@ mod tests { #[tokio::test] async fn test_find_resumable_no_match_different_dir(#[future] store: AiSessionStore) { let store = store.await; - store - .create_session("s1", Some("/home/user/project"), None) - .await - .unwrap(); + store.create_session("s1", Some("/home/user/project"), None).await.unwrap(); - let found = store - .find_resumable_session(Some("/other/dir"), None, 3600) - .await - .unwrap(); + let found = store.find_resumable_session(Some("/other/dir"), None, 3600).await.unwrap(); assert!(found.is_none()); } @@ -547,10 +504,7 @@ mod tests { async fn test_update_server_session_id(#[future] store_with_s1: AiSessionStore) { let store = store_with_s1.await; - store - .update_server_session_id("s1", "server-abc") - .await - .unwrap(); + store.update_server_session_id("s1", "server-abc").await.unwrap(); let session = store.get_session("s1").await.unwrap().unwrap(); assert_eq!(session.server_session_id.as_deref(), Some("server-abc")); @@ -562,11 +516,7 @@ mod tests { let store = store_with_s1.await; let before = store.get_session("s1").await.unwrap().unwrap(); - store - .find_resumable_session(Some("/tmp"), None, 3600) - .await - .unwrap() - .unwrap(); + store.find_resumable_session(Some("/tmp"), None, 3600).await.unwrap().unwrap(); let after = store.get_session("s1").await.unwrap().unwrap(); assert_eq!(before.updated_at, after.updated_at); @@ -613,18 +563,9 @@ mod tests { async fn test_events_ordered_chronologically(#[future] store_with_s1: AiSessionStore) { let store = store_with_s1.await; - store - .append_event("s1", "e1", None, "inv1", "user_message", "{}") - .await - .unwrap(); - store - .append_event("s1", "e2", Some("e1"), "inv1", "text", "{}") - .await - .unwrap(); - store - .append_event("s1", "e3", Some("e2"), "inv2", "user_message", "{}") - .await - .unwrap(); + store.append_event("s1", "e1", None, "inv1", "user_message", "{}").await.unwrap(); + store.append_event("s1", "e2", Some("e1"), "inv1", "text", "{}").await.unwrap(); + store.append_event("s1", "e3", Some("e2"), "inv2", "user_message", "{}").await.unwrap(); let events = store.load_events("s1").await.unwrap(); assert_eq!(events.len(), 3); diff --git a/crates/atuin-ai/src/stream.rs b/crates/atuin-ai/src/stream.rs index 25a07f335..9b7139bff 100644 --- a/crates/atuin-ai/src/stream.rs +++ b/crates/atuin-ai/src/stream.rs @@ -4,17 +4,14 @@ use atuin_client::history::History; use atuin_client::settings::AiCapabilities; - -use crate::context::history_output_capability_available; use atuin_common::url::UrlAppendExt; - use eventsource_stream::Eventsource; use eyre::Result; use futures::StreamExt; use reqwest::Url; use reqwest::header::USER_AGENT; -use crate::context::ClientContext; +use crate::context::{ClientContext, history_output_capability_available}; pub(crate) static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION")); @@ -76,10 +73,7 @@ impl ChatRequest { invocation_id: String, model: Option, ) -> Self { - let mut caps = vec![ - "client_invocations".to_string(), - "client_v1_load_skill".to_string(), - ]; + let mut caps = vec!["client_invocations".to_string(), "client_v1_load_skill".to_string()]; if capabilities.enable_history_search.unwrap_or(true) { caps.push("client_v1_atuin_history".to_string()); } @@ -97,12 +91,7 @@ impl ChatRequest { caps.push("client_v1_atuin_output".to_string()); } if let Ok(extra) = std::env::var("ATUIN_AI__ADDITIONAL_CAPS") { - caps.extend( - extra - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()), - ); + caps.extend(extra.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty())); } Self { diff --git a/crates/atuin-ai/src/tools/descriptor.rs b/crates/atuin-ai/src/tools/descriptor.rs index 4190540c7..697c003e4 100644 --- a/crates/atuin-ai/src/tools/descriptor.rs +++ b/crates/atuin-ai/src/tools/descriptor.rs @@ -122,8 +122,5 @@ const ALL_DESCRIPTORS: &[&ToolDescriptor] = &[ /// Look up a tool descriptor by its canonical wire name. /// Returns None for unknown tool names. pub(crate) fn by_name(name: &str) -> Option<&'static ToolDescriptor> { - ALL_DESCRIPTORS - .iter() - .find(|d| d.canonical_names.contains(&name)) - .copied() + ALL_DESCRIPTORS.iter().find(|d| d.canonical_names.contains(&name)).copied() } diff --git a/crates/atuin-ai/src/tools/mod.rs b/crates/atuin-ai/src/tools/mod.rs index c9fed1d68..dafa1405d 100644 --- a/crates/atuin-ai/src/tools/mod.rs +++ b/crates/atuin-ai/src/tools/mod.rs @@ -1,9 +1,7 @@ -use std::{ - io::BufRead, - num::NonZeroU16, - path::{Path, PathBuf}, - time::Duration, -}; +use std::io::BufRead; +use std::num::NonZeroU16; +use std::path::{Path, PathBuf}; +use std::time::Duration; use atuin_client::history::AuthorPattern; use atuin_common::ansi::{self, Vt100ParserExt as _}; @@ -27,9 +25,7 @@ use crate::permissions::rule::Rule; /// `?`, and `[...]` via `glob_match`. fn path_matches_scope(path: &Path, scope: &str) -> bool { let path = if path.is_relative() { - std::env::current_dir() - .map(|cwd| cwd.join(path)) - .unwrap_or_else(|_| path.to_path_buf()) + std::env::current_dir().map(|cwd| cwd.join(path)).unwrap_or_else(|_| path.to_path_buf()) } else { path.to_path_buf() }; @@ -179,15 +175,13 @@ impl TryFrom<(&str, &serde_json::Value)> for ClientToolCall { "edit_file" => Ok(ClientToolCall::Edit(EditToolCall::try_from(input)?)), "write_file" => Ok(ClientToolCall::Write(WriteToolCall::try_from(input)?)), "execute_shell_command" => Ok(ClientToolCall::Shell(ShellToolCall::try_from(input)?)), - "atuin_history" => Ok(ClientToolCall::AtuinHistory( - AtuinHistoryToolCall::try_from(input)?, - )), - "atuin_output" => Ok(ClientToolCall::AtuinOutput(AtuinOutputToolCall::try_from( - input, - )?)), - "load_skill" => Ok(ClientToolCall::LoadSkill(LoadSkillToolCall::try_from( - input, - )?)), + "atuin_history" => { + Ok(ClientToolCall::AtuinHistory(AtuinHistoryToolCall::try_from(input)?)) + } + "atuin_output" => { + Ok(ClientToolCall::AtuinOutput(AtuinOutputToolCall::try_from(input)?)) + } + "load_skill" => Ok(ClientToolCall::LoadSkill(LoadSkillToolCall::try_from(input)?)), _ => Err(eyre::eyre!("Unknown tool call: {name}")), } } @@ -284,10 +278,8 @@ impl TryFrom<&serde_json::Value> for ReadToolCall { type Error = eyre::Error; fn try_from(value: &serde_json::Value) -> Result { - let path = value - .get("file_path") - .and_then(|v| v.as_str()) - .ok_or(eyre::eyre!("Missing path"))?; + let path = + value.get("file_path").and_then(|v| v.as_str()).ok_or(eyre::eyre!("Missing path"))?; let offset = value.get("offset").and_then(|v| v.as_u64()).unwrap_or(0); let limit = value @@ -366,7 +358,8 @@ impl ReadToolCall { if numbered.len() > 100_000 { ToolOutcome::Error(format!( - "Error: file is too large to read ({} bytes in {} lines); use view_range to read a subset of the file", + "Error: file is too large to read ({} bytes in {} lines); use view_range \ + to read a subset of the file", numbered.len(), lines.len() )) @@ -424,10 +417,7 @@ impl TryFrom<&serde_json::Value> for EditToolCall { .and_then(|v| v.as_str()) .ok_or(eyre::eyre!("Missing new_string"))?; - let replace_all = value - .get("replace_all") - .and_then(|v| v.as_bool()) - .unwrap_or(false); + let replace_all = value.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false); Ok(EditToolCall { path: expand_path(path), @@ -507,16 +497,15 @@ impl EditToolCall { Ok(FreshnessCheck::Stale) => { return ( ToolOutcome::Error( - "File has been modified since read, either by the user or by a linter. Read it again before attempting to edit it.".to_string(), + "File has been modified since read, either by the user or by a linter. \ + Read it again before attempting to edit it." + .to_string(), ), None, ); } Err(e) => { - return ( - ToolOutcome::Error(format!("Error checking file state: {e}")), - None, - ); + return (ToolOutcome::Error(format!("Error checking file state: {e}")), None); } Ok(FreshnessCheck::Fresh) => {} } @@ -533,7 +522,8 @@ impl EditToolCall { if match_count == 0 { return ( ToolOutcome::Error(format!( - "old_string not found in {}. Make sure it matches exactly, including whitespace and indentation.", + "old_string not found in {}. Make sure it matches exactly, including \ + whitespace and indentation.", resolved_path.display() )), None, @@ -543,7 +533,9 @@ impl EditToolCall { if match_count > 1 && !self.replace_all { return ( ToolOutcome::Error(format!( - "Found {match_count} matches of old_string in {}, but replace_all is false. Either provide more context to make the match unique, or set replace_all to true.", + "Found {match_count} matches of old_string in {}, but replace_all is false. \ + Either provide more context to make the match unique, or set replace_all to \ + true.", resolved_path.display() )), None, @@ -612,15 +604,10 @@ impl TryFrom<&serde_json::Value> for WriteToolCall { .and_then(|v| v.as_str()) .ok_or(eyre::eyre!("Missing file_path"))?; - let content = value - .get("content") - .and_then(|v| v.as_str()) - .ok_or(eyre::eyre!("Missing content"))?; + let content = + value.get("content").and_then(|v| v.as_str()).ok_or(eyre::eyre!("Missing content"))?; - let overwrite = value - .get("overwrite") - .and_then(|v| v.as_bool()) - .unwrap_or(false); + let overwrite = value.get("overwrite").and_then(|v| v.as_bool()).unwrap_or(false); Ok(WriteToolCall { path: expand_path(path), @@ -659,7 +646,8 @@ impl WriteToolCall { if resolved_path.exists() && !self.overwrite { return ( ToolOutcome::Error(format!( - "File already exists: {}. Set overwrite to true to replace it, or use edit_file to make targeted changes.", + "File already exists: {}. Set overwrite to true to replace it, or use \ + edit_file to make targeted changes.", resolved_path.display() )), None, @@ -676,7 +664,11 @@ impl WriteToolCall { } let line_count = self.content.lines().count(); - let verb = if existed { "Overwrote" } else { "Created" }; + let verb = if existed { + "Overwrote" + } else { + "Created" + }; ( ToolOutcome::Success(format!( "{verb} {} ({line_count} lines).", @@ -722,28 +714,15 @@ impl TryFrom<&serde_json::Value> for ShellToolCall { fn try_from(value: &serde_json::Value) -> Result { let dir = value.get("dir").and_then(|v| v.as_str()); - let command = value - .get("command") - .and_then(|v| v.as_str()) - .ok_or(eyre::eyre!("Missing command"))?; + let command = + value.get("command").and_then(|v| v.as_str()).ok_or(eyre::eyre!("Missing command"))?; - let shell = value - .get("shell") - .and_then(|v| v.as_str()) - .unwrap_or("bash") - .to_string(); + let shell = value.get("shell").and_then(|v| v.as_str()).unwrap_or("bash").to_string(); - let timeout_secs = value - .get("timeout") - .and_then(|v| v.as_u64()) - .filter(|&v| v > 0) - .unwrap_or(30) - .min(600); + let timeout_secs = + value.get("timeout").and_then(|v| v.as_u64()).filter(|&v| v > 0).unwrap_or(30).min(600); - let description = value - .get("description") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); + let description = value.get("description").and_then(|v| v.as_str()).map(|s| s.to_string()); Ok(ShellToolCall { dir: dir.map(expand_path), @@ -1034,21 +1013,12 @@ impl TryFrom<&serde_json::Value> for AtuinHistoryToolCall { }) .collect::>>()?; - let query = value - .get("query") - .and_then(|v| v.as_str()) - .ok_or(eyre::eyre!("Missing query"))?; + let query = + value.get("query").and_then(|v| v.as_str()).ok_or(eyre::eyre!("Missing query"))?; - let limit = value - .get("limit") - .and_then(|v| v.as_i64()) - .unwrap_or(10) - .clamp(1, 50); + let limit = value.get("limit").and_then(|v| v.as_i64()).unwrap_or(10).clamp(1, 50); - let only_failed = value - .get("only_failed") - .and_then(|v| v.as_bool()) - .unwrap_or(false); + let only_failed = value.get("only_failed").and_then(|v| v.as_bool()).unwrap_or(false); let authors = match value.get("authors") { Some(authors) => authors @@ -1110,8 +1080,8 @@ impl AtuinHistoryToolCall { && context.session.is_empty() { return ToolOutcome::Error( - "Session-scoped search is unavailable: $ATUIN_SESSION is not set, so there is \ - no shell session to scope to. Use another filter mode." + "Session-scoped search is unavailable: $ATUIN_SESSION is not set, so there is no \ + shell session to scope to. Use another filter mode." .to_string(), ); } @@ -1124,13 +1094,7 @@ impl AtuinHistoryToolCall { }; let results = match db - .search( - DbSearchMode::Fuzzy, - filter_mode, - &context, - &self.query, - filter_options, - ) + .search(DbSearchMode::Fuzzy, filter_mode, &context, &self.query, filter_options) .await { Ok(results) => results, @@ -1175,11 +1139,8 @@ impl TryFrom<&serde_json::Value> for AtuinOutputToolCall { .and_then(|v| Uuid::parse_str(v).ok()) .ok_or(eyre::eyre!("Missing or invalid history ID"))?; - let ranges = value - .get("ranges") - .and_then(|v| v.as_array()) - .map(Vec::as_slice) - .unwrap_or(&[]); + let ranges = + value.get("ranges").and_then(|v| v.as_array()).map(Vec::as_slice).unwrap_or(&[]); let ranges = ranges .iter() @@ -1192,9 +1153,8 @@ impl TryFrom<&serde_json::Value> for AtuinOutputToolCall { let start = range[0] .as_i64() .ok_or_else(|| eyre::eyre!("Range start must be an integer"))?; - let end = range[1] - .as_i64() - .ok_or_else(|| eyre::eyre!("Range end must be an integer"))?; + let end = + range[1].as_i64().ok_or_else(|| eyre::eyre!("Range end must be an integer"))?; Ok((start, end)) }) @@ -1219,14 +1179,8 @@ impl PermissibleToolCall for AtuinOutputToolCall { } fn format_output_lines_for_llm(lines: &[atuin_daemon::semantic::OutputLine]) -> String { - let width = lines - .iter() - .map(|line| line.line_number) - .max() - .unwrap_or(1) - .max(1) - .ilog10() as usize - + 1; + let width = + lines.iter().map(|line| line.line_number).max().unwrap_or(1).max(1).ilog10() as usize + 1; let mut formatted = Vec::with_capacity(lines.len()); let mut previous_line_number = None; @@ -1258,10 +1212,7 @@ impl AtuinOutputToolCall { }; let history_id = self.history_id.as_simple().to_string(); - let response = match client - .command_output(history_id.clone(), self.ranges.clone()) - .await - { + let response = match client.command_output(history_id.clone(), self.ranges.clone()).await { Ok(response) => response, Err(e) => return ToolOutcome::Error(format!("Failed to fetch command output: {e}")), }; @@ -1291,10 +1242,7 @@ impl AtuinOutputToolCall { response.total_bytes, response.output_observed_bytes, response.total_lines ) } else { - format!( - "{} bytes, {} lines", - response.total_bytes, response.total_lines - ) + format!("{} bytes, {} lines", response.total_bytes, response.total_lines) }; ToolOutcome::Success(format!( @@ -1312,10 +1260,8 @@ impl TryFrom<&serde_json::Value> for LoadSkillToolCall { type Error = eyre::Error; fn try_from(value: &serde_json::Value) -> Result { - let name = value - .get("name") - .and_then(|v| v.as_str()) - .ok_or(eyre::eyre!("Missing skill name"))?; + let name = + value.get("name").and_then(|v| v.as_str()).ok_or(eyre::eyre!("Missing skill name"))?; Ok(LoadSkillToolCall { name: name.to_string(), @@ -1335,10 +1281,11 @@ impl PermissibleToolCall for LoadSkillToolCall { #[cfg(test)] mod tests { - use super::*; use atuin_common::filter; use rstest::*; + use super::*; + fn read_rule(scope: Option<&str>) -> Rule { Rule { tool: "Read".to_string(), @@ -1391,10 +1338,7 @@ mod tests { let call = AtuinHistoryToolCall::try_from(&input).unwrap(); assert!(call.only_failed); - assert_eq!( - call.authors.items(), - filter::Items::Some([AuthorPattern::AllAgent].as_slice()) - ); + assert_eq!(call.authors.items(), filter::Items::Some([AuthorPattern::AllAgent].as_slice())); } #[rstest] @@ -1405,10 +1349,7 @@ mod tests { let call = AtuinOutputToolCall::try_from(&input)?; - assert_eq!( - call.history_id.as_simple().to_string(), - "018f0000000070008000000000000000" - ); + assert_eq!(call.history_id.as_simple().to_string(), "018f0000000070008000000000000000"); assert!(call.ranges.is_empty()); Ok(()) } @@ -1537,10 +1478,7 @@ mod tests { ) )] fn read_scope_glob(#[case] path: &str, #[case] scope: &str, #[case] expected: bool) { - assert_eq!( - read_tool(path).matches_rule(&read_rule(Some(scope))), - expected - ); + assert_eq!(read_tool(path).matches_rule(&read_rule(Some(scope))), expected); } #[rstest] @@ -1549,11 +1487,7 @@ mod tests { fn relative_multi_segment_glob(#[case] scope: &str, #[case] expected: bool) { // This matches against the path relative to cwd let cwd = std::env::current_dir().unwrap(); - let abs = cwd - .join("crates") - .join("atuin-ai") - .join("src") - .join("lib.rs"); + let abs = cwd.join("crates").join("atuin-ai").join("src").join("lib.rs"); let tool = read_tool(abs.to_str().unwrap()); assert_eq!(tool.matches_rule(&read_rule(Some(scope))), expected); } @@ -1916,10 +1850,7 @@ mod tests { let (outcome, new_bytes) = call2.execute(&file_path, &tracker); assert!(matches!(outcome, ToolOutcome::Success(_))); assert!(new_bytes.is_some()); - assert_eq!( - std::fs::read_to_string(&file_path).unwrap(), - "key1 = xxx\nkey2 = yyy\n" - ); + assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "key1 = xxx\nkey2 = yyy\n"); } #[rstest] @@ -1961,10 +1892,7 @@ mod tests { } // File should be unchanged (the user's edit preserved) - assert_eq!( - std::fs::read_to_string(&file_path).unwrap(), - "value = user_changed\n" - ); + assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "value = user_changed\n"); } #[rstest] @@ -1995,9 +1923,7 @@ mod tests { // Second edit — snapshot should NOT be recreated let content_before_second = std::fs::read(&file_path).unwrap(); - let created = store - .ensure_snapshot(&file_path, &content_before_second) - .unwrap(); + let created = store.ensure_snapshot(&file_path, &content_before_second).unwrap(); assert!(!created); // idempotent — already snapshotted } diff --git a/crates/atuin-ai/src/tui/app.rs b/crates/atuin-ai/src/tui/app.rs index a38e2ca7e..36612ddb0 100644 --- a/crates/atuin-ai/src/tui/app.rs +++ b/crates/atuin-ai/src/tui/app.rs @@ -7,7 +7,6 @@ use std::path::PathBuf; use std::time::{Duration, Instant}; use atuin_client::settings::Settings; - use crossterm::event::{KeyCode, KeyEventKind}; use eye_declare::{ App, Ctx, Element, ElementExt, Fluent, Focus, FocusHandle, InputEvent, Keymap, Task, col, key, @@ -17,11 +16,10 @@ use ratatui_core::style::{Color, Modifier, Style}; use tokio::sync::mpsc::UnboundedSender; use tui_textarea::TextArea; -use crate::fsm::StreamPhase; use crate::fsm::effects::{Effect, ExitAction, PermissionTarget, TimeoutKind}; use crate::fsm::events::{Event, PermissionChoice, PermissionResponse}; use crate::fsm::tools::ToolPreviewData; -use crate::fsm::{AgentFsm, AgentState}; +use crate::fsm::{AgentFsm, AgentState, StreamPhase}; use crate::tools::{ClientToolCall, PermissibleToolCall}; use crate::tui::events::PermissionResult; use crate::tui::persist::PersistJob; @@ -231,12 +229,9 @@ impl AiApp { } fn has_confirmation(&self) -> bool { - matches!( - self.fsm.state, - AgentState::Idle { - confirmation: Some(_) - } - ) + matches!(self.fsm.state, AgentState::Idle { + confirmation: Some(_) + }) } fn input_active(&self) -> bool { @@ -256,9 +251,7 @@ impl AiApp { /// Whether the visible conversation carries a suggested command. fn has_command(&self) -> bool { - self.visible_events() - .iter() - .any(|e| e.as_command().is_some()) + self.visible_events().iter().any(|e| e.as_command().is_some()) } fn visible_events(&self) -> &[crate::tui::state::ConversationEvent] { @@ -288,12 +281,7 @@ impl AiApp { /// events past the pushed frontier. fn live_turns(&self) -> Vec { let events = &self.fsm.ctx.events; - let start = self - .fsm - .ctx - .view_start_index - .max(self.pushed_events) - .min(events.len()); + let start = self.fsm.ctx.view_start_index.max(self.pushed_events).min(events.len()); let mut builder = TurnBuilder::new(&self.fsm.ctx.tools); for event in &events[start..] { builder.add_event(event); @@ -320,9 +308,7 @@ impl AiApp { Some(query) => { let mut results = self.slash_registry.search_fuzzy(&query); results.sort_by(|a, b| { - b.relevance - .partial_cmp(&a.relevance) - .unwrap_or(std::cmp::Ordering::Equal) + b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal) }); self.slash_results = results; } @@ -376,11 +362,8 @@ impl AiApp { Effect::AbortStream => self.streaming = None, Effect::Persist => self.persist(), Effect::TurnEnded => { - let elapsed = self - .turn_started_at - .take() - .map(|t| t.elapsed()) - .unwrap_or_default(); + let elapsed = + self.turn_started_at.take().map(|t| t.elapsed()).unwrap_or_default(); self.responded = Some(Responded { elapsed, tip: self.turn_tip.take(), @@ -420,7 +403,9 @@ impl AiApp { tool_id, history_id, } => { - let Some(io) = &self.io else { continue }; + let Some(io) = &self.io else { + continue; + }; let db = io.app_ctx.history_db.clone(); ctx.perform(async move { use atuin_client::database::Database as _; @@ -455,7 +440,9 @@ impl AiApp { rule, disposition, } => { - let Some(io) = &self.io else { continue }; + let Some(io) = &self.io else { + continue; + }; let file_path = match target { PermissionTarget::Project => { let project_root = io @@ -480,13 +467,11 @@ impl AiApp { }); } Effect::LoadSkill { name, arguments } => { - let Some(io) = &self.io else { continue }; + let Some(io) = &self.io else { + continue; + }; let registry = io.skill_registry.clone(); - let shell = io - .client_ctx - .shell - .clone() - .unwrap_or_else(|| "sh".to_string()); + let shell = io.client_ctx.shell.clone().unwrap_or_else(|| "sh".to_string()); ctx.perform(async move { let content = crate::tui::tools_exec::load_skill_content( ®istry, @@ -504,7 +489,9 @@ impl AiApp { .detach(); } Effect::FetchModels => { - let Some(io) = &self.io else { continue }; + let Some(io) = &self.io else { + continue; + }; let endpoint = io.app_ctx.endpoint.clone(); let token = io.app_ctx.token.clone(); ctx.perform(async move { @@ -516,7 +503,9 @@ impl AiApp { .detach(); } Effect::SaveModelSelection { alias } => { - let Some(_io) = &self.io else { continue }; + let Some(_io) = &self.io else { + continue; + }; tokio::spawn(async move { if let Err(e) = crate::models::save_model_selection(&alias).await { tracing::error!("Failed to save model selection: {e}"); @@ -536,12 +525,7 @@ impl AiApp { /// Keep the prompt cursor tied to the tool being asked about: a new /// question starts from the top. fn sync_permission_prompt(&mut self) { - let current = self - .fsm - .ctx - .tools - .awaiting_permission() - .map(|t| t.id.clone()); + let current = self.fsm.ctx.tools.awaiting_permission().map(|t| t.id.clone()); if current != self.permission_prompt_for { self.permission_prompt_for = current; self.permission_select = SelectState::default(); @@ -619,12 +603,8 @@ impl AiApp { ClientToolCall::Shell(shell_call) => { let (interrupt_tx, interrupt_rx) = tokio::sync::oneshot::channel(); self.tool_interrupts.insert(tool_id.clone(), interrupt_tx); - ctx.spawn(crate::tui::tools_exec::shell_stream( - tool_id, - shell_call, - interrupt_rx, - )) - .detach(); + ctx.spawn(crate::tui::tools_exec::shell_stream(tool_id, shell_call, interrupt_rx)) + .detach(); } ClientToolCall::Edit(edit_call) => { let resolved = edit_call.resolved_path(); @@ -647,8 +627,7 @@ impl AiApp { && let Some(io) = &mut self.io && let Ok(mtime) = std::fs::metadata(&resolved).and_then(|m| m.modified()) { - io.file_tracker - .update_after_edit(&resolved, new_bytes, mtime); + io.file_tracker.update_after_edit(&resolved, new_bytes, mtime); } let preview = match (&old_content, &new_content) { @@ -687,8 +666,7 @@ impl AiApp { && let Some(io) = &mut self.io && let Ok(mtime) = std::fs::metadata(&resolved).and_then(|m| m.modified()) { - io.file_tracker - .update_after_edit(&resolved, new_bytes, mtime); + io.file_tracker.update_after_edit(&resolved, new_bytes, mtime); } let preview = (!outcome.is_error()).then(|| { @@ -730,7 +708,9 @@ impl AiApp { ); } ClientToolCall::AtuinHistory(history_call) => { - let Some(io) = &self.io else { return }; + let Some(io) = &self.io else { + return; + }; let db = io.app_ctx.history_db.clone(); ctx.perform(async move { let outcome = history_call.execute(&db).await; @@ -754,13 +734,11 @@ impl AiApp { .detach(); } ClientToolCall::LoadSkill(skill_call) => { - let Some(io) = &self.io else { return }; + let Some(io) = &self.io else { + return; + }; let registry = io.skill_registry.clone(); - let shell = io - .client_ctx - .shell - .clone() - .unwrap_or_else(|| "sh".to_string()); + let shell = io.client_ctx.shell.clone().unwrap_or_else(|| "sh".to_string()); ctx.perform(async move { let content = crate::tui::tools_exec::load_skill_content( ®istry, @@ -815,7 +793,9 @@ impl AiApp { /// channel order, so a later snapshot can never be overwritten by an /// earlier one. fn persist(&self) { - let Some(io) = &self.io else { return }; + let Some(io) = &self.io else { + return; + }; let _ = io.persist.send(PersistJob::Session { events: self.fsm.ctx.events.clone(), server_session_id: self.fsm.ctx.session_id.clone(), @@ -836,11 +816,7 @@ impl AiApp { if turns.is_empty() { return; } - if self.is_busy() - && matches!( - turns.last().map(|t| &t.kind), - Some(UiTurnKind::Agent { .. }) - ) + if self.is_busy() && matches!(turns.last().map(|t| &t.kind), Some(UiTurnKind::Agent { .. })) { // Mid-stream: the whole in-flight agent turn stays in the tail // (statuses and text still mutate). It seals when the FSM @@ -853,19 +829,12 @@ impl AiApp { && let Some(notice) = self.resume_notice.take() { ctx.push( - text(notice).style( - Style::default() - .fg(Color::DarkGray) - .add_modifier(Modifier::ITALIC), - ), + text(notice) + .style(Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC)), ); } for (i, turn) in turns.iter().enumerate() { - ctx.push(view::turn_view( - turn, - self.pushed_turns == 0 && i == 0, - None, - )); + ctx.push(view::turn_view(turn, self.pushed_turns == 0 && i == 0, None)); self.pushed_turns += 1; } self.pushed_events = self.fsm.ctx.events.len(); @@ -974,11 +943,8 @@ impl AiApp { /// Pull the next relevant tip for a starting turn. fn pull_tip(&mut self) -> Option<&'static Tip> { - let has_context_files = self - .io - .as_ref() - .map(|io| io.user_context_cache.has_gathered()) - .unwrap_or(None); + let has_context_files = + self.io.as_ref().map(|io| io.user_context_cache.has_gathered()).unwrap_or(None); let tip_ctx = TipContext { settings: &self.settings, model_set: self.fsm.ctx.model.is_some(), @@ -1151,16 +1117,11 @@ impl App for AiApp { Some(crate::fsm::ModelPicker::Ready(list)) if asking.is_none() => Some(list), _ => None, }; - let picker_loading = matches!( - self.fsm.ctx.model_picker, - Some(crate::fsm::ModelPicker::Loading) - ); + let picker_loading = + matches!(self.fsm.ctx.model_picker, Some(crate::fsm::ModelPicker::Loading)); let show_input = asking.is_none() && ready_picker.is_none() && !self.exiting; - let needs_pending_banner = busy - && !matches!( - turns.last().map(|t| &t.kind), - Some(UiTurnKind::Agent { .. }) - ); + let needs_pending_banner = + busy && !matches!(turns.last().map(|t| &t.kind), Some(UiTurnKind::Agent { .. })); let status_text = if let AgentState::Turn { stream: StreamPhase::Streaming { @@ -1175,15 +1136,11 @@ impl App for AiApp { col() .when_some( - (self.pushed_turns == 0) - .then_some(self.resume_notice.clone()) - .flatten(), + (self.pushed_turns == 0).then_some(self.resume_notice.clone()).flatten(), |c, notice| { c.child( text(notice).style( - Style::default() - .fg(Color::DarkGray) - .add_modifier(Modifier::ITALIC), + Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC), ), ) }, @@ -1250,10 +1207,7 @@ impl App for AiApp { self.is_input_blank() && self.has_command() && self.input_active(), &self.slash_results, )) - .child(view::status_bar_view( - self.fsm.ctx.model.as_deref(), - self.usage.as_ref(), - )) + .child(view::status_bar_view(self.fsm.ctx.model.as_deref(), self.usage.as_ref())) }) } @@ -1283,9 +1237,7 @@ impl App for AiApp { ); if matches!(self.fsm.state, AgentState::Error(_)) { - km = km - .on(key(KeyCode::Enter), Msg::Retry) - .on(key(KeyCode::Char('r')), Msg::Retry); + km = km.on(key(KeyCode::Enter), Msg::Retry).on(key(KeyCode::Char('r')), Msg::Retry); } if self.has_confirmation() { @@ -1296,10 +1248,7 @@ impl App for AiApp { km = km .on(key(KeyCode::Enter), Msg::ConfirmPermission) .merge(SelectState::keymap().map(Msg::PermissionSelect)); - } else if matches!( - self.fsm.ctx.model_picker, - Some(crate::fsm::ModelPicker::Ready(_)) - ) { + } else if matches!(self.fsm.ctx.model_picker, Some(crate::fsm::ModelPicker::Ready(_))) { km = km .on(key(KeyCode::Enter), Msg::ConfirmModel) .merge(SelectState::keymap().map(Msg::ModelSelect)); @@ -1337,13 +1286,14 @@ fn capitalize(s: &str) -> String { #[cfg(test)] mod tests { - use super::*; - use crate::tui::state::ConversationEvent; use crossterm::event::{KeyEvent, KeyModifiers}; use eye_declare::Runtime; use eye_declare_engine::test_terminal::TestTerminal; use rstest::{fixture, rstest}; + use super::*; + use crate::tui::state::ConversationEvent; + fn fixture_app() -> AiApp { let mut fsm = AgentFsm::new(vec![], "test-invocation".into()); fsm.ctx.events.push(ConversationEvent::UserMessage { @@ -1380,9 +1330,7 @@ mod tests { } fn press_mod(&mut self, code: KeyCode, modifiers: KeyModifiers) -> Option { - let (bytes, exit) = self - .rt - .handle(InputEvent::Key(KeyEvent::new(code, modifiers))); + let (bytes, exit) = self.rt.handle(InputEvent::Key(KeyEvent::new(code, modifiers))); self.term.feed(&bytes); exit } @@ -1414,9 +1362,7 @@ mod tests { /// Scrollback + viewport, for content that has scrolled off. fn all_lines(&self) -> String { - [self.term.scrollback_lines(), self.term.viewport_lines()] - .concat() - .join("\n") + [self.term.scrollback_lines(), self.term.viewport_lines()].concat().join("\n") } } @@ -1439,18 +1385,9 @@ mod tests { fn tail_renders_conversation_turns(#[from(harness)] h: Harness) { let screen = h.screen(); assert!(screen.contains(" You"), "user label missing:\n{screen}"); - assert!( - screen.contains("list my files"), - "user text missing:\n{screen}" - ); - assert!( - screen.contains(" Atuin AI"), - "agent label missing:\n{screen}" - ); - assert!( - screen.contains("Use ls to list files."), - "agent markdown missing:\n{screen}" - ); + assert!(screen.contains("list my files"), "user text missing:\n{screen}"); + assert!(screen.contains(" Atuin AI"), "agent label missing:\n{screen}"); + assert!(screen.contains("Use ls to list files."), "agent markdown missing:\n{screen}"); } #[rstest] @@ -1468,8 +1405,7 @@ mod tests { h.press(KeyCode::Esc); assert!( - !h.all_lines() - .contains("Generate a command or ask a question"), + !h.all_lines().contains("Generate a command or ask a question"), "input box still on screen after exit:\n{}", h.all_lines() ); @@ -1534,10 +1470,7 @@ mod tests { h.type_str("/he"); let screen = h.screen(); - assert!( - screen.contains("Show help information"), - "suggestion missing:\n{screen}" - ); + assert!(screen.contains("Show help information"), "suggestion missing:\n{screen}"); h.press(KeyCode::Tab); assert_eq!(h.app().input.borrow().lines(), ["/help".to_string()]); @@ -1548,10 +1481,7 @@ mod tests { h.type_str("a"); h.press_mod(KeyCode::Char('j'), KeyModifiers::CONTROL); h.type_str("b"); - assert_eq!( - h.app().input.borrow().lines(), - ["a".to_string(), "b".to_string()] - ); + assert_eq!(h.app().input.borrow().lines(), ["a".to_string(), "b".to_string()]); } #[rstest] @@ -1565,12 +1495,8 @@ mod tests { fn two_message_fsm() -> AgentFsm { let mut fsm = AgentFsm::new(vec![], "test-invocation".into()); for (q, a) in [("first question", "one"), ("second question", "two")] { - fsm.ctx - .events - .push(ConversationEvent::UserMessage { content: q.into() }); - fsm.ctx - .events - .push(ConversationEvent::Text { content: a.into() }); + fsm.ctx.events.push(ConversationEvent::UserMessage { content: q.into() }); + fsm.ctx.events.push(ConversationEvent::Text { content: a.into() }); } fsm } @@ -1690,15 +1616,8 @@ mod tests { let app = h.app(); assert!(!app.is_busy(), "turn should be over"); - assert!( - app.pushed_turns > pushed_mid, - "agent turn should seal to scrollback on Done" - ); - assert_eq!( - app.pushed_events, - app.fsm.ctx.events.len(), - "frontier should cover all events" - ); + assert!(app.pushed_turns > pushed_mid, "agent turn should seal to scrollback on Done"); + assert_eq!(app.pushed_events, app.fsm.ctx.events.len(), "frontier should cover all events"); // The sealed turn is on the terminal, and the live tail no longer // repeats it. let all = h.all_lines(); @@ -1733,10 +1652,7 @@ mod tests { }); let screen = h.screen(); - assert!( - screen.contains("Responded in"), - "responded line missing:\n{screen}" - ); + assert!(screen.contains("Responded in"), "responded line missing:\n{screen}"); assert!( screen.contains("└ Tip: press Esc to interrupt a response"), "turn's tip should ride along onto the responded line:\n{screen}" @@ -1751,10 +1667,7 @@ mod tests { !screen.contains("Responded in"), "responded line should clear on new turn:\n{screen}" ); - assert!( - screen.contains("└ Tip:"), - "second turn should show a tip\n{screen}" - ); + assert!(screen.contains("└ Tip:"), "second turn should show a tip\n{screen}"); assert!( !screen.contains("└ Tip: press Esc to interrupt a response"), "second turn should pull the next tip:\n{screen}" @@ -1792,10 +1705,7 @@ mod tests { let all = h.all_lines(); assert!(!all.contains("Tip:"), "tips disabled but shown:\n{all}"); - assert!( - all.contains("Responded in"), - "responded line should still show:\n{all}" - ); + assert!(all.contains("Responded in"), "responded line should still show:\n{all}"); } #[rstest] @@ -1822,20 +1732,10 @@ mod tests { h.stream(Event::StreamError("connection lost".into())); assert!(matches!(h.app().fsm.state, AgentState::Error(_))); - assert_eq!( - h.app().pushed_turns, - pushed_before, - "failed turn must stay live for retry" - ); + assert_eq!(h.app().pushed_turns, pushed_before, "failed turn must stay live for retry"); let screen = h.screen(); - assert!( - screen.contains("Error: connection lost"), - "error line missing:\n{screen}" - ); - assert!( - screen.contains("[Enter]/[r] Retry"), - "retry footer:\n{screen}" - ); + assert!(screen.contains("Error: connection lost"), "error line missing:\n{screen}"); + assert!(screen.contains("[Enter]/[r] Retry"), "retry footer:\n{screen}"); h.press(KeyCode::Char('r')); assert!(h.app().is_busy(), "retry should restart the turn"); @@ -1888,10 +1788,7 @@ mod tests { #[from(harness_awaiting_shell_permission)] h: Harness, ) { let screen = h.screen(); - assert!( - screen.contains("Atuin AI would like to run: "), - "prompt line missing:\n{screen}" - ); + assert!(screen.contains("Atuin AI would like to run: "), "prompt line missing:\n{screen}"); assert!(screen.contains("echo hi"), "tool desc missing:\n{screen}"); assert!(screen.contains("Allow"), "options missing:\n{screen}"); assert!( @@ -1937,10 +1834,7 @@ mod tests { let app = h.app(); assert!(app.fsm.ctx.tools.awaiting_permission().is_none()); - assert!( - app.tool_interrupts.is_empty(), - "denied tool must not execute" - ); + assert!(app.tool_interrupts.is_empty(), "denied tool must not execute"); assert!( app.fsm .ctx @@ -1978,10 +1872,7 @@ mod tests { outcome: crate::tools::ToolOutcome::Success("done".into()), preview: None, }); - assert!( - h.app().tool_interrupts.is_empty(), - "completion must prune the interrupt sender" - ); + assert!(h.app().tool_interrupts.is_empty(), "completion must prune the interrupt sender"); // With nothing executing, Ctrl+C means quit again. assert_eq!( h.press_mod(KeyCode::Char('c'), KeyModifiers::CONTROL), @@ -1995,10 +1886,8 @@ mod tests { let mut file = tempfile::NamedTempFile::new().unwrap(); writeln!(file, "hello from the file").unwrap(); - let mut h = Harness::new(app_with(AgentFsm::new( - vec!["client_v1_read_file".into()], - "t".into(), - ))); + let mut h = + Harness::new(app_with(AgentFsm::new(vec!["client_v1_read_file".into()], "t".into()))); h.type_str("read it"); h.press(KeyCode::Enter); h.stream(Event::StreamStarted); @@ -2036,9 +1925,7 @@ mod tests { assert!(rt.app().is_busy(), "initial prompt should start a turn"); assert!(rt.app().pushed_turns > 0, "user turn should be pushed"); - let all = [term.scrollback_lines(), term.viewport_lines()] - .concat() - .join("\n"); + let all = [term.scrollback_lines(), term.viewport_lines()].concat().join("\n"); assert!(all.contains("what is atuin?"), "prompt missing:\n{all}"); } @@ -2067,18 +1954,11 @@ mod tests { h.press(KeyCode::Enter); // Headless: FetchModels degrades, picker sits in Loading. - assert!( - h.screen().contains("Loading models…"), - "loading spinner missing:\n{}", - h.screen() - ); + assert!(h.screen().contains("Loading models…"), "loading spinner missing:\n{}", h.screen()); h.stream(Event::ModelListLoaded(Ok(model_list()))); let screen = h.screen(); - assert!( - screen.contains("Select a model:"), - "picker missing:\n{screen}" - ); + assert!(screen.contains("Select a model:"), "picker missing:\n{screen}"); assert!( screen.contains("Smart — balanced (current)"), "server default should be marked current:\n{screen}" @@ -2110,11 +1990,7 @@ mod tests { assert_eq!(h.press(KeyCode::Esc), None, "Esc closes, not exits"); assert!(h.app().fsm.ctx.model_picker.is_none()); - assert_eq!( - h.app().model_select.cursor, - 0, - "cursor resets when the picker closes" - ); + assert_eq!(h.app().model_select.cursor, 0, "cursor resets when the picker closes"); } #[rstest] @@ -2125,10 +2001,7 @@ mod tests { h.stream(Event::ModelListLoaded(Ok(model_list()))); h.type_str("stray keys"); - assert!( - h.app().is_input_blank(), - "editor must not receive keys while the picker is open" - ); + assert!(h.app().is_input_blank(), "editor must not receive keys while the picker is open"); } #[rstest] @@ -2152,10 +2025,7 @@ mod tests { let screen = h.screen(); assert!(screen.contains("92%"), "usage percent missing:\n{screen}"); - assert!( - screen.contains("resets in"), - "reset delta missing:\n{screen}" - ); + assert!(screen.contains("resets in"), "reset delta missing:\n{screen}"); } /// Run one full exchange so the frontier is past zero, then /new. @@ -2178,10 +2048,7 @@ mod tests { #[rstest] fn new_session_notice_renders(#[from(harness_after_new_session)] h: Harness) { let all = h.all_lines(); - assert!( - all.contains("Started a new session."), - "/new notice missing:\n{all}" - ); + assert!(all.contains("Started a new session."), "/new notice missing:\n{all}"); } #[rstest] @@ -2209,27 +2076,16 @@ mod tests { session_id: "s2".into(), }); let all = h.all_lines(); - assert!( - all.contains("the second answer"), - "response vanished on stream end:\n{all}" - ); + assert!(all.contains("the second answer"), "response vanished on stream end:\n{all}"); } #[rstest] fn skill_dispatches_even_when_registered_for_autocomplete() { let mut registry = SlashCommandRegistry::default(); let mut skill_names = HashSet::new(); - registry.register(crate::tui::slash::SlashCommand::new( - "release", - "Cut a release", - )); + registry.register(crate::tui::slash::SlashCommand::new("release", "Cut a release")); skill_names.insert("release".to_string()); - let app = AiApp::headless( - AgentFsm::new(vec![], "t".into()), - None, - registry, - skill_names, - ); + let app = AiApp::headless(AgentFsm::new(vec![], "t".into()), None, registry, skill_names); assert!(matches!( app.dispatch_submit("/release 1.2".into()), @@ -2242,22 +2098,12 @@ mod tests { fn builtin_takes_precedence_over_skill_with_same_name() { let mut registry = SlashCommandRegistry::default(); let mut skill_names = HashSet::new(); - registry.register(crate::tui::slash::SlashCommand::new( - "reload", - "A skill shadowing /reload", - )); + registry + .register(crate::tui::slash::SlashCommand::new("reload", "A skill shadowing /reload")); skill_names.insert("reload".to_string()); - let app = AiApp::headless( - AgentFsm::new(vec![], "t".into()), - None, - registry, - skill_names, - ); + let app = AiApp::headless(AgentFsm::new(vec![], "t".into()), None, registry, skill_names); - assert!(matches!( - app.dispatch_submit("/reload".into()), - Event::SlashCommand { .. } - )); + assert!(matches!(app.dispatch_submit("/reload".into()), Event::SlashCommand { .. })); } #[rstest] @@ -2279,10 +2125,7 @@ mod tests { .iter() .position(|l| l.contains("Continuing previous session")) .expect("notice missing"); - let user_row = lines - .iter() - .position(|l| l.contains(" You")) - .expect("user label missing"); + let user_row = lines.iter().position(|l| l.contains(" You")).expect("user label missing"); assert!(notice_row < user_row); } } diff --git a/crates/atuin-ai/src/tui/persist.rs b/crates/atuin-ai/src/tui/persist.rs index adc5eaef4..8e181f1a9 100644 --- a/crates/atuin-ai/src/tui/persist.rs +++ b/crates/atuin-ai/src/tui/persist.rs @@ -57,9 +57,8 @@ pub(crate) fn spawn_persist_worker( tracing::warn!("failed to persist server session ID: {e}"); } if let Some(json) = file_tracker - && let Err(e) = session_mgr - .set_metadata(crate::file_tracker::METADATA_KEY, &json) - .await + && let Err(e) = + session_mgr.set_metadata(crate::file_tracker::METADATA_KEY, &json).await { tracing::warn!("failed to persist file tracker: {e}"); } diff --git a/crates/atuin-ai/src/tui/select.rs b/crates/atuin-ai/src/tui/select.rs index 325143770..583432009 100644 --- a/crates/atuin-ai/src/tui/select.rs +++ b/crates/atuin-ai/src/tui/select.rs @@ -32,9 +32,7 @@ impl SelectState { } pub fn keymap() -> Keymap { - keymap() - .on(key(KeyCode::Up), SelectMsg::Up) - .on(key(KeyCode::Down), SelectMsg::Down) + keymap().on(key(KeyCode::Up), SelectMsg::Up).on(key(KeyCode::Down), SelectMsg::Down) } } diff --git a/crates/atuin-ai/src/tui/state.rs b/crates/atuin-ai/src/tui/state.rs index 251cdcf9c..1f87ac758 100644 --- a/crates/atuin-ai/src/tui/state.rs +++ b/crates/atuin-ai/src/tui/state.rs @@ -7,9 +7,13 @@ #[derive(Debug, Clone)] pub(crate) enum ConversationEvent { /// User message (what the user typed) - UserMessage { content: String }, + UserMessage { + content: String, + }, /// Text content from assistant (streamed or complete) - Text { content: String }, + Text { + content: String, + }, /// Tool call from assistant ToolCall { id: String, @@ -35,7 +39,9 @@ pub(crate) enum ConversationEvent { }, /// Context injected for the LLM that is not rendered in the TUI. /// Converted to a user message in the API protocol. - SystemContext { content: String }, + SystemContext { + content: String, + }, /// A skill was loaded and its content injected into the conversation. /// Serialized as a full user message for the API but rendered compactly /// in the TUI (just the `/name args` invocation line). diff --git a/crates/atuin-ai/src/tui/tips.rs b/crates/atuin-ai/src/tui/tips.rs index 58abb8a3d..478a18f84 100644 --- a/crates/atuin-ai/src/tui/tips.rs +++ b/crates/atuin-ai/src/tui/tips.rs @@ -33,13 +33,7 @@ pub(crate) const TIPS: &[Tip] = &[ Tip { id: "ctrl-c-interrupt", text: "Ctrl+C interrupts a running command", - relevant: Some(|ctx| { - ctx.settings - .ai - .capabilities - .enable_command_execution - .unwrap_or(true) - }), + relevant: Some(|ctx| ctx.settings.ai.capabilities.enable_command_execution.unwrap_or(true)), }, Tip { id: "model", @@ -58,7 +52,8 @@ pub(crate) const TIPS: &[Tip] = &[ }, Tip { id: "send-cwd", - text: "You can include your working directory in AI requests automatically: `atuin config set ai.opening.send_cwd true`", + text: "You can include your working directory in AI requests automatically: `atuin config \ + set ai.opening.send_cwd true`", relevant: Some(|ctx| { let ai = &ctx.settings.ai; !ai.opening.send_cwd.or(ai.send_cwd).unwrap_or(false) @@ -71,7 +66,8 @@ pub(crate) const TIPS: &[Tip] = &[ }, Tip { id: "send-last-command", - text: "Send your last command to Atuin AI automatically with `atuin config set ai.opening.send_last_command true`", + text: "Send your last command to Atuin AI automatically with `atuin config set \ + ai.opening.send_last_command true`", relevant: Some(|ctx| !ctx.settings.ai.opening.send_last_command.unwrap_or(false)), }, Tip { @@ -165,10 +161,7 @@ mod tests { .filter(|t| t.relevant.is_none_or(|applies| applies(&c))) .map(|t| t.id) .collect(); - assert!( - relevant_ids.len() >= 2, - "test needs at least two relevant tips" - ); + assert!(relevant_ids.len() >= 2, "test needs at least two relevant tips"); let mut rotation = TipRotation::starting_at(0); // Pull two full cycles: tips come back in order, then wrap. diff --git a/crates/atuin-ai/src/tui/tools_exec.rs b/crates/atuin-ai/src/tui/tools_exec.rs index d7397f8c3..69c578087 100644 --- a/crates/atuin-ai/src/tui/tools_exec.rs +++ b/crates/atuin-ai/src/tui/tools_exec.rs @@ -106,9 +106,8 @@ mod tests { #[tokio::test] async fn shell_stream_yields_previews_then_outcome() { let (_interrupt_tx, interrupt_rx) = oneshot::channel(); - let msgs: Vec = shell_stream("t1".into(), shell_call("echo hello"), interrupt_rx) - .collect() - .await; + let msgs: Vec = + shell_stream("t1".into(), shell_call("echo hello"), interrupt_rx).collect().await; let mut saw_preview_with_output = false; let mut done: Option<&Event> = None; @@ -125,10 +124,7 @@ mod tests { other => panic!("unexpected message: {other:?}"), } } - assert!( - saw_preview_with_output, - "expected echoed output in a preview" - ); + assert!(saw_preview_with_output, "expected echoed output in a preview"); let Some(Event::ToolExecutionDone { tool_id, outcome, .. }) = done @@ -137,13 +133,10 @@ mod tests { }; assert_eq!(tool_id, "t1"); assert!( - matches!( - outcome, - ToolOutcome::Structured { - exit_code: Some(0), - .. - } - ), + matches!(outcome, ToolOutcome::Structured { + exit_code: Some(0), + .. + }), "expected clean exit, got {outcome:?}" ); } diff --git a/crates/atuin-ai/src/tui/view/input.rs b/crates/atuin-ai/src/tui/view/input.rs index 8c7068e21..acaa51e6e 100644 --- a/crates/atuin-ai/src/tui/view/input.rs +++ b/crates/atuin-ai/src/tui/view/input.rs @@ -28,11 +28,8 @@ pub(crate) fn new_textarea() -> TextArea<'static> { textarea.set_cursor_line_style(Style::default()); textarea.set_wrap_mode(tui_textarea::WrapMode::Word); textarea.set_placeholder_text("Type a message..."); - textarea.set_placeholder_style( - Style::default() - .fg(Color::DarkGray) - .add_modifier(Modifier::ITALIC), - ); + textarea + .set_placeholder_style(Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC)); textarea.set_max_rows(MAX_INPUT_ROWS); textarea } @@ -72,9 +69,7 @@ pub(crate) fn input_area<'a>( slash_results: &[SlashCommandSearchResult], ) -> AnyElement<'a> { let border_style = Style::default().fg(Color::DarkGray); - let title_style = Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::BOLD); + let title_style = Style::default().fg(Color::Gray).add_modifier(Modifier::BOLD); col() .child( @@ -93,11 +88,7 @@ pub(crate) fn input_area<'a>( ) }) .children( - slash_results - .iter() - .take(4) - .enumerate() - .map(|(i, result)| slash_row(result, i == 0)), + slash_results.iter().take(4).enumerate().map(|(i, result)| slash_row(result, i == 0)), ) .pad_top(1) .any() diff --git a/crates/atuin-ai/src/tui/view/mod.rs b/crates/atuin-ai/src/tui/view/mod.rs index db4e087df..670b137fb 100644 --- a/crates/atuin-ai/src/tui/view/mod.rs +++ b/crates/atuin-ai/src/tui/view/mod.rs @@ -23,7 +23,6 @@ mod trunc; pub(crate) mod turn; use trunc::{command_spinner, truncated_line}; - use turn::{ ConfidenceLevel, DangerLevel, OutOfBandOutputDetails, SuggestedCommandDetails, ToolCallDetails, ToolGroup, ToolGroupKind, ToolRenderData, ToolResultStatus, ToolSummary, UiEvent, UiTurn, @@ -61,9 +60,7 @@ fn md(source: impl Into) -> Markdown { code_block: Style::default().fg(Color::Green), bold: Style::default().add_modifier(Modifier::BOLD), italic: Style::default().add_modifier(Modifier::ITALIC), - heading: Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), + heading: Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), table_border: Style::default().fg(Color::DarkGray), table_header: Style::default().add_modifier(Modifier::BOLD), }) @@ -104,7 +101,11 @@ pub(crate) fn user_turn_view(events: &[UiEvent], first_turn: bool) -> AnyElement UiEvent::Text { content } => Some(text(content.clone()).pad_left(2)), _ => None, })) - .pad_top(if first_turn { 0 } else { 1 }) + .pad_top(if first_turn { + 0 + } else { + 1 + }) .any() } @@ -119,24 +120,16 @@ pub(crate) fn agent_turn_view( col() .child(text(" Atuin AI ").style(label_style)) - .children(events.iter().enumerate().map(|(i, event)| { - col() - .when(i > 0, |c| c.child(text(""))) - .child(event_view(event)) - })) + .children( + events.iter().enumerate().map(|(i, event)| { + col().when(i > 0, |c| c.child(text(""))).child(event_view(event)) + }), + ) .when_some(working, |c, working| { c.child( spinner(working.status.unwrap_or("")) - .spinner_style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - .label_style( - Style::default() - .fg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ) + .spinner_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .label_style(Style::default().fg(Color::DarkGray).add_modifier(Modifier::BOLD)) .pad_left(2) .pad_top(1), ) @@ -164,9 +157,7 @@ pub(crate) fn responded_view( /// the line it hangs off. fn tip_line(tip: &'static Tip) -> impl Element + 'static { let dim = Style::default().fg(Color::DarkGray); - row() - .fixed(2, text("└ ").style(dim)) - .fill(text(format!("Tip: {}", tip.text)).style(dim)) + row().fixed(2, text("└ ").style(dim)).fill(text(format!("Tip: {}", tip.text)).style(dim)) } /// Humanized elapsed time: sub-minute in seconds ("3.2 seconds", @@ -246,10 +237,7 @@ fn tool_status_view(name: &str, status: &ToolResultStatus) -> AnyElement<'static ToolResultStatus::Success => tool_spinner(format!("Ran: {name}"), true).any(), ToolResultStatus::Error { content } => text("✗ ") .style(Style::default().fg(Color::Red)) - .span( - format!("{name}: {}", error_label(content)), - Style::default().fg(Color::Red), - ) + .span(format!("{name}: {}", error_label(content)), Style::default().fg(Color::Red)) .any(), } } @@ -276,19 +264,21 @@ fn shell_tool_view(command: &str, preview: Option<&ToolPreview>) -> AnyElement<' match preview { Some(preview) => col() .child(command_spinner( - if done { "Ran: " } else { "Running: " }, + if done { + "Ran: " + } else { + "Running: " + }, command, done, )) .child( - row() - .fixed(2, text("└ ").style(Style::default().fg(Color::DarkGray))) - .fill( - viewport(preview.lines.iter().cloned()) - .height((preview.lines.len() as u16).clamp(1, MAX_SHELL_PREVIEW_LINES)) - .style(Style::default().fg(Color::Gray)) - .wrap(false), - ), + row().fixed(2, text("└ ").style(Style::default().fg(Color::DarkGray))).fill( + viewport(preview.lines.iter().cloned()) + .height((preview.lines.len() as u16).clamp(1, MAX_SHELL_PREVIEW_LINES)) + .style(Style::default().fg(Color::Gray)) + .wrap(false), + ), ) .child(shell_tool_footer(preview, done)) .any(), @@ -307,9 +297,7 @@ fn shell_tool_footer(preview: &ToolPreview, done: bool) -> AnyElement<'static> { .any(); } if !done { - return text("[Ctrl+C] Interrupt") - .style(Style::default().fg(Color::DarkGray)) - .any(); + return text("[Ctrl+C] Interrupt").style(Style::default().fg(Color::DarkGray)).any(); } if let Some(code) = preview.exit_code { let style = if code == 0 { @@ -347,9 +335,7 @@ fn file_edit_tool_view( col() .child(status_line) .child( - col() - .children(preview.hunks.iter().map(|hunk| hunk_view(hunk, gutter_w))) - .pad_left(2), + col().children(preview.hunks.iter().map(|hunk| hunk_view(hunk, gutter_w))).pad_left(2), ) .any() } @@ -400,14 +386,8 @@ fn file_write_tool_view( (ToolResultStatus::Success, Some(p)) => format!(" ({} lines)", p.total_lines), _ => String::new(), }; - let status_line = tool_status_line( - status, - "Writing", - "Wrote", - "Write", - &display_path, - &line_info, - ); + let status_line = + tool_status_line(status, "Writing", "Wrote", "Write", &display_path, &line_info); let Some(preview) = preview else { return status_line; @@ -480,7 +460,11 @@ fn group_view(group: &ToolGroup) -> AnyElement<'static> { /// Tree-connector marker: `└ ` for the first visible row, spaces after. fn tree_marker(is_first: bool) -> &'static str { - if is_first { "└ " } else { " " } + if is_first { + "└ " + } else { + " " + } } /// 2-char status marker column: ✓ / ✗ / blank. @@ -505,10 +489,7 @@ fn group_row_view( content: AnyElement<'static>, ) -> AnyElement<'static> { row() - .fixed( - 2, - text(tree_marker(is_first)).style(Style::default().fg(Color::DarkGray)), - ) + .fixed(2, text(tree_marker(is_first)).style(Style::default().fg(Color::DarkGray))) .fixed(2, status_marker_view(status)) .fill(content) .any() @@ -568,21 +549,15 @@ fn history_search_row(is_first: bool, details: &ToolCallDetails) -> AnyElement<' let content = if query.trim().is_empty() { text("recent commands") - .style( - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::ITALIC), - ) + .style(Style::default().fg(Color::Gray).add_modifier(Modifier::ITALIC)) .when(!filter_label.is_empty(), |t| { - t.span(" ", Style::default()) - .span(&filter_label, filter_style) + t.span(" ", Style::default()).span(&filter_label, filter_style) }) .any() } else { text(query) .when(!filter_label.is_empty(), |t| { - t.span(" ", Style::default()) - .span(&filter_label, filter_style) + t.span(" ", Style::default()).span(&filter_label, filter_style) }) .any() }; @@ -615,10 +590,8 @@ fn format_filter_modes(modes: &[HistorySearchFilterMode]) -> String { // ─────────────────────────────────────────────────────────────────── fn suggested_command_view(details: &SuggestedCommandDetails) -> AnyElement<'static> { - let is_dangerous = matches!( - details.danger_level, - DangerLevel::High(_) | DangerLevel::Medium(_) - ); + let is_dangerous = + matches!(details.danger_level, DangerLevel::High(_) | DangerLevel::Medium(_)); let danger_notes = details.danger_level.notes().cloned(); let danger_style = match details.danger_level { DangerLevel::High(_) => Style::default().fg(Color::Red), @@ -632,10 +605,8 @@ fn suggested_command_view(details: &SuggestedCommandDetails) -> AnyElement<'stat DangerLevel::Unknown(_) => "Unknown", }; - let low_confidence = matches!( - details.confidence_level, - ConfidenceLevel::Low(_) | ConfidenceLevel::Medium(_) - ); + let low_confidence = + matches!(details.confidence_level, ConfidenceLevel::Low(_) | ConfidenceLevel::Medium(_)); let confidence_level = match details.confidence_level { ConfidenceLevel::Low(_) => "Low", ConfidenceLevel::Medium(_) => "Medium", @@ -666,41 +637,33 @@ fn suggested_command_view(details: &SuggestedCommandDetails) -> AnyElement<'stat .pad_left(2), ) }) - .when_some( - is_dangerous.then_some(danger_notes).flatten(), - |c, notes| { - c.child( - row() - .fixed(2, text("└").style(Style::default().fg(Color::DarkGray))) - .fill(md(notes)) - .pad_left(2), - ) - }, - ) + .when_some(is_dangerous.then_some(danger_notes).flatten(), |c, notes| { + c.child( + row() + .fixed(2, text("└").style(Style::default().fg(Color::DarkGray))) + .fill(md(notes)) + .pad_left(2), + ) + }) .when(low_confidence, |c| { c.child( text("Confidence: ") .style(Style::default().fg(Color::Blue)) .span( confidence_level, - Style::default() - .fg(Color::Blue) - .add_modifier(Modifier::BOLD), + Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD), ) .pad_left(2), ) }) - .when_some( - low_confidence.then_some(confidence_notes).flatten(), - |c, notes| { - c.child( - row() - .fixed(2, text("└").style(Style::default().fg(Color::DarkGray))) - .fill(md(notes)) - .pad_left(2), - ) - }, - ) + .when_some(low_confidence.then_some(confidence_notes).flatten(), |c, notes| { + c.child( + row() + .fixed(2, text("└").style(Style::default().fg(Color::DarkGray))) + .fill(md(notes)) + .pad_left(2), + ) + }) .any() } @@ -720,10 +683,7 @@ pub(crate) fn permission_options( match tool { ClientToolCall::Edit(_) | ClientToolCall::Write(_) => vec![ ("Allow", PermissionResult::Allow), - ( - "Allow this file for this session", - PermissionResult::AllowFileForSession, - ), + ("Allow this file for this session", PermissionResult::AllowFileForSession), ("Always allow", PermissionResult::AlwaysAllow), ("Deny", PermissionResult::Deny), ], @@ -765,10 +725,7 @@ pub(crate) fn permission_prompt_view( // command is context here, not the thing being approved, so one // middle-elided line beats wrapping. ClientToolCall::AtuinOutput(tool) => { - let desc = tool - .command - .clone() - .unwrap_or_else(|| tool.history_id.to_string()); + let desc = tool.command.clone().unwrap_or_else(|| tool.history_id.to_string()); truncated_line(prefix.clone(), Style::default(), desc, desc_style).any() } ClientToolCall::LoadSkill(tool) => wrapped(format!("skill: {}", tool.name)), @@ -800,7 +757,11 @@ pub(crate) fn model_picker_view( .models .iter() .map(|m| { - let marker = if m.alias == in_use { " (current)" } else { "" }; + let marker = if m.alias == in_use { + " (current)" + } else { + "" + }; format!("{} — {}{}", m.name, m.description, marker) }) .collect(); @@ -837,10 +798,7 @@ pub(crate) fn status_bar_view( let left = text(" Model: ") .style(Style::default()) - .span( - model.unwrap_or("default"), - Style::default().add_modifier(Modifier::BOLD), - ) + .span(model.unwrap_or("default"), Style::default().add_modifier(Modifier::BOLD)) .span(" (/model to change)", Style::default().fg(Color::Gray)); let Some((pct, resets_in)) = usage else { @@ -890,9 +848,10 @@ fn format_path_for_display(path: &std::path::Path) -> String { #[cfg(test)] mod tests { - use super::format_elapsed; use std::time::Duration; + use super::format_elapsed; + #[test] fn elapsed_formats_by_magnitude() { assert_eq!(format_elapsed(Duration::from_millis(600)), "0.6 seconds"); diff --git a/crates/atuin-ai/src/tui/view/trunc.rs b/crates/atuin-ai/src/tui/view/trunc.rs index 0f84b9e70..8dee9a12b 100644 --- a/crates/atuin-ai/src/tui/view/trunc.rs +++ b/crates/atuin-ai/src/tui/view/trunc.rs @@ -11,9 +11,8 @@ use std::time::Duration; -use atuin_common::string::EllipsizeExt as _; -use atuin_common::string::Measure; use atuin_common::string::ellipsis::{Indicator, Pos}; +use atuin_common::string::{EllipsizeExt as _, Measure}; use eye_declare::{Element, Spinner}; use ratatui_core::buffer::Buffer; use ratatui_core::layout::Rect; @@ -25,9 +24,7 @@ use unicode_width::UnicodeWidthStr as _; /// spaces, and the middle is elided when it still doesn't fit. fn fit_middle(command: &str, cols: usize) -> String { let one_line = command.split_whitespace().collect::>().join(" "); - one_line - .ellipsize(Measure::Columns(cols), Pos::Middle, Indicator::UNICODE) - .to_string() + one_line.ellipsize(Measure::Columns(cols), Pos::Middle, Indicator::UNICODE).to_string() } /// A tool spinner labeled `prefix` + command, the command middle-elided to @@ -55,10 +52,13 @@ impl CommandSpinner { fn spinner_at(&self, width: u16) -> Spinner { // Marker glyph + trailing space while animating; a done spinner // with a hidden checkmark has no marker (see Spinner::render). - let marker_cols = if self.done { 0 } else { 2 }; - let budget = (width as usize) - .saturating_sub(marker_cols) - .saturating_sub(self.prefix.width()); + let marker_cols = if self.done { + 0 + } else { + 2 + }; + let budget = + (width as usize).saturating_sub(marker_cols).saturating_sub(self.prefix.width()); let label = format!("{}{}", self.prefix, fit_middle(&self.command, budget)); super::tool_spinner(label, self.done).hide_checkmark() } @@ -66,7 +66,11 @@ impl CommandSpinner { impl Element for CommandSpinner { fn height(&self, width: u16) -> u16 { - if width == 0 { 0 } else { 1 } + if width == 0 { + 0 + } else { + 1 + } } fn render(&self, area: Rect, buf: &mut Buffer) { @@ -104,7 +108,11 @@ pub(crate) fn truncated_line( impl Element for TruncatedLine { fn height(&self, width: u16) -> u16 { - if width == 0 { 0 } else { 1 } + if width == 0 { + 0 + } else { + 1 + } } fn render(&self, area: Rect, buf: &mut Buffer) { @@ -130,9 +138,10 @@ impl Element for TruncatedLine { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + fn render_line(el: &impl Element, width: u16) -> String { let area = Rect::new(0, 0, width, 1); let mut buf = Buffer::empty(area); diff --git a/crates/atuin-ai/src/tui/view/turn.rs b/crates/atuin-ai/src/tui/view/turn.rs index fb280457d..00cca1b2f 100644 --- a/crates/atuin-ai/src/tui/view/turn.rs +++ b/crates/atuin-ai/src/tui/view/turn.rs @@ -1,8 +1,7 @@ use std::path::PathBuf; use crate::fsm::tools::ToolManager; -use crate::tools::descriptor; -use crate::tools::{ClientToolCall, HistorySearchFilterMode, ToolPreview}; +use crate::tools::{ClientToolCall, HistorySearchFilterMode, ToolPreview, descriptor}; use crate::tui::ConversationEvent; /// Server-sent danger level for a suggested command @@ -106,9 +105,7 @@ pub(crate) struct ToolGroup { impl ToolGroup { /// True if any call in the group is still pending. pub(crate) fn any_pending(&self) -> bool { - self.calls - .iter() - .any(|c| c.status == ToolResultStatus::Pending) + self.calls.iter().any(|c| c.status == ToolResultStatus::Pending) } } @@ -135,7 +132,9 @@ pub(crate) enum ToolRenderData { preview: Option, }, /// File read operation. - FileRead { path: PathBuf }, + FileRead { + path: PathBuf, + }, /// File edit (str_replace) operation. FileEdit { path: PathBuf, @@ -152,7 +151,9 @@ pub(crate) enum ToolRenderData { filter_modes: Vec, }, /// Skill loading — read-only, auto-approved. - SkillLoad { _name: String }, + SkillLoad { + _name: String, + }, /// Server-side tool — no client rendering data available. Remote, } @@ -214,9 +215,15 @@ pub(crate) struct UiTurn { #[derive(Debug)] pub(crate) enum UiTurnKind { - User { events: Vec }, - Agent { events: Vec }, - OutOfBand { events: Vec }, + User { + events: Vec, + }, + Agent { + events: Vec, + }, + OutOfBand { + events: Vec, + }, } pub(crate) struct TurnBuilder<'a> { @@ -388,11 +395,7 @@ impl<'a> TurnBuilder<'a> { } fn add_suggested_command(&mut self, input: &serde_json::Value) { - let command = input - .get("command") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let command = input.get("command").and_then(|v| v.as_str()).unwrap_or("").to_string(); if command.is_empty() { return; @@ -401,39 +404,23 @@ impl<'a> TurnBuilder<'a> { self.start_agent_turn(); { let events = self.current_events_mut(); - let danger_level = input - .get("danger") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let danger_level = + input.get("danger").and_then(|v| v.as_str()).unwrap_or("").to_string(); - let confidence_level = input - .get("confidence") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let confidence_level = + input.get("confidence").and_then(|v| v.as_str()).unwrap_or("").to_string(); - let danger_notes = input - .get("danger_notes") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let danger_notes = + input.get("danger_notes").and_then(|v| v.as_str()).unwrap_or("").to_string(); - let confidence_notes = input - .get("confidence_notes") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let confidence_notes = + input.get("confidence_notes").and_then(|v| v.as_str()).unwrap_or("").to_string(); let danger = DangerLevel::from((&danger_level, &danger_notes)); let confidence = ConfidenceLevel::from((&confidence_level, &confidence_notes)); events.push(UiEvent::SuggestedCommand(SuggestedCommandDetails { - command: input - .get("command") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(), + command: input.get("command").and_then(|v| v.as_str()).unwrap_or("").to_string(), danger_level: danger, confidence_level: confidence, })); @@ -444,13 +431,12 @@ impl<'a> TurnBuilder<'a> { let render_data = self.build_render_data(id, name); self.start_agent_turn(); - self.current_events_mut() - .push(UiEvent::ToolCall(ToolCallDetails { - tool_use_id: id.to_string(), - name: name.to_string(), - status: ToolResultStatus::Pending, - render_data, - })); + self.current_events_mut().push(UiEvent::ToolCall(ToolCallDetails { + tool_use_id: id.to_string(), + name: name.to_string(), + status: ToolResultStatus::Pending, + render_data, + })); } /// Build tool-type-specific render data from the ToolTracker. @@ -513,11 +499,10 @@ impl<'a> TurnBuilder<'a> { fn add_out_of_band_output(&mut self, _name: &str, command: Option<&str>, content: &str) { self.start_out_of_band_turn(); - self.current_events_mut() - .push(UiEvent::OutOfBandOutput(OutOfBandOutputDetails { - command: command.map(|c| c.to_string()), - content: content.to_string(), - })); + self.current_events_mut().push(UiEvent::OutOfBandOutput(OutOfBandOutputDetails { + command: command.map(|c| c.to_string()), + content: content.to_string(), + })); } } @@ -553,11 +538,8 @@ impl ToolSummary { pub(crate) fn summary(&self) -> String { if self.any_pending() { // Find the last pending tool for the active verb - if let Some(pending) = self - .tool_calls - .iter() - .rev() - .find(|t| t.status == ToolResultStatus::Pending) + if let Some(pending) = + self.tool_calls.iter().rev().find(|t| t.status == ToolResultStatus::Pending) { return Self::progressive_verb(&pending.name); } @@ -572,9 +554,7 @@ impl ToolSummary { /// Determines if the spinner should be spinning pub(crate) fn any_pending(&self) -> bool { - self.tool_calls - .iter() - .any(|tool_call| tool_call.status == ToolResultStatus::Pending) + self.tool_calls.iter().any(|tool_call| tool_call.status == ToolResultStatus::Pending) } /// Present-tense progressive verb for a tool name (e.g. "Searching...") diff --git a/crates/atuin-ai/src/usage.rs b/crates/atuin-ai/src/usage.rs index cb37faa47..0692732a3 100644 --- a/crates/atuin-ai/src/usage.rs +++ b/crates/atuin-ai/src/usage.rs @@ -106,17 +106,15 @@ pub(crate) async fn fetch_usage(endpoint: &reqwest::Url, token: &str) -> Result< eyre::bail!("usage request failed ({status})"); } - response - .json::() - .await - .context("failed to parse usage response") + response.json::().await.context("failed to parse usage response") } #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[test] fn deserializes_server_payload() { // Shape documented in the hub's CliUsageController / credits_payload. @@ -146,10 +144,7 @@ mod tests { }; let json = serde_json::to_string(&snapshot).unwrap(); - assert_eq!( - serde_json::from_str::(&json).unwrap(), - snapshot - ); + assert_eq!(serde_json::from_str::(&json).unwrap(), snapshot); } #[test] diff --git a/crates/atuin-ai/src/user_context/interpolate.rs b/crates/atuin-ai/src/user_context/interpolate.rs index 2c8753d33..06d47940b 100644 --- a/crates/atuin-ai/src/user_context/interpolate.rs +++ b/crates/atuin-ai/src/user_context/interpolate.rs @@ -98,9 +98,7 @@ pub(crate) async fn interpolate(source: &str, shell: &str) -> String { for cmd in &commands { let shell = shell.to_string(); let body = cmd.body.clone(); - handles.push(tokio::spawn( - async move { run_command(&shell, &body).await }, - )); + handles.push(tokio::spawn(async move { run_command(&shell, &body).await })); } // Collect results. @@ -137,10 +135,7 @@ pub(crate) async fn interpolate(source: &str, shell: &str) -> String { async fn run_command(shell: &str, body: &str) -> String { let result = tokio::time::timeout( COMMAND_TIMEOUT, - tokio::process::Command::new(shell) - .arg("-c") - .arg(body) - .output(), + tokio::process::Command::new(shell).arg("-c").arg(body).output(), ) .await; @@ -164,28 +159,23 @@ async fn run_command(shell: &str, body: &str) -> String { } } Ok(Err(e)) => format!("[error: {e}]"), - Err(_) => format!( - "[error: command timed out after {}s]", - COMMAND_TIMEOUT.as_secs() - ), + Err(_) => format!("[error: command timed out after {}s]", COMMAND_TIMEOUT.as_secs()), } } #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[test] fn parse_inline_command() { let source = "Branch: !`git branch --show-current`"; let cmds = parse_commands(source); assert_eq!(cmds.len(), 1); assert_eq!(cmds[0].body, "git branch --show-current"); - assert_eq!( - &source[cmds[0].range.clone()], - "!`git branch --show-current`" - ); + assert_eq!(&source[cmds[0].range.clone()], "!`git branch --show-current`"); } #[rstest] diff --git a/crates/atuin-ai/src/user_context/mod.rs b/crates/atuin-ai/src/user_context/mod.rs index 2dba9600c..a747d75a4 100644 --- a/crates/atuin-ai/src/user_context/mod.rs +++ b/crates/atuin-ai/src/user_context/mod.rs @@ -92,9 +92,7 @@ impl UserContextCache { /// the cached value is only ever replaced wholesale — it can't be torn. /// Recover with the inner value rather than propagating the panic. fn lock(&self) -> std::sync::MutexGuard<'_, CacheSlot> { - self.inner - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) + self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner) } } @@ -181,9 +179,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("TERMINAL.md"); // The embedded sleep holds the gather open while we invalidate. - tokio::fs::write(&file, "!`sleep 0.5 && echo one`") - .await - .unwrap(); + tokio::fs::write(&file, "!`sleep 0.5 && echo one`").await.unwrap(); let cache = UserContextCache::default(); @@ -214,9 +210,7 @@ mod tests { async fn slow_gather_does_not_overwrite_concurrent_result() { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("TERMINAL.md"); - tokio::fs::write(&file, "!`sleep 0.5 && echo one`") - .await - .unwrap(); + tokio::fs::write(&file, "!`sleep 0.5 && echo one`").await.unwrap(); let cache = UserContextCache::default(); diff --git a/crates/atuin-client/benches/history_store.rs b/crates/atuin-client/benches/history_store.rs index 304fb1b48..6c8a6516c 100644 --- a/crates/atuin-client/benches/history_store.rs +++ b/crates/atuin-client/benches/history_store.rs @@ -15,13 +15,11 @@ const BATCH_SIZES: [usize; 2] = [1, 100]; #[divan::bench(args = BATCH_SIZES, min_time = 1)] fn serialize(bencher: divan::Bencher, n: usize) { - bencher - .with_inputs(|| records(n)) - .bench_values(|records: Vec| { - for record in &records { - divan::black_box(record.serialize().unwrap()); - } - }); + bencher.with_inputs(|| records(n)).bench_values(|records: Vec| { + for record in &records { + divan::black_box(record.serialize().unwrap()); + } + }); } #[divan::bench(args = BATCH_SIZES, min_time = 1)] @@ -44,8 +42,5 @@ fn deserialize(bencher: divan::Bencher, n: usize) { fn records(n: usize) -> Vec { let mut ctx = BenchCtx::new(); - BenchHistory::count(&mut ctx, n) - .into_iter() - .map(HistoryRecord::Create) - .collect() + BenchHistory::count(&mut ctx, n).into_iter().map(HistoryRecord::Create).collect() } diff --git a/crates/atuin-client/benches/ordering.rs b/crates/atuin-client/benches/ordering.rs index 074cd0985..33a61caeb 100644 --- a/crates/atuin-client/benches/ordering.rs +++ b/crates/atuin-client/benches/ordering.rs @@ -1,8 +1,9 @@ -use crate::_util::context::BenchCtx; -use crate::history::BenchHistory; use atuin_client::database::DbSearchMode; use atuin_client::ordering::reorder_fuzzy; +use crate::_util::context::BenchCtx; +use crate::history::BenchHistory; + /// reorder_fuzzy is invoked on every keystroke during interactive fuzzy search, so keeping the /// performance in check is important. /// diff --git a/crates/atuin-client/benches/record/sqlite_store.rs b/crates/atuin-client/benches/record/sqlite_store.rs index 850f6af0c..9eb7e81e8 100644 --- a/crates/atuin-client/benches/record/sqlite_store.rs +++ b/crates/atuin-client/benches/record/sqlite_store.rs @@ -33,18 +33,10 @@ impl BenchRecord { let host = Host::new(HostId(uuid_v7())); let version: String = "v1".into(); let tag = uuid_v7().simple().to_string(); - let data: String = ctx - .rng() - .sample_iter(&Alphanumeric) - .take(Self::PAYLOAD_SIZE) - .map(char::from) - .collect(); - let key: String = ctx - .rng() - .sample_iter(&Alphanumeric) - .take(Self::KEY_SIZE) - .map(char::from) - .collect(); + let data: String = + ctx.rng().sample_iter(&Alphanumeric).take(Self::PAYLOAD_SIZE).map(char::from).collect(); + let key: String = + ctx.rng().sample_iter(&Alphanumeric).take(Self::KEY_SIZE).map(char::from).collect(); (0..n as u64) .map(|idx| { @@ -74,9 +66,7 @@ impl BenchSqliteStore { async fn new() -> Self { let dir = tempfile::tempdir().unwrap(); let db_path = dir.path().join("bench.db"); - let store = SqliteStore::new(db_path, Self::SQL_TIMEOUT_S) - .await - .unwrap(); + let store = SqliteStore::new(db_path, Self::SQL_TIMEOUT_S).await.unwrap(); Self { _temp_dir: dir, @@ -101,7 +91,6 @@ fn push_batch(bencher: divan::Bencher, n: usize) { (db, records) }) .bench_values(|(db, records)| { - rt.block_on(db.sqlite_store.push_batch(records.iter())) - .unwrap(); + rt.block_on(db.sqlite_store.push_batch(records.iter())).unwrap(); }); } diff --git a/crates/atuin-client/src/api_client.rs b/crates/atuin-client/src/api_client.rs index 073aae493..c7f84d6cf 100644 --- a/crates/atuin-client/src/api_client.rs +++ b/crates/atuin-client/src/api_client.rs @@ -3,12 +3,6 @@ use std::env; use std::sync::Arc; use std::time::Duration; -use eyre::{Result, bail}; -use reqwest::{ - Response, StatusCode, Url, - header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue, USER_AGENT}, -}; - use atuin_common::url::UrlAppendExt; use atuin_domain::api::{ ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ATUIN_VERSION, ChangePasswordRequest, ErrorResponse, @@ -19,7 +13,9 @@ use atuin_domain::caps::{CapClient, CapMismatch, CapabilitiesExt}; use atuin_domain::record::{ EncryptedData, HostId, Record, RecordId, RecordIdx, RecordStatus, RecordTag, }; - +use eyre::{Result, bail}; +use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue, USER_AGENT}; +use reqwest::{Response, StatusCode, Url}; use reqwest_middleware::ClientWithMiddleware; use semver::Version; @@ -82,7 +78,8 @@ pub(crate) fn client_builder(extra_headers: &HashMap) -> reqwest if !same_origin { attempt.error( - "refusing to follow cross-origin redirect: extra_headers are configured and will not be sent to a different origin", + "refusing to follow cross-origin redirect: extra_headers are configured and will \ + not be sent to a different origin", ) } else if attempt.previous().len() > 10 { attempt.error("too many redirects") @@ -173,11 +170,7 @@ pub async fn latest_version() -> Result { let url = crate::settings::DEFAULT_SYNC_URL.clone(); let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(USER_AGENT, APP_USER_AGENT) - .send() - .await?; + let resp = client.get(url).header(USER_AGENT, APP_USER_AGENT).send().await?; let resp = handle_resp_error(resp).await?; let index = resp.json::().await?; @@ -203,7 +196,8 @@ pub fn ensure_version(response: &Response) -> Result { // If the client is newer than the server if version.major < ATUIN_VERSION.major { println!( - "Atuin version mismatch! In order to successfully sync, the server needs to run a newer version of Atuin" + "Atuin version mismatch! In order to successfully sync, the server needs to run a \ + newer version of Atuin" ); println!("Client: {ATUIN_CARGO_VERSION}"); println!("Server: {version}"); @@ -239,12 +233,14 @@ async fn handle_resp_error(resp: Response) -> Result { } bail!( - "There was an error with the atuin sync service at {url}, server error {status}: {reason}.\nIf the problem persists, contact the host" + "There was an error with the atuin sync service at {url}, server error {status}: \ + {reason}.\nIf the problem persists, contact the host" ); } bail!( - "There was an error with the atuin sync service at {url}, Status {status:?}.\nResponse body: {body}\nIf the problem persists, contact the host" + "There was an error with the atuin sync service at {url}, Status \ + {status:?}.\nResponse body: {body}\nIf the problem persists, contact the host" ); } @@ -260,14 +256,9 @@ pub fn caps_client( headers.insert(USER_AGENT, APP_USER_AGENT.parse()?); headers.insert(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION.parse()?); - let http = client_builder(extra_headers) - .default_headers(headers) - .build()?; + let http = client_builder(extra_headers).default_headers(headers).build()?; - Ok(CapClient::new( - sync_addr.append_path("api/v0/capabilities")?, - http, - )) + Ok(CapClient::new(sync_addr.append_path("api/v0/capabilities")?, http)) } impl Client { @@ -374,9 +365,7 @@ impl Client { /// Confirm a packfile body upload with the server. async fn confirm_packfile(&self, manifest_id: RecordId) -> Result<()> { let path = format!("api/v0/packfiles/{}/confirm", manifest_id.0); - let url = self - .sync_addr - .append(path.split('/').filter(|s| !s.is_empty()))?; + let url = self.sync_addr.append(path.split('/').filter(|s| !s.is_empty()))?; let resp = self.client.post(url).send().await?; handle_resp_error(resp).await?; Ok(()) @@ -389,12 +378,7 @@ impl Client { packfile: impl Into, ) -> Result<()> { // Not self.client: S3 rejects presigned requests that also carry an Authorization header. - let resp = self - .lfs_client - .put(upload_url.clone()) - .body(packfile) - .send() - .await?; + let resp = self.lfs_client.put(upload_url.clone()).body(packfile).send().await?; handle_resp_error(resp).await?; Ok(()) } @@ -402,9 +386,7 @@ impl Client { async fn get_packfile_download_url(&self, manifest_id: RecordId) -> Result { // `append_path` takes `&'static str`; the manifest id is dynamic, so inline its logic. let path = format!("api/v0/packfiles/{}", manifest_id.0); - let url = self - .sync_addr - .append(path.split('/').filter(|s| !s.is_empty()))?; + let url = self.sync_addr.append(path.split('/').filter(|s| !s.is_empty()))?; let resp = self.client.get(url).send().await?; let resp = handle_resp_error(resp).await?; @@ -506,9 +488,10 @@ impl Client { #[cfg(test)] mod tests { - use super::*; use rstest::*; + use super::*; + #[fixture] fn extra_headers() -> HashMap { let mut extra = HashMap::new(); @@ -564,7 +547,8 @@ mod tests { serve_one( &listener, format!( - "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{}/\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{}/\r\nContent-Length: \ + 0\r\nConnection: close\r\n\r\n", port + 1 ), ) @@ -572,16 +556,9 @@ mod tests { }); let client = client_builder(&extra_headers).build().unwrap(); - let err = client - .get(format!("http://127.0.0.1:{port}/")) - .send() - .await - .unwrap_err(); + let err = client.get(format!("http://127.0.0.1:{port}/")).send().await.unwrap_err(); - assert!( - err.is_redirect(), - "expected a redirect policy error: {err:?}" - ); + assert!(err.is_redirect(), "expected a redirect policy error: {err:?}"); } #[rstest] @@ -596,7 +573,8 @@ mod tests { serve_one( &listener, format!( - "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{port}/ok\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + "HTTP/1.1 302 Found\r\nLocation: \ + http://127.0.0.1:{port}/ok\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" ), ) .await; @@ -608,11 +586,7 @@ mod tests { }); let client = client_builder(&extra_headers).build().unwrap(); - let resp = client - .get(format!("http://127.0.0.1:{port}/")) - .send() - .await - .unwrap(); + let resp = client.get(format!("http://127.0.0.1:{port}/")).send().await.unwrap(); assert_eq!(resp.status(), 200); assert_eq!(resp.url().path(), "/ok"); @@ -645,15 +619,8 @@ mod tests { let addr: Url = server.uri().parse().unwrap(); let caps = caps_client(&addr, &HashMap::new()).unwrap(); - let client = Client::new( - addr, - AuthToken::Token("t".into()), - 30, - 30, - &HashMap::new(), - caps, - ) - .unwrap(); + let client = + Client::new(addr, AuthToken::Token("t".into()), 30, 30, &HashMap::new(), caps).unwrap(); // The client observes the server's advertised packfile cap; a second read stays warm // (the mock expects a single capabilities fetch). diff --git a/crates/atuin-client/src/auth.rs b/crates/atuin-client/src/auth.rs index c8ea46e31..dbc07bee2 100644 --- a/crates/atuin-client/src/auth.rs +++ b/crates/atuin-client/src/auth.rs @@ -1,15 +1,15 @@ use std::collections::HashMap; -use enum_dispatch::enum_dispatch; -use eyre::{Context, Result, bail}; -use reqwest::{StatusCode, Url, header::USER_AGENT}; -use serde::Deserialize; - use atuin_common::url::UrlAppendExt; use atuin_domain::api::{ ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ChangePasswordRequest, LoginRequest, LoginResponse, RegisterResponse, }; +use enum_dispatch::enum_dispatch; +use eyre::{Context, Result, bail}; +use reqwest::header::USER_AGENT; +use reqwest::{StatusCode, Url}; +use serde::Deserialize; use crate::settings::Settings; @@ -44,10 +44,7 @@ pub enum MutateResponse { /// CLI commands use this trait so they don't need to know which backend is /// active — they just prompt for input and call these methods. #[enum_dispatch] -#[allow( - async_fn_in_trait, - reason = "only used within our code and we don't need it to be Send" -)] +#[allow(async_fn_in_trait, reason = "only used within our code and we don't need it to be Send")] pub trait AuthClient: Send + Sync { /// Log in with username + password, optionally providing a TOTP code. async fn login( @@ -87,10 +84,7 @@ pub enum AnyAuthClient { pub async fn auth_client(settings: &Settings) -> AnyAuthClient { if settings.is_hub_sync() { let endpoint = settings.hub_endpoint(); - AnyAuthClient::Hub(HubAuthClient::new( - &endpoint, - settings.hub_session_token().await.ok(), - )) + AnyAuthClient::Hub(HubAuthClient::new(&endpoint, settings.hub_session_token().await.ok())) } else { AnyAuthClient::Legacy(LegacyAuthClient::new( &settings.sync_address, @@ -132,16 +126,10 @@ impl LegacyAuthClient { } fn authenticated_client(&self) -> Result { - let token = self - .session_token - .as_deref() - .ok_or_else(|| eyre::eyre!("Not logged in"))?; + let token = self.session_token.as_deref().ok_or_else(|| eyre::eyre!("Not logged in"))?; let mut headers = crate::api_client::extra_headers_map(&self.extra_headers)?; - headers.insert( - reqwest::header::AUTHORIZATION, - format!("Token {token}").parse()?, - ); + headers.insert(reqwest::header::AUTHORIZATION, format!("Token {token}").parse()?); headers.insert(USER_AGENT, APP_USER_AGENT.parse()?); headers.insert(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION.parse()?); @@ -232,11 +220,8 @@ impl AuthClient for LegacyAuthClient { let client = self.authenticated_client()?; let url = self.address.append(["account"])?; - let resp = client - .delete(url) - .json(&serde_json::json!({ "password": password })) - .send() - .await?; + let resp = + client.delete(url).json(&serde_json::json!({ "password": password })).send().await?; match resp.status().as_u16() { 200 => Ok(MutateResponse::Success), @@ -373,16 +358,13 @@ impl AuthClient for HubAuthClient { totp_code: Option<&str>, ) -> Result { let hub_token = self.hub_token.as_deref().ok_or_else(|| { - eyre::eyre!( - "Not logged in to Atuin Hub. \ - Please run 'atuin login' to authenticate." - ) + eyre::eyre!("Not logged in to Atuin Hub. Please run 'atuin login' to authenticate.") })?; if !hub_token.starts_with("atapi_") { bail!( - "Your Hub session token is invalid. \ - Please run 'atuin login' to re-authenticate with Atuin Hub." + "Your Hub session token is invalid. Please run 'atuin login' to re-authenticate \ + with Atuin Hub." ); } @@ -444,16 +426,13 @@ impl AuthClient for HubAuthClient { totp_code: Option<&str>, ) -> Result { let hub_token = self.hub_token.as_deref().ok_or_else(|| { - eyre::eyre!( - "Not logged in to Atuin Hub. \ - Please run 'atuin login' to authenticate." - ) + eyre::eyre!("Not logged in to Atuin Hub. Please run 'atuin login' to authenticate.") })?; if !hub_token.starts_with("atapi_") { bail!( - "Your Hub session token is invalid. \ - Please run 'atuin login' to re-authenticate with Atuin Hub." + "Your Hub session token is invalid. Please run 'atuin login' to re-authenticate \ + with Atuin Hub." ); } diff --git a/crates/atuin-client/src/database.rs b/crates/atuin-client/src/database.rs index ee8b97098..380222ec4 100644 --- a/crates/atuin-client/src/database.rs +++ b/crates/atuin-client/src/database.rs @@ -1,38 +1,29 @@ -use std::{ - env, - path::{Path, PathBuf}, - str::FromStr, - time::Duration, -}; +use std::env; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::time::Duration; -use crate::history::{AuthorPattern, KNOWN_AGENTS}; use async_trait::async_trait; use atuin_common::filter::{self, OrFilter}; use atuin_common::time::OffsetDateTimeExt; use atuin_common::utils; use fs_err as fs; use itertools::Itertools; -use sql_builder::{SqlBuilder, SqlName, bind::Bind, esc, quote}; -use sqlx::{ - Result, Row, - sqlite::{ - SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, - SqliteSynchronous, - }, +use sql_builder::bind::Bind; +use sql_builder::{SqlBuilder, SqlName, esc, quote}; +use sqlx::sqlite::{ + SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, + SqliteSynchronous, }; +use sqlx::{Result, Row}; use time::OffsetDateTime; use uuid::Uuid; -use crate::{ - history::{HistoryId, HistoryStats}, - utils::get_host_user, -}; - -use super::{ - history::History, - ordering, - settings::{FilterMode, SearchMode, Settings}, -}; +use super::history::History; +use super::ordering; +use super::settings::{FilterMode, SearchMode, Settings}; +use crate::history::{AuthorPattern, HistoryId, HistoryStats, KNOWN_AGENTS}; +use crate::utils::get_host_user; #[derive(Clone)] pub struct Context { @@ -89,7 +80,8 @@ pub async fn query_context() -> eyre::Result { pub async fn current_context() -> eyre::Result { if env::var("ATUIN_SESSION").is_err() { return Err(eyre::eyre!( - "Failed to find $ATUIN_SESSION in the environment. Check that you have correctly set up your shell." + "Failed to find $ATUIN_SESSION in the environment. Check that you have correctly set \ + up your shell." )); } @@ -116,30 +108,19 @@ fn apply_author_filter(sql: &mut SqlBuilder, authors: OrFilter<&[AuthorPattern]> filter::Items::Some(a) => a, }; - let author_expr = "CASE \ - WHEN author IS NULL OR trim(author) = '' THEN \ - CASE \ - WHEN instr(hostname, ':') > 0 THEN substr(hostname, instr(hostname, ':') + 1) \ - ELSE hostname \ - END \ - ELSE author \ - END"; + let author_expr = "CASE WHEN author IS NULL OR trim(author) = '' THEN CASE WHEN \ + instr(hostname, ':') > 0 THEN substr(hostname, instr(hostname, ':') + 1) \ + ELSE hostname END ELSE author END"; let mut agent_list: Option = None; let get_agent_list = || KNOWN_AGENTS.iter().map(quote).join(", "); let mut conditions = authors.iter().map(|author| match author { AuthorPattern::AllUser => { - format!( - "{author_expr} NOT IN ({})", - agent_list.get_or_insert_with(get_agent_list) - ) + format!("{author_expr} NOT IN ({})", agent_list.get_or_insert_with(get_agent_list)) } AuthorPattern::AllAgent => { - format!( - "{author_expr} IN ({})", - agent_list.get_or_insert_with(get_agent_list) - ) + format!("{author_expr} IN ({})", agent_list.get_or_insert_with(get_agent_list)) } AuthorPattern::Name(name) => { format!("{author_expr} = {}", quote(name)) @@ -299,7 +280,8 @@ impl Sqlite { if utils::broken_symlink(path) { eprintln!( - "Atuin: Sqlite db path ({path:?}) is a broken symlink. Unable to read or create replacement." + "Atuin: Sqlite db path ({path:?}) is a broken symlink. Unable to read or create \ + replacement." ); std::process::exit(1); } @@ -329,9 +311,7 @@ impl Sqlite { } pub async fn sqlite_version(&self) -> Result { - sqlx::query_scalar("SELECT sqlite_version()") - .fetch_one(&self.pool) - .await + sqlx::query_scalar("SELECT sqlite_version()").fetch_one(&self.pool).await } async fn setup_db(pool: &SqlitePool) -> Result<()> { @@ -477,7 +457,8 @@ impl Database for Sqlite { sqlx::query( "update history - set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8, author = ?9, intent = ?10, deleted_at = ?11 + set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = \ + ?7, hostname = ?8, author = ?9, intent = ?10, deleted_at = ?11 where id = ?1", ) .bind(h.id.0.as_str()) @@ -490,7 +471,7 @@ impl Database for Sqlite { .bind(h.hostname.as_str()) .bind(h.author.as_str()) .bind(h.intent.as_deref()) - .bind(h.deleted_at.map(|t|t.unix_timestamp_nanos() as i64)) + .bind(h.deleted_at.map(|t| t.unix_timestamp_nanos() as i64)) .execute(&self.pool) .await?; @@ -574,11 +555,12 @@ impl Database for Sqlite { debug!("listing history from {:?} to {:?}", from, to); let res = sqlx::query( - "select * from history where timestamp >= ?1 and timestamp <= ?2 order by timestamp asc", + "select * from history where timestamp >= ?1 and timestamp <= ?2 order by timestamp \ + asc", ) .bind(from.unix_timestamp_nanos() as i64) .bind(to.unix_timestamp_nanos() as i64) - .map(Self::query_history) + .map(Self::query_history) .fetch_all(&self.pool) .await?; @@ -716,25 +698,17 @@ impl Database for Sqlite { sql.and_where("command regexp ?".bind(®ex)); } - filter_options - .exit - .map(|exit| sql.and_where_eq("exit", exit)); + filter_options.exit.map(|exit| sql.and_where_eq("exit", exit)); - filter_options - .exclude_exit - .map(|exclude_exit| sql.and_where_ne("exit", exclude_exit)); + filter_options.exclude_exit.map(|exclude_exit| sql.and_where_ne("exit", exclude_exit)); if filter_options.only_failed { sql.and_where("exit != 0 AND exit != -1"); } - filter_options - .cwd - .map(|cwd| sql.and_where_eq("cwd", quote(cwd))); + filter_options.cwd.map(|cwd| sql.and_where_eq("cwd", quote(cwd))); - filter_options - .exclude_cwd - .map(|exclude_cwd| sql.and_where_ne("cwd", quote(exclude_cwd))); + filter_options.exclude_cwd.map(|exclude_cwd| sql.and_where_ne("cwd", quote(exclude_cwd))); if let Some(before) = filter_options.before { let parsed = @@ -791,13 +765,9 @@ impl Database for Sqlite { format!("SELECT * FROM ({inner}) f ORDER BY f.timestamp {order}{tail}") } else { format!( - "SELECT * FROM ({inner}) f \ - WHERE NOT EXISTS ( \ - SELECT 1 FROM ({inner}) f2 \ - WHERE f2.command = f.command \ - AND (f2.timestamp, f2.id) > (f.timestamp, f.id) \ - ) \ - ORDER BY f.timestamp {order}{tail}" + "SELECT * FROM ({inner}) f WHERE NOT EXISTS ( SELECT 1 FROM ({inner}) f2 WHERE \ + f2.command = f.command AND (f2.timestamp, f2.id) > (f.timestamp, f.id) ) ORDER \ + BY f.timestamp {order}{tail}" ) }; @@ -919,10 +889,7 @@ impl Database for Sqlite { average.field("avg(duration)").and_where("command = ?1"); let mut exits = SqlBuilder::select_from("history"); - exits - .fields(&["exit", "count(1) as count"]) - .and_where("command = ?1") - .group_by("exit"); + exits.fields(&["exit", "count(1) as count"]).and_where("command = ?1").group_by("exit"); // rewrite the following with sqlbuilder let mut day_of_week = SqlBuilder::select_from("history"); @@ -954,9 +921,8 @@ impl Database for Sqlite { let average = average.sql().expect("issue in stats previous query"); let exits = exits.sql().expect("issue in stats exits query"); let day_of_week = day_of_week.sql().expect("issue in stats day of week query"); - let duration_over_time = duration_over_time - .sql() - .expect("issue in stats duration over time query"); + let duration_over_time = + duration_over_time.sql().expect("issue in stats duration over time query"); // The queries are all independent, so run them concurrently on the pool. let (prev, next, total, average, exits, day_of_week, duration_over_time): ( @@ -978,27 +944,17 @@ impl Database for Sqlite { .bind(&h.session) .map(Self::query_history) .fetch_optional(&self.pool), - sqlx::query_as(sqlx::AssertSqlSafe(total)) - .bind(&h.command) - .fetch_one(&self.pool), - sqlx::query_as(sqlx::AssertSqlSafe(average)) - .bind(&h.command) - .fetch_one(&self.pool), - sqlx::query_as(sqlx::AssertSqlSafe(exits)) - .bind(&h.command) - .fetch_all(&self.pool), - sqlx::query_as(sqlx::AssertSqlSafe(day_of_week)) - .bind(&h.command) - .fetch_all(&self.pool), + sqlx::query_as(sqlx::AssertSqlSafe(total)).bind(&h.command).fetch_one(&self.pool), + sqlx::query_as(sqlx::AssertSqlSafe(average)).bind(&h.command).fetch_one(&self.pool), + sqlx::query_as(sqlx::AssertSqlSafe(exits)).bind(&h.command).fetch_all(&self.pool), + sqlx::query_as(sqlx::AssertSqlSafe(day_of_week)).bind(&h.command).fetch_all(&self.pool), sqlx::query_as(sqlx::AssertSqlSafe(duration_over_time)) .bind(&h.command) .fetch_all(&self.pool), )?; - let duration_over_time = duration_over_time - .iter() - .map(|f| (f.0.clone(), f.1.round() as i64)) - .collect(); + let duration_over_time = + duration_over_time.iter().map(|f| (f.0.clone(), f.1.round() as i64)).collect(); Ok(HistoryStats { next, @@ -1073,9 +1029,7 @@ impl Paged { // We want to deduplicate on command, but the user can search via cwd, hostname, and session. // Without those fields, filter modes won't work right. With those fields, we get duplicates. // This must be handled upstream. - query - .group_by("command, cwd, hostname, session") - .having("max(timestamp)"); + query.group_by("command, cwd, hostname, session").having("max(timestamp)"); } query.limit(self.page_size); @@ -1229,11 +1183,13 @@ impl<'a> Iterator for QueryTokenizer<'a> { #[cfg(test)] mod test { + use std::time::{Duration, Instant}; + + use rstest::{fixture, rstest}; + use time::format_description::well_known::Rfc3339; + use super::*; use crate::settings::test_local_timeout; - use rstest::{fixture, rstest}; - use std::time::{Duration, Instant}; - use time::format_description::well_known::Rfc3339; fn new_context() -> Context { Context { @@ -1247,9 +1203,7 @@ mod test { #[fixture] async fn empty_db() -> Sqlite { - Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap() + Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap() } async fn assert_search_eq( @@ -1262,15 +1216,9 @@ mod test { let context = new_context(); let results = db - .search( - mode, - filter_mode, - &context, - query, - OptFilters { - ..Default::default() - }, - ) + .search(mode, filter_mode, &context, query, OptFilters { + ..Default::default() + }) .await?; assert_eq!( @@ -1290,9 +1238,8 @@ mod test { query: &str, expected_commands: Vec<&str>, ) { - let results = assert_search_eq(db, mode, filter_mode, query, expected_commands.len()) - .await - .unwrap(); + let results = + assert_search_eq(db, mode, filter_mode, query, expected_commands.len()).await.unwrap(); let commands: Vec<&str> = results.iter().map(|a| a.command.as_str()).collect(); assert_eq!(commands, expected_commands); } @@ -1370,18 +1317,12 @@ mod test { save_history_item(&db, "ls /home/frank").await; // No range -> everything. - let all = db - .list(&[], &context, None, false, false, None) - .await - .unwrap(); + let all = db.list(&[], &context, None, false, false, None).await.unwrap(); assert_eq!(all.len(), 2); // A zero-width window on the item's exact timestamp matches it, because the // bounds are inclusive (`timestamp >= from AND timestamp <= to`). - let hits = db - .list(&[], &context, None, false, false, Some((at, at))) - .await - .unwrap(); + let hits = db.list(&[], &context, None, false, false, Some((at, at))).await.unwrap(); assert_eq!(hits.len(), 1); assert_eq!(hits[0].command, "ls /home/ellie"); } @@ -1397,10 +1338,7 @@ mod test { let bravo = save_history_item(&db, "echo bravo").await; let _charlie = save_history_item(&db, "echo charlie").await; - let loaded = db - .load_active(&[alpha.id.clone(), bravo.id.clone()]) - .await - .unwrap(); + let loaded = db.load_active(&[alpha.id.clone(), bravo.id.clone()]).await.unwrap(); let mut commands: Vec = loaded.into_iter().map(|h| h.command).collect(); commands.sort(); @@ -1437,10 +1375,7 @@ mod test { alpha.command = String::new(); db.update(&alpha).await.unwrap(); - let loaded = db - .load_active(&[alpha.id.clone(), bravo.id.clone()]) - .await - .unwrap(); + let loaded = db.load_active(&[alpha.id.clone(), bravo.id.clone()]).await.unwrap(); assert_eq!(loaded.len(), 1); assert_eq!(loaded[0].command, "echo bravo"); @@ -1484,9 +1419,7 @@ mod test { } async fn db_with(commands: &[&str]) -> Sqlite { - let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); for command in commands { new_history_item(&mut db, command).await.unwrap(); @@ -1510,41 +1443,25 @@ mod test { ) { let t = OffsetDateTime::from_unix_timestamp(1708330400).unwrap(); - let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); - new_history_item_at(&mut db, "ls /home/ellie", Some(t)) - .await - .unwrap(); - new_history_item_at(&mut db, "ls /home/frank", None) - .await - .unwrap(); + let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); + new_history_item_at(&mut db, "ls /home/ellie", Some(t)).await.unwrap(); + new_history_item_at(&mut db, "ls /home/frank", None).await.unwrap(); let context = new_context(); - let stamp = |seconds: i64| { - (t + time::Duration::seconds(seconds)) - .format(&Rfc3339) - .unwrap() - }; + let stamp = |seconds: i64| (t + time::Duration::seconds(seconds)).format(&Rfc3339).unwrap(); let (after, before) = match offsets { Some((after, before)) => (Some(stamp(after)), Some(stamp(before))), None => (None, None), }; let results = db - .search( - DbSearchMode::FullText, - FilterMode::Global, - &context, - "", - OptFilters { - after: after.as_deref(), - before: before.as_deref(), - include_duplicates: true, - ..Default::default() - }, - ) + .search(DbSearchMode::FullText, FilterMode::Global, &context, "", OptFilters { + after: after.as_deref(), + before: before.as_deref(), + include_duplicates: true, + ..Default::default() + }) .await .unwrap(); @@ -1567,16 +1484,10 @@ mod test { let context = new_context(); let hits = db - .search( - DbSearchMode::FullText, - FilterMode::Global, - &context, - "", - OptFilters { - include_duplicates, - ..Default::default() - }, - ) + .search(DbSearchMode::FullText, FilterMode::Global, &context, "", OptFilters { + include_duplicates, + ..Default::default() + }) .await .unwrap(); @@ -1598,15 +1509,8 @@ mod test { _ => unreachable!(), } - let result = db - .search( - DbSearchMode::FullText, - FilterMode::Global, - &context, - "", - filters, - ) - .await; + let result = + db.search(DbSearchMode::FullText, FilterMode::Global, &context, "", filters).await; assert!(result.is_err(), "unparsable `{which}` filter must error"); } @@ -1619,15 +1523,9 @@ mod test { async fn test_search_prefix(#[case] query: &str, #[case] expected: usize) { let db = db_with(&["ls /home/ellie"]).await; - assert_search_eq( - &db, - DbSearchMode::Prefix, - FilterMode::Global, - query, - expected, - ) - .await - .unwrap(); + assert_search_eq(&db, DbSearchMode::Prefix, FilterMode::Global, query, expected) + .await + .unwrap(); } #[rstest] @@ -1649,15 +1547,9 @@ mod test { async fn test_search_fulltext(#[case] query: &str, #[case] expected: usize) { let db = db_with(&["ls /home/ellie"]).await; - assert_search_eq( - &db, - DbSearchMode::FullText, - FilterMode::Global, - query, - expected, - ) - .await - .unwrap(); + assert_search_eq(&db, DbSearchMode::FullText, FilterMode::Global, query, expected) + .await + .unwrap(); } #[rstest] @@ -1694,15 +1586,9 @@ mod test { ]) .await; - assert_search_eq( - &db, - DbSearchMode::Fuzzy, - FilterMode::Global, - query, - expected, - ) - .await - .unwrap(); + assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, query, expected) + .await + .unwrap(); } #[rstest] @@ -1719,21 +1605,13 @@ mod test { new_history_item(&mut db, "corburl").await.unwrap(); // if fuzzy reordering is on, it should come back in a more sensible order - assert_search_commands( - &db, - DbSearchMode::Fuzzy, - FilterMode::Global, - "curl", - vec!["curl", "corburl"], - ) + assert_search_commands(&db, DbSearchMode::Fuzzy, FilterMode::Global, "curl", vec![ + "curl", "corburl", + ]) .await; - assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, "xxxx", 0) - .await - .unwrap(); - assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, "", 2) - .await - .unwrap(); + assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, "xxxx", 0).await.unwrap(); + assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, "", 2).await.unwrap(); } #[rstest] @@ -1769,9 +1647,7 @@ mod test { #[case::daemon_fuzzy(SearchMode::DaemonFuzzy)] #[tokio::test(flavor = "multi_thread")] async fn test_search_interactive_only_modes_rank_like_fuzzy(#[case] mode: SearchMode) { - let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); // "corburl" is strictly newer, so an unranked query would return it first and the assertion // below would fail. @@ -1779,17 +1655,11 @@ mod test { new_history_item_at(&mut db, "curl", Some(now - time::Duration::seconds(10))) .await .unwrap(); - new_history_item_at(&mut db, "corburl", Some(now)) - .await - .unwrap(); + new_history_item_at(&mut db, "corburl", Some(now)).await.unwrap(); - assert_search_commands( - &db, - mode.closest_db_mode(), - FilterMode::Global, - "curl", - vec!["curl", "corburl"], - ) + assert_search_commands(&db, mode.closest_db_mode(), FilterMode::Global, "curl", vec![ + "curl", "corburl", + ]) .await; } @@ -1800,9 +1670,7 @@ mod test { #[case::trailing_space("screen ")] #[tokio::test(flavor = "multi_thread")] async fn test_search_fuzzy_trailing_space(#[case] query: &str) { - let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); let now = OffsetDateTime::now_utc(); let irssi = "screen irssi"; @@ -1810,12 +1678,8 @@ mod test { let ls_ld = "ls -ld secrets/rendered"; let screen_r = "screen -r"; - new_history_item_at(&mut db, irssi, Some(now - time::Duration::days(5))) - .await - .unwrap(); - new_history_item_at(&mut db, ls_l, Some(now - time::Duration::days(4))) - .await - .unwrap(); + new_history_item_at(&mut db, irssi, Some(now - time::Duration::days(5))).await.unwrap(); + new_history_item_at(&mut db, ls_l, Some(now - time::Duration::days(4))).await.unwrap(); new_history_item_at( &mut db, ls_ld, @@ -1823,13 +1687,10 @@ mod test { ) .await .unwrap(); - new_history_item_at(&mut db, screen_r, Some(now - time::Duration::hours(1))) - .await - .unwrap(); + new_history_item_at(&mut db, screen_r, Some(now - time::Duration::hours(1))).await.unwrap(); - let results = assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, query, 4) - .await - .unwrap(); + let results = + assert_search_eq(&db, DbSearchMode::Fuzzy, FilterMode::Global, query, 4).await.unwrap(); assert_eq!( results[0].command, screen_r, @@ -1855,25 +1716,15 @@ mod test { #[case] close: &str, #[case] far: &str, ) { - let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let mut db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); let now = OffsetDateTime::now_utc(); - new_history_item_at(&mut db, close, Some(now - time::Duration::days(5))) - .await - .unwrap(); - new_history_item_at(&mut db, far, Some(now - time::Duration::hours(1))) - .await - .unwrap(); + new_history_item_at(&mut db, close, Some(now - time::Duration::days(5))).await.unwrap(); + new_history_item_at(&mut db, far, Some(now - time::Duration::hours(1))).await.unwrap(); - assert_search_commands( - &db, - DbSearchMode::Fuzzy, - FilterMode::Global, - query, - vec![close, far], - ) + assert_search_commands(&db, DbSearchMode::Fuzzy, FilterMode::Global, query, vec![ + close, far, + ]) .await; } @@ -1883,13 +1734,9 @@ mod test { async fn test_search_fuzzy_operator() { let db = db_with(&["use screen", "screenshot tool"]).await; - assert_search_commands( - &db, - DbSearchMode::Fuzzy, - FilterMode::Global, - "screen$", - vec!["use screen"], - ) + assert_search_commands(&db, DbSearchMode::Fuzzy, FilterMode::Global, "screen$", vec![ + "use screen", + ]) .await; } @@ -1903,9 +1750,7 @@ mod test { let mut db = db; // Add 5 history items for i in 0..5 { - new_history_item(&mut db, &format!("command{}", i)) - .await - .unwrap(); + new_history_item(&mut db, &format!("command{}", i)).await.unwrap(); } // Create a paged iterator with page_size of 2 @@ -2003,11 +1848,7 @@ mod test { .await .unwrap(); - let to_delete = all - .iter() - .find(|h| h.command == "delete_me") - .unwrap() - .clone(); + let to_delete = all.iter().find(|h| h.command == "delete_me").unwrap().clone(); db.delete(to_delete).await.unwrap(); // Deletes remove the row outright, so both views should get 2 @@ -2044,21 +1885,13 @@ mod test { let context = new_context(); for _i in 1..10000 { - new_history_item(&mut db, "i am a duplicated command") - .await - .unwrap(); + new_history_item(&mut db, "i am a duplicated command").await.unwrap(); } let start = Instant::now(); let _results = db - .search( - DbSearchMode::Fuzzy, - FilterMode::Global, - &context, - "", - OptFilters { - ..Default::default() - }, - ) + .search(DbSearchMode::Fuzzy, FilterMode::Global, &context, "", OptFilters { + ..Default::default() + }) .await .unwrap(); let duration = start.elapsed(); @@ -2079,9 +1912,7 @@ mod test { #[case] shells: [&str; N], #[case] expected_count: usize, ) { - let db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); for (command, shell) in [ ("echo unknown1", None), @@ -2117,13 +1948,7 @@ mod test { }; let results = db - .search( - DbSearchMode::FullText, - FilterMode::Global, - &context, - "echo", - filters, - ) + .search(DbSearchMode::FullText, FilterMode::Global, &context, "echo", filters) .await .unwrap(); @@ -2142,9 +1967,7 @@ mod test { #[case] authors: [&str; N], #[case] expected_count: usize, ) { - let db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); for (command, author) in [ ("echo alice1", "alice"), @@ -2178,13 +2001,7 @@ mod test { }; let results = db - .search( - DbSearchMode::FullText, - FilterMode::Global, - &context, - "echo", - filters, - ) + .search(DbSearchMode::FullText, FilterMode::Global, &context, "echo", filters) .await .unwrap(); diff --git a/crates/atuin-client/src/distro.rs b/crates/atuin-client/src/distro.rs index dead8355f..40e9d3219 100644 --- a/crates/atuin-client/src/distro.rs +++ b/crates/atuin-client/src/distro.rs @@ -27,27 +27,19 @@ fn detect_from_os_release() -> Option { } fn detect_from_debian_version() -> Option { - std::fs::read_to_string("/etc/debian_version") - .ok() - .map(|v| format!("Debian {}", v.trim())) + std::fs::read_to_string("/etc/debian_version").ok().map(|v| format!("Debian {}", v.trim())) } fn detect_from_centos_release() -> Option { - std::fs::read_to_string("/etc/centos-release") - .ok() - .map(|v| v.trim().to_string()) + std::fs::read_to_string("/etc/centos-release").ok().map(|v| v.trim().to_string()) } fn detect_from_redhat_release() -> Option { - std::fs::read_to_string("/etc/redhat-release") - .ok() - .map(|v| v.trim().to_string()) + std::fs::read_to_string("/etc/redhat-release").ok().map(|v| v.trim().to_string()) } fn detect_from_fedora_release() -> Option { - std::fs::read_to_string("/etc/fedora-release") - .ok() - .map(|v| v.trim().to_string()) + std::fs::read_to_string("/etc/fedora-release").ok().map(|v| v.trim().to_string()) } fn detect_from_arch_release() -> Option { @@ -58,9 +50,7 @@ fn detect_from_arch_release() -> Option { } fn detect_from_alpine_release() -> Option { - std::fs::read_to_string("/etc/alpine-release") - .ok() - .map(|v| format!("Alpine {}", v.trim())) + std::fs::read_to_string("/etc/alpine-release").ok().map(|v| format!("Alpine {}", v.trim())) } fn detect_from_suse_release() -> Option { diff --git a/crates/atuin-client/src/history.rs b/crates/atuin-client/src/history.rs index 7cbb71aa9..7352ab9d0 100644 --- a/crates/atuin-client/src/history.rs +++ b/crates/atuin-client/src/history.rs @@ -7,7 +7,6 @@ use atuin_common::rmp::encode::{self, ByteBuf, EncodeError}; use atuin_common::time::OffsetDateTimeExt; use atuin_common::utils::{normalize_optional_string, uuid_v7}; use atuin_domain::record::DecryptedData; - use eyre::{Result, bail}; use time::OffsetDateTime; @@ -199,9 +198,7 @@ pub struct HistoryStats { impl History { pub(crate) fn author_from_hostname(hostname: &str) -> String { - hostname - .split_once(':') - .map_or_else(|| hostname.to_owned(), |(_, user)| user.to_owned()) + hostname.split_once(':').map_or_else(|| hostname.to_owned(), |(_, user)| user.to_owned()) } #[allow(clippy::too_many_arguments)] @@ -516,14 +513,14 @@ impl History { #[cfg(test)] mod tests { + use atuin_common::filter::OrFilter; use regex::RegexSet; use rstest::*; use time::macros::datetime; - use crate::{history::Version, settings::Settings}; - use super::{AuthorPattern, History, all_user_author_filter, is_known_agent}; - use atuin_common::filter::OrFilter; + use crate::history::Version; + use crate::settings::Settings; /// Whether an author filter permits `author`, mirroring the SQL that /// [`apply_author_filter`](crate::database::OptFilters::authors) builds. @@ -658,11 +655,7 @@ mod tests { })] fn serialize_deserialize_roundtrip(#[case] history: History) { let serialized = history.serialize().expect("failed to serialize history"); - assert_eq!( - &serialized.0[0..3], - [205, 0, 2], - "should encode as history v2" - ); + assert_eq!(&serialized.0[0..3], [205, 0, 2], "should encode as history v2"); let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) .expect("failed to deserialize history"); @@ -741,10 +734,7 @@ mod tests { if decode_as == source { assert_eq!(got.unwrap(), expected, "{decode_as}"); } else { - assert!( - got.is_err(), - "unexpected success deserializing as {decode_as}" - ); + assert!(got.is_err(), "unexpected success deserializing as {decode_as}"); } } } diff --git a/crates/atuin-client/src/history/store.rs b/crates/atuin-client/src/history/store.rs index 4ec864c92..a6729bdcb 100644 --- a/crates/atuin-client/src/history/store.rs +++ b/crates/atuin-client/src/history/store.rs @@ -1,20 +1,19 @@ -use std::{collections::HashSet, fmt::Write, time::Duration}; +use std::collections::HashSet; +use std::fmt::Write; +use std::time::Duration; +use atuin_common::encryption::paseto_v4; use atuin_common::rmp::decode::Bytes; +use atuin_domain::record::{ + DecryptedData, Host, HostId, Record, RecordId, RecordIdx, RecordTag, RecordVersion, +}; use eyre::{Result, bail, eyre}; use futures::{Stream, StreamExt, TryStreamExt, future, stream}; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; -use crate::{ - database::{Database, current_context}, - record::sqlite_store::SqliteStore, -}; -use atuin_common::encryption::paseto_v4; -use atuin_domain::record::{ - DecryptedData, Host, HostId, Record, RecordId, RecordIdx, RecordTag, RecordVersion, -}; - use super::{History, HistoryId, Version}; +use crate::database::{Database, current_context}; +use crate::record::sqlite_store::SqliteStore; #[derive(Debug, Clone)] pub struct HistoryStore { @@ -128,11 +127,8 @@ impl HistoryStore { async fn push_record(&self, record: HistoryRecord) -> Result<(RecordId, RecordIdx)> { let bytes = record.serialize()?; - let idx = self - .store - .last(self.host_id, &RecordTag::History) - .await? - .map_or(0, |p| p.idx + 1); + let idx = + self.store.last(self.host_id, &RecordTag::History).await?.map_or(0, |p| p.idx + 1); let record = Record::builder() .host(Host::new(self.host_id)) @@ -144,9 +140,7 @@ impl HistoryStore { let id = record.id; - self.store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.store.push(&record.encrypt(&self.encryption_key)).await?; Ok((id, idx)) } @@ -154,11 +148,8 @@ impl HistoryStore { async fn push_batch(&self, records: impl Iterator) -> Result<()> { let mut ret = Vec::new(); - let idx = self - .store - .last(self.host_id, &RecordTag::History) - .await? - .map_or(0, |p| p.idx + 1); + let idx = + self.store.last(self.host_id, &RecordTag::History).await?.map_or(0, |p| p.idx + 1); // Could probably _also_ do this as an iterator, but let's see how this is for now. // optimizing for minimal sqlite transactions, this code can be optimised later @@ -243,7 +234,9 @@ impl HistoryStore { if skipped > 0 { // library code that may run under the TUI or shell hooks, so no stderr here warn!( - "skipped {skipped} history records that could not be decrypted or decoded. Run `atuin store verify` to check your store, and `atuin store purge` to remove broken records locally." + "skipped {skipped} history records that could not be decrypted or decoded. Run \ + `atuin store verify` to check your store, and `atuin store purge` to remove \ + broken records locally." ); } @@ -336,9 +329,7 @@ impl HistoryStore { /// Use this when you want the database writes but not the values. Callers that need /// the created entries should use [`HistoryStore::incremental_build`] directly. pub async fn build_all(&self, database: &dyn Database, ids: &[RecordId]) -> Result<()> { - self.incremental_build(database, ids) - .try_for_each(|_| future::ready(Ok(()))) - .await + self.incremental_build(database, ids).try_for_each(|_| future::ready(Ok(()))).await } /// Get a list of history IDs that exist in the store @@ -412,14 +403,12 @@ mod tests { use rstest::*; use time::macros::datetime; - use crate::{ - database::Sqlite, - history::{Version, store::HistoryRecord, store::HistoryStore}, - record::sqlite_store::SqliteStore, - settings::test_local_timeout, - }; - use super::History; + use crate::database::Sqlite; + use crate::history::Version; + use crate::history::store::{HistoryRecord, HistoryStore}; + use crate::record::sqlite_store::SqliteStore; + use crate::settings::test_local_timeout; /// The identical `History` literal used by both async tests. #[fixture] @@ -446,9 +435,7 @@ mod tests { /// and the `HistoryStore` layered on it must originate from a single fixture. #[fixture] async fn stores() -> (SqliteStore, HostId, HistoryStore) { - let store = SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap(); + let store = SqliteStore::new(":memory:", test_local_timeout()).await.unwrap(); let host_id = HostId(atuin_common::utils::uuid_v7()); let history_store = HistoryStore::new(store.clone(), host_id, [0u8; 32].into()); (store, host_id, history_store) @@ -531,10 +518,7 @@ mod tests { .data(DecryptedData(vec![1, 2, 3])) .build(); - store - .push(&corrupt.encrypt(&[1u8; 32].into())) - .await - .unwrap(); + store.push(&corrupt.encrypt(&[1u8; 32].into())).await.unwrap(); let records = history_store.history().await.unwrap(); @@ -555,15 +539,10 @@ mod tests { // `history.id` (the HistoryId). This distinction is the whole bug. let (record_id, _) = history_store.push(history.clone()).await.unwrap(); - let db = Sqlite::new("sqlite::memory:", test_local_timeout()) - .await - .unwrap(); + let db = Sqlite::new("sqlite::memory:", test_local_timeout()).await.unwrap(); - let created: Vec = history_store - .incremental_build(&db, &[record_id]) - .try_collect() - .await - .unwrap(); + let created: Vec = + history_store.incremental_build(&db, &[record_id]).try_collect().await.unwrap(); assert_eq!(created.len(), 1); assert_eq!(created[0], history); diff --git a/crates/atuin-client/src/hub.rs b/crates/atuin-client/src/hub.rs index 0f4be6b50..ad890c59f 100644 --- a/crates/atuin-client/src/hub.rs +++ b/crates/atuin-client/src/hub.rs @@ -10,14 +10,14 @@ use std::time::Duration; -use eyre::{Context, Result, bail}; -use reqwest::{StatusCode, Url, header::USER_AGENT}; -use thiserror::Error; - use atuin_common::url::UrlAppendExt; use atuin_domain::api::{ ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, CliCodeResponse, CliVerifyResponse, ErrorResponse, }; +use eyre::{Context, Result, bail}; +use reqwest::header::USER_AGENT; +use reqwest::{StatusCode, Url}; +use thiserror::Error; use crate::settings::Settings; @@ -185,11 +185,7 @@ pub async fn save_session(token: &str) -> Result<()> { /// Delete the hub session token (logout from Hub) pub async fn delete_session() -> Result<()> { - Settings::meta_store() - .await? - .delete_hub_session() - .await - .context("Failed to delete hub session") + Settings::meta_store().await?.delete_hub_session().await.context("Failed to delete hub session") } /// Check if the user is logged in with Hub authentication @@ -266,11 +262,7 @@ async fn handle_resp_error(resp: reqwest::Response) -> Result() - .await - .ok() - .map(|e| e.reason.into_owned()); + let reason = resp.json::().await.ok().map(|e| e.reason.into_owned()); Err(HubError::Status { status, reason }) } diff --git a/crates/atuin-client/src/import/bash.rs b/crates/atuin-client/src/import/bash.rs index 9cbbfd309..9fa9f9ec2 100644 --- a/crates/atuin-client/src/import/bash.rs +++ b/crates/atuin-client/src/import/bash.rs @@ -1,4 +1,5 @@ -use std::{path::PathBuf, str}; +use std::path::PathBuf; +use std::str; use async_trait::async_trait; use directories::UserDirs; @@ -67,9 +68,7 @@ impl Importer for Bash { let backfill = timestamp_increment * u32::try_from(commands_before_first_timestamp).unwrap_or(u32::MAX); // a timestamp near the start of the representable range would underflow - let mut next_timestamp = first_timestamp - .checked_sub(backfill) - .unwrap_or(first_timestamp); + let mut next_timestamp = first_timestamp.checked_sub(backfill).unwrap_or(first_timestamp); for line in lines.into_iter() { match line { @@ -78,22 +77,20 @@ impl Importer for Bash { LineType::Timestamp(t) => { if t < next_timestamp { warn!( - "Time reversal detected in Bash history! Commands may be ordered incorrectly." + "Time reversal detected in Bash history! Commands may be ordered \ + incorrectly." ); } next_timestamp = t; } LineType::Command(c) => { - let imported = History::import() - .shell("bash") - .timestamp(next_timestamp) - .command(c); + let imported = + History::import().shell("bash").timestamp(next_timestamp).command(c); h.push(imported.build().into()).await?; // a timestamp near the end of the representable range would overflow - next_timestamp = next_timestamp - .checked_add(timestamp_increment) - .unwrap_or(next_timestamp); + next_timestamp = + next_timestamp.checked_add(timestamp_increment).unwrap_or(next_timestamp); } } } @@ -141,9 +138,9 @@ mod test { use itertools::{Itertools, assert_equal}; use rstest::rstest; - use crate::import::{Importer, tests::TestLoader}; - use super::Bash; + use crate::import::Importer; + use crate::import::tests::TestLoader; #[rstest] #[case::no_timestamps( @@ -174,10 +171,7 @@ mod test { let mut loader = TestLoader::default(); bash.load(&mut loader).await.unwrap(); - assert_equal( - loader.buf.iter().map(|h| h.command.as_str()), - expected_commands, - ); + assert_equal(loader.buf.iter().map(|h| h.command.as_str()), expected_commands); assert!(is_strictly_sorted(loader.buf.iter().map(|h| h.timestamp))); } @@ -199,14 +193,14 @@ cd ../ let mut loader = TestLoader::default(); bash.load(&mut loader).await.unwrap(); - assert_equal( - loader.buf.iter().map(|h| h.command.as_str()), - ["git reset", "git clean -dxf", "cd ../"], - ); - assert_equal( - loader.buf.iter().map(|h| h.timestamp.unix_timestamp()), - [1672918999, 1672919006, 1672919020], - ) + assert_equal(loader.buf.iter().map(|h| h.command.as_str()), [ + "git reset", + "git clean -dxf", + "cd ../", + ]); + assert_equal(loader.buf.iter().map(|h| h.timestamp.unix_timestamp()), [ + 1672918999, 1672919006, 1672919020, + ]) } fn is_strictly_sorted(iter: impl IntoIterator) -> bool @@ -235,10 +229,10 @@ cargo update let mut loader = TestLoader::default(); bash.load(&mut loader).await.unwrap(); - assert_equal( - loader.buf.iter().map(|h| h.command.as_str()), - ["cargo install atuin", "cargo update"], - ); + assert_equal(loader.buf.iter().map(|h| h.command.as_str()), [ + "cargo install atuin", + "cargo update", + ]); } #[rstest] @@ -264,9 +258,6 @@ cargo update // the increment saturates at the maximum representable instant rather than // wrapping or resetting to the epoch - assert_eq!( - loader.buf.last().unwrap().timestamp.unix_timestamp(), - 253_402_300_799 - ); + assert_eq!(loader.buf.last().unwrap().timestamp.unix_timestamp(), 253_402_300_799); } } diff --git a/crates/atuin-client/src/import/fish.rs b/crates/atuin-client/src/import/fish.rs index 604015a1c..51a598aff 100644 --- a/crates/atuin-client/src/import/fish.rs +++ b/crates/atuin-client/src/import/fish.rs @@ -20,10 +20,8 @@ pub struct Fish { /// see fn default_histpath() -> Result { let base = BaseDirs::new().ok_or_else(|| eyre!("could not determine data directory"))?; - let data = std::env::var("XDG_DATA_HOME").map_or_else( - |_| base.home_dir().join(".local").join("share"), - PathBuf::from, - ); + let data = std::env::var("XDG_DATA_HOME") + .map_or_else(|_| base.home_dir().join(".local").join("share"), PathBuf::from); // fish supports multiple history sessions // If `fish_history` var is missing, or set to `default`, use `fish` as the session @@ -114,9 +112,9 @@ impl Importer for Fish { #[cfg(test)] mod test { - use crate::import::{Importer, tests::TestLoader}; - use super::Fish; + use crate::import::Importer; + use crate::import::tests::TestLoader; #[tokio::test] async fn parse_out_of_range_timestamp() { diff --git a/crates/atuin-client/src/import/mod.rs b/crates/atuin-client/src/import/mod.rs index a829188a7..0ce5ad21a 100644 --- a/crates/atuin-client/src/import/mod.rs +++ b/crates/atuin-client/src/import/mod.rs @@ -104,20 +104,14 @@ fn is_file(p: PathBuf) -> Result { if p.is_file() { Ok(p) } else { - bail!( - "Could not find history file {:?}. Try setting and exporting $HISTFILE", - p - ); + bail!("Could not find history file {:?}. Try setting and exporting $HISTFILE", p); } } fn is_dir(p: PathBuf) -> Result { if p.is_dir() { Ok(p) } else { - bail!( - "Could not find history directory {:?}. Try setting and exporting $HISTFILE", - p - ); + bail!("Could not find history directory {:?}. Try setting and exporting $HISTFILE", p); } } diff --git a/crates/atuin-client/src/import/nu.rs b/crates/atuin-client/src/import/nu.rs index bd1d47be7..ef798b00d 100644 --- a/crates/atuin-client/src/import/nu.rs +++ b/crates/atuin-client/src/import/nu.rs @@ -68,10 +68,7 @@ impl Importer for Nu { let cmd: String = s.replace("<\\n>", "\n"); - let entry = History::import() - .shell("nu") - .timestamp(current_timestamp) - .command(cmd); + let entry = History::import().shell("nu").timestamp(current_timestamp).command(cmd); h.push(entry.build().into()).await?; } diff --git a/crates/atuin-client/src/import/nu_histdb.rs b/crates/atuin-client/src/import/nu_histdb.rs index a7bc44908..00d48c075 100644 --- a/crates/atuin-client/src/import/nu_histdb.rs +++ b/crates/atuin-client/src/import/nu_histdb.rs @@ -7,7 +7,8 @@ use async_trait::async_trait; use atuin_common::time::OffsetDateTimeExt; use directories::BaseDirs; use eyre::{Result, eyre}; -use sqlx::{Pool, sqlite::SqlitePool}; +use sqlx::Pool; +use sqlx::sqlite::SqlitePool; use time::OffsetDateTime; use super::Importer; @@ -58,12 +59,9 @@ pub struct NuHistDb { /// Read db at given file, return vector of entries. async fn hist_from_db(dbpath: PathBuf) -> Result> { - let connection_str = dbpath.to_str().ok_or_else(|| { - eyre!( - "Invalid path for SQLite database: {}", - dbpath.to_string_lossy() - ) - })?; + let connection_str = dbpath + .to_str() + .ok_or_else(|| eyre!("Invalid path for SQLite database: {}", dbpath.to_string_lossy()))?; let pool = SqlitePool::connect(connection_str).await?; hist_from_db_conn(pool).await } @@ -76,9 +74,8 @@ async fn hist_from_db_conn(pool: Pool) -> Result> FROM history ORDER BY start_timestamp "#; - let histdb_vec: Vec = sqlx::query_as::<_, HistDbEntry>(query) - .fetch_all(&pool) - .await?; + let histdb_vec: Vec = + sqlx::query_as::<_, HistDbEntry>(query).fetch_all(&pool).await?; Ok(histdb_vec) } @@ -146,13 +143,7 @@ mod test { /// A corrupt row must degrade to something representable rather than /// panicking; the command is what matters and must always survive. #[rstest] - #[case::valid( - 1_639_162_832_500, - b"echo hello", - 1_639_162_832, - 500_000_000, - "echo hello" - )] + #[case::valid(1_639_162_832_500, b"echo hello", 1_639_162_832, 500_000_000, "echo hello")] #[case::out_of_range_timestamp(i64::MAX, b"echo hello", 0, 0, "echo hello")] #[case::invalid_utf8_command(0, &[0x65, 0x63, 0x68, 0x6f, 0xff], 0, 0, "echo\u{fffd}")] fn corrupt_rows_degrade( diff --git a/crates/atuin-client/src/import/powershell.rs b/crates/atuin-client/src/import/powershell.rs index f1fe1d0f7..8495125c8 100644 --- a/crates/atuin-client/src/import/powershell.rs +++ b/crates/atuin-client/src/import/powershell.rs @@ -1,7 +1,8 @@ +use std::path::PathBuf; + use async_trait::async_trait; use directories::BaseDirs; use eyre::{Result, eyre}; -use std::path::PathBuf; use time::{Duration, OffsetDateTime}; use super::{Importer, Loader, count_lines, unix_byte_lines}; @@ -27,17 +28,10 @@ fn get_history_path() -> Result { // > or `$Env:HOME/.local/share/powershell/PSReadLine`. let dir = if cfg!(windows) { - base.data_dir() - .join("Microsoft") - .join("Windows") - .join("PowerShell") - .join("PSReadLine") + base.data_dir().join("Microsoft").join("Windows").join("PowerShell").join("PSReadLine") } else { std::env::var("XDG_DATA_HOME") - .map_or_else( - |_| base.home_dir().join(".local").join("share"), - PathBuf::from, - ) + .map_or_else(|_| base.home_dir().join(".local").join("share"), PathBuf::from) .join("powershell") .join("PSReadLine") }; @@ -113,10 +107,8 @@ impl Importer for PowerShell { let offset = Duration::milliseconds(counter); counter += 1; - let entry = History::import() - .shell("powershell") - .timestamp(start + offset) - .command(cmd); + let entry = + History::import().shell("powershell").timestamp(start + offset).command(cmd); h.push(entry.build().into()).await?; } @@ -135,11 +127,12 @@ fn read_line(s: &[u8]) -> Result<&str> { #[cfg(test)] mod test { - use super::*; - use crate::import::tests::TestLoader; use itertools::assert_equal; use rstest::rstest; + use super::*; + use crate::import::tests::TestLoader; + const INPUT: &str = r#"cargo install atuin cargo update echo "first line` diff --git a/crates/atuin-client/src/import/replxx.rs b/crates/atuin-client/src/import/replxx.rs index eee19a5ce..7d1255ab2 100644 --- a/crates/atuin-client/src/import/replxx.rs +++ b/crates/atuin-client/src/import/replxx.rs @@ -1,9 +1,11 @@ -use std::{path::PathBuf, str}; +use std::path::PathBuf; +use std::str; use async_trait::async_trait; use directories::UserDirs; use eyre::{Result, eyre}; -use time::{OffsetDateTime, PrimitiveDateTime, macros::format_description}; +use time::macros::format_description; +use time::{OffsetDateTime, PrimitiveDateTime}; use super::{Importer, Loader, get_histfile_path, unix_byte_lines}; use crate::history::History; @@ -63,10 +65,8 @@ impl Importer for Replxx { None => { // replxx uses ETB character (0x17) as line breaker let cmd = s.replace('\u{0017}', "\n"); - let imported = History::import() - .shell("replxx") - .timestamp(timestamp) - .command(cmd); + let imported = + History::import().shell("replxx").timestamp(timestamp).command(cmd); h.push(imported.build().into()).await?; } @@ -92,9 +92,9 @@ fn try_parse_line_as_timestamp(line: &str) -> Option { #[cfg(test)] mod test { - use crate::import::{Importer, tests::TestLoader}; - use super::Replxx; + use crate::import::Importer; + use crate::import::tests::TestLoader; #[tokio::test] async fn parse_complex() { @@ -127,16 +127,15 @@ CREATE TABLE test( stamp DateTime('UTC'))ENGINE = MergeTreePARTITION BY toDat }; } - history!( - 1707603388, - "select * from remote('127.0.0.1:20222', view(select 1))" - ); + history!(1707603388, "select * from remote('127.0.0.1:20222', view(select 1))"); history!(1707603396, "select * from numbers(10)"); history!(1707603401, "select * from system.numbers"); history!(1707603568, "select 1"); history!( 1708600533, - "CREATE TABLE test\n( stamp DateTime('UTC'))\nENGINE = MergeTree\nPARTITION BY toDate(stamp)\norder by tuple() as select toDateTime('2020-01-01')+number*60 from numbers(80000);" + "CREATE TABLE test\n( stamp DateTime('UTC'))\nENGINE = MergeTree\nPARTITION BY \ + toDate(stamp)\norder by tuple() as select toDateTime('2020-01-01')+number*60 from \ + numbers(80000);" ); } @@ -149,10 +148,7 @@ CREATE TABLE test( stamp DateTime('UTC'))ENGINE = MergeTreePARTITION BY toDat let replxx = Replxx { bytes }; let mut loader = TestLoader::default(); - replxx - .load(&mut loader) - .await - .expect("import must not fail"); + replxx.load(&mut loader).await.expect("import must not fail"); let commands: Vec<&str> = loader.buf.iter().map(|h| h.command.as_str()).collect(); assert_eq!(commands, ["select 1", "select 2"]); diff --git a/crates/atuin-client/src/import/resh.rs b/crates/atuin-client/src/import/resh.rs index b81a4742e..5a9a1d1dd 100644 --- a/crates/atuin-client/src/import/resh.rs +++ b/crates/atuin-client/src/import/resh.rs @@ -1,12 +1,11 @@ use std::path::PathBuf; use async_trait::async_trait; +use atuin_common::time::OffsetDateTimeExt; +use atuin_common::utils::uuid_v7; use directories::UserDirs; use eyre::{Result, eyre}; use serde::Deserialize; - -use atuin_common::time::OffsetDateTimeExt; -use atuin_common::utils::uuid_v7; use time::OffsetDateTime; use super::{Importer, Loader, get_histfile_path, unix_byte_lines}; @@ -157,9 +156,10 @@ impl Importer for Resh { #[cfg(test)] mod test { + use rstest::rstest; + use super::*; use crate::import::tests::TestLoader; - use rstest::rstest; /// resh writes one JSON object per line. Every field on `ReshEntry` is /// required, so spell them all out once here. @@ -195,14 +195,8 @@ mod test { m.insert("timezoneAfter".into(), serde_json::json!("+0000")); m.insert("realtimeBefore".into(), serde_json::json!(realtime_before)); m.insert("realtimeAfter".into(), serde_json::json!(realtime_after)); - m.insert( - "realtimeBeforeLocal".into(), - serde_json::json!(realtime_before), - ); - m.insert( - "realtimeAfterLocal".into(), - serde_json::json!(realtime_after), - ); + m.insert("realtimeBeforeLocal".into(), serde_json::json!(realtime_before)); + m.insert("realtimeAfterLocal".into(), serde_json::json!(realtime_after)); m.insert("realtimeDuration".into(), serde_json::json!(0.0)); m.insert("realtimeSinceSessionStart".into(), serde_json::json!(0.0)); m.insert("realtimeSinceBoot".into(), serde_json::json!(0.0)); @@ -297,14 +291,7 @@ mod test { let mut loader = TestLoader::default(); resh.load(&mut loader).await.expect("import must not fail"); - assert_eq!( - loader - .buf - .iter() - .map(|h| h.command.as_str()) - .collect::>(), - [cmd] - ); + assert_eq!(loader.buf.iter().map(|h| h.command.as_str()).collect::>(), [cmd]); assert_eq!(loader.buf[0].timestamp.unix_timestamp(), expected_unix_ts); assert_eq!(loader.buf[0].duration, expected_duration); } diff --git a/crates/atuin-client/src/import/xonsh.rs b/crates/atuin-client/src/import/xonsh.rs index 89b021b4f..290f88f3a 100644 --- a/crates/atuin-client/src/import/xonsh.rs +++ b/crates/atuin-client/src/import/xonsh.rs @@ -9,7 +9,8 @@ use eyre::{Result, eyre}; use serde::Deserialize; use time::OffsetDateTime; use uuid::Uuid; -use uuid::timestamp::{Timestamp, context::NoContext}; +use uuid::timestamp::Timestamp; +use uuid::timestamp::context::NoContext; use super::{Importer, Loader, get_histdir_path}; use crate::history::History; @@ -147,17 +148,13 @@ mod tests { use time::macros::datetime; use super::*; - use crate::history::History; use crate::import::tests::TestLoader; #[test] fn test_hist_dir_xonsh() { let hist_dir = xonsh_hist_dir(Some("/home/user/xonsh_data".to_string())).unwrap(); - assert_eq!( - hist_dir, - PathBuf::from("/home/user/xonsh_data/history_json") - ); + assert_eq!(hist_dir, PathBuf::from("/home/user/xonsh_data/history_json")); } #[tokio::test] diff --git a/crates/atuin-client/src/import/xonsh_sqlite.rs b/crates/atuin-client/src/import/xonsh_sqlite.rs index 293380135..c256f3073 100644 --- a/crates/atuin-client/src/import/xonsh_sqlite.rs +++ b/crates/atuin-client/src/import/xonsh_sqlite.rs @@ -6,10 +6,12 @@ use atuin_common::time::OffsetDateTimeExt; use directories::BaseDirs; use eyre::{Result, eyre}; use futures::TryStreamExt; -use sqlx::{FromRow, Row, sqlite::SqlitePool}; +use sqlx::sqlite::SqlitePool; +use sqlx::{FromRow, Row}; use time::OffsetDateTime; use uuid::Uuid; -use uuid::timestamp::{Timestamp, context::NoContext}; +use uuid::timestamp::Timestamp; +use uuid::timestamp::context::NoContext; use super::{Importer, Loader, get_histfile_path}; use crate::history::History; @@ -67,10 +69,7 @@ fn xonsh_db_path(xonsh_data_dir: Option) -> Result { if hist_file.exists() || cfg!(test) { Ok(hist_file) } else { - Err(eyre!( - "Could not find xonsh history db at: {}", - hist_file.to_string_lossy() - )) + Err(eyre!("Could not find xonsh history db at: {}", hist_file.to_string_lossy())) } } @@ -89,10 +88,7 @@ impl Importer for XonshSqlite { let xonsh_data_dir = env::var("XONSH_DATA_DIR").ok(); let db_path = get_histfile_path(|| xonsh_db_path(xonsh_data_dir))?; let connection_str = db_path.to_str().ok_or_else(|| { - eyre!( - "Invalid path for SQLite database: {}", - db_path.to_string_lossy() - ) + eyre!("Invalid path for SQLite database: {}", db_path.to_string_lossy()) })?; let pool = SqlitePool::connect(connection_str).await?; @@ -134,17 +130,13 @@ mod tests { use time::macros::datetime; use super::*; - use crate::history::History; use crate::import::tests::TestLoader; #[test] fn test_db_path_xonsh() { let db_path = xonsh_db_path(Some("/home/user/xonsh_data".to_string())).unwrap(); - assert_eq!( - db_path, - PathBuf::from("/home/user/xonsh_data/xonsh-history.sqlite") - ); + assert_eq!(db_path, PathBuf::from("/home/user/xonsh_data/xonsh-history.sqlite")); } #[test] diff --git a/crates/atuin-client/src/import/zsh.rs b/crates/atuin-client/src/import/zsh.rs index b6e16b73a..719f2c849 100644 --- a/crates/atuin-client/src/import/zsh.rs +++ b/crates/atuin-client/src/import/zsh.rs @@ -76,16 +76,12 @@ impl Entry { }; }; - let time = time - .parse::() - .ok() - .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()); + let time = + time.parse::().ok().and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()); // use nanos, because why the hell not? we won't display them. // saturate rather than overflow on an implausible duration - let duration = duration - .parse::() - .map_or(-1, |t| t.saturating_mul(1_000_000_000)); + let duration = duration.parse::().map_or(-1, |t| t.saturating_mul(1_000_000_000)); Self { command: command.trim_end().to_owned(), @@ -139,18 +135,14 @@ impl Importer for Zsh { let backfill = u32::try_from(commands_until_timestamp).unwrap_or(u32::MAX) * timestamp_increment; // a timestamp near the start of the representable range would underflow - let mut timestamp = first_timestamp - .checked_sub(backfill) - .unwrap_or(first_timestamp); + let mut timestamp = first_timestamp.checked_sub(backfill).unwrap_or(first_timestamp); for entry in entries { if let Some(time) = entry.timestamp { timestamp = time; } else { // a timestamp near the end of the representable range would overflow - timestamp = timestamp - .checked_add(timestamp_increment) - .unwrap_or(timestamp); + timestamp = timestamp.checked_add(timestamp_increment).unwrap_or(timestamp); } let imported = History::import() @@ -190,9 +182,8 @@ mod test { use itertools::assert_equal; use rstest::rstest; - use crate::import::tests::TestLoader; - use super::*; + use crate::import::tests::TestLoader; #[rstest] #[case::zero_duration( @@ -254,10 +245,7 @@ mod test { let parsed = Entry::parse(line); assert_eq!(parsed.command, command); - assert_eq!( - parsed.timestamp.map(OffsetDateTime::unix_timestamp), - timestamp - ); + assert_eq!(parsed.timestamp.map(OffsetDateTime::unix_timestamp), timestamp); assert_eq!(parsed.duration, duration); } @@ -277,14 +265,11 @@ cargo update let mut loader = TestLoader::default(); zsh.load(&mut loader).await.unwrap(); - assert_equal( - loader.buf.iter().map(|h| h.command.as_str()), - [ - "cargo install atuin", - "cargo install atuin; \\\ncargo update", - "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", - ], - ); + assert_equal(loader.buf.iter().map(|h| h.command.as_str()), [ + "cargo install atuin", + "cargo install atuin; \\\ncargo update", + "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", + ]); } #[tokio::test] @@ -299,10 +284,10 @@ cargo update let mut loader = TestLoader::default(); zsh.load(&mut loader).await.unwrap(); - assert_equal( - loader.buf.iter().map(|h| h.command.as_str()), - ["cargo install atuin", "cargo update"], - ); + assert_equal(loader.buf.iter().map(|h| h.command.as_str()), [ + "cargo install atuin", + "cargo update", + ]); } #[tokio::test] @@ -333,10 +318,7 @@ cargo update assert_eq!(loader.buf[0].timestamp.unix_timestamp(), 253_402_300_799); // the increment saturates at the maximum representable instant rather than // wrapping or resetting to the epoch - assert_eq!( - loader.buf.last().unwrap().timestamp.unix_timestamp(), - 253_402_300_799 - ); + assert_eq!(loader.buf.last().unwrap().timestamp.unix_timestamp(), 253_402_300_799); } #[tokio::test] @@ -350,9 +332,6 @@ cargo update let mut loader = TestLoader::default(); zsh.load(&mut loader).await.unwrap(); - assert_equal( - loader.buf.iter().map(|h| h.command.as_str()), - ["echo 你好", "ls ~/音乐"], - ); + assert_equal(loader.buf.iter().map(|h| h.command.as_str()), ["echo 你好", "ls ~/音乐"]); } } diff --git a/crates/atuin-client/src/import/zsh_histdb.rs b/crates/atuin-client/src/import/zsh_histdb.rs index f418738ce..978dcbf6f 100644 --- a/crates/atuin-client/src/import/zsh_histdb.rs +++ b/crates/atuin-client/src/import/zsh_histdb.rs @@ -39,7 +39,8 @@ use async_trait::async_trait; use atuin_common::utils::uuid_v7; use directories::UserDirs; use eyre::{Result, eyre}; -use sqlx::{Pool, sqlite::SqlitePool}; +use sqlx::Pool; +use sqlx::sqlite::SqlitePool; use time::PrimitiveDateTime; use super::Importer; @@ -72,12 +73,9 @@ pub struct ZshHistDb { /// Read db at given file, return vector of entries. async fn hist_from_db(dbpath: PathBuf) -> Result> { - let connection_str = dbpath.to_str().ok_or_else(|| { - eyre!( - "Invalid path for SQLite database: {}", - dbpath.to_string_lossy() - ) - })?; + let connection_str = dbpath + .to_str() + .ok_or_else(|| eyre!("Invalid path for SQLite database: {}", dbpath.to_string_lossy()))?; let pool = SqlitePool::connect(connection_str).await?; hist_from_db_conn(pool).await } @@ -92,9 +90,8 @@ async fn hist_from_db_conn(pool: Pool) -> Result> LEFT JOIN places ON history.place_id = places.id ORDER BY history.start_time "#; - let histdb_vec: Vec = sqlx::query_as::<_, HistDbEntry>(query) - .fetch_all(&pool) - .await?; + let histdb_vec: Vec = + sqlx::query_as::<_, HistDbEntry>(query).fetch_all(&pool).await?; Ok(histdb_vec) } @@ -118,9 +115,7 @@ impl ZshHistDb { if histdb_path.exists() { Ok(histdb_path) } else { - Err(eyre!( - "Could not find history file. Try setting $HISTDB_FILE" - )) + Err(eyre!("Could not find history file. Try setting $HISTDB_FILE")) } } } @@ -182,9 +177,11 @@ impl Importer for ZshHistDb { #[cfg(test)] mod test { - use super::*; - use sqlx::sqlite::SqlitePoolOptions; use std::env; + + use sqlx::sqlite::SqlitePoolOptions; + + use super::*; #[tokio::test(flavor = "multi_thread")] #[allow(unsafe_code)] async fn test_env_vars() { @@ -203,9 +200,10 @@ mod test { #[tokio::test] async fn duration_saturates_instead_of_overflowing() { - use crate::import::tests::TestLoader; use time::macros::datetime; + use crate::import::tests::TestLoader; + // a corrupt duration column must not overflow when scaled to nanoseconds let histdb = ZshHistDb { histdb: vec![HistDbEntry { @@ -222,10 +220,7 @@ mod test { }; let mut loader = TestLoader::default(); - histdb - .load(&mut loader) - .await - .expect("import must not fail"); + histdb.load(&mut loader).await.expect("import must not fail"); assert_eq!(loader.buf.len(), 1); assert_eq!(loader.buf[0].command, "echo hello"); @@ -234,11 +229,8 @@ mod test { #[tokio::test(flavor = "multi_thread")] async fn test_import() { - let pool: SqlitePool = SqlitePoolOptions::new() - .min_connections(2) - .connect(":memory:") - .await - .unwrap(); + let pool: SqlitePool = + SqlitePoolOptions::new().min_connections(2).connect(":memory:").await.unwrap(); // sql dump directly from a test database. let db_sql = r#" diff --git a/crates/atuin-client/src/login.rs b/crates/atuin-client/src/login.rs index 574115394..e5247e3e5 100644 --- a/crates/atuin-client/src/login.rs +++ b/crates/atuin-client/src/login.rs @@ -2,7 +2,9 @@ use atuin_common::encryption::paseto_v4; use atuin_domain::api::LoginRequest; use eyre::Result; -use crate::{api_client, record::sqlite_store::SqliteStore, settings::Settings}; +use crate::api_client; +use crate::record::sqlite_store::SqliteStore; +use crate::settings::Settings; pub async fn login( settings: &Settings, @@ -43,10 +45,7 @@ pub async fn login( ) .await?; - Settings::meta_store() - .await? - .save_session(&session.session) - .await?; + Settings::meta_store().await?.save_session(&session.session).await?; Ok(session.session) } diff --git a/crates/atuin-client/src/logs.rs b/crates/atuin-client/src/logs.rs index b7ed86402..61d12c693 100644 --- a/crates/atuin-client/src/logs.rs +++ b/crates/atuin-client/src/logs.rs @@ -1,7 +1,9 @@ -use crate::settings; -use atuin_common::logs::{FileConfig, LogConfig}; use std::path::PathBuf; +use atuin_common::logs::{FileConfig, LogConfig}; + +use crate::settings; + pub trait FromSettings: Sized { type Output; fn from_settings(settings: &settings::Logs, child: &settings::LogConfig) -> Self::Output; diff --git a/crates/atuin-client/src/meta.rs b/crates/atuin-client/src/meta.rs index 9df338688..331c80a53 100644 --- a/crates/atuin-client/src/meta.rs +++ b/crates/atuin-client/src/meta.rs @@ -5,7 +5,8 @@ use std::time::Duration; use atuin_domain::record::HostId; use eyre::{Result, eyre}; use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions}; -use time::{OffsetDateTime, format_description::well_known::Rfc3339}; +use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; use tokio::sync::OnceCell; use uuid::Uuid; @@ -107,10 +108,7 @@ impl MetaStore { } pub async fn delete(&self, key: &str) -> Result<()> { - sqlx::query("DELETE FROM meta WHERE key = ?1") - .bind(key) - .execute(&self.pool) - .await?; + sqlx::query("DELETE FROM meta WHERE key = ?1").bind(key).execute(&self.pool).await?; Ok(()) } @@ -127,8 +125,7 @@ impl MetaStore { } let uuid = atuin_common::utils::uuid_v7(); - self.set(KEY_HOST_ID, uuid.as_simple().to_string().as_ref()) - .await?; + self.set(KEY_HOST_ID, uuid.as_simple().to_string().as_ref()).await?; Ok(HostId(uuid)) }) @@ -144,11 +141,7 @@ impl MetaStore { } pub async fn save_sync_time(&self) -> Result<()> { - self.set( - KEY_LAST_SYNC, - OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(), - ) - .await + self.set(KEY_LAST_SYNC, OffsetDateTime::now_utc().format(&Rfc3339)?.as_str()).await } pub async fn last_version_check(&self) -> Result { @@ -159,11 +152,7 @@ impl MetaStore { } pub async fn save_version_check_time(&self) -> Result<()> { - self.set( - KEY_LAST_VERSION_CHECK, - OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(), - ) - .await + self.set(KEY_LAST_VERSION_CHECK, OffsetDateTime::now_utc().format(&Rfc3339)?.as_str()).await } pub async fn latest_version(&self) -> Result> { @@ -295,9 +284,10 @@ impl MetaStore { #[cfg(test)] mod tests { - use super::*; use rstest::*; + use super::*; + #[fixture] async fn store() -> MetaStore { MetaStore::new("sqlite::memory:", 2.0).await.unwrap() @@ -357,10 +347,7 @@ mod tests { store.save_session("tok123").await.unwrap(); assert!(store.logged_in().await.unwrap()); - assert_eq!( - store.session_token().await.unwrap(), - Some("tok123".to_string()) - ); + assert_eq!(store.session_token().await.unwrap(), Some("tok123".to_string())); store.delete_session().await.unwrap(); assert!(!store.logged_in().await.unwrap()); @@ -372,9 +359,6 @@ mod tests { assert_eq!(store.latest_version().await.unwrap(), None); store.save_latest_version("1.2.3").await.unwrap(); - assert_eq!( - store.latest_version().await.unwrap(), - Some("1.2.3".to_string()) - ); + assert_eq!(store.latest_version().await.unwrap(), Some("1.2.3".to_string())); } } diff --git a/crates/atuin-client/src/ordering.rs b/crates/atuin-client/src/ordering.rs index 5003694b0..67d443a8d 100644 --- a/crates/atuin-client/src/ordering.rs +++ b/crates/atuin-client/src/ordering.rs @@ -1,6 +1,7 @@ use minspan::minspan; -use super::{database::DbSearchMode, history::History}; +use super::database::DbSearchMode; +use super::history::History; pub fn reorder_fuzzy(mode: DbSearchMode, query: &str, res: Vec) -> Vec { match mode { @@ -26,11 +27,12 @@ where #[cfg(test)] mod tests { - use super::*; - use crate::history::History; use rstest::rstest; use time::OffsetDateTime; + use super::*; + use crate::history::History; + fn hist(command: &str) -> History { History::capture() .timestamp(OffsetDateTime::now_utc()) diff --git a/crates/atuin-client/src/packfile/mod.rs b/crates/atuin-client/src/packfile/mod.rs index d037da608..66bb28a2d 100644 --- a/crates/atuin-client/src/packfile/mod.rs +++ b/crates/atuin-client/src/packfile/mod.rs @@ -110,7 +110,6 @@ mod record; mod sync; pub use packer::{PackingError, try_pack}; -pub use sync::{DownloadError, UploadError, download_packed, upload_packed}; - #[cfg(test)] pub(crate) use record::PackManifestRecordView; +pub use sync::{DownloadError, UploadError, download_packed, upload_packed}; diff --git a/crates/atuin-client/src/packfile/packer.rs b/crates/atuin-client/src/packfile/packer.rs index 2cafa1ad4..0d2040db8 100644 --- a/crates/atuin-client/src/packfile/packer.rs +++ b/crates/atuin-client/src/packfile/packer.rs @@ -1,12 +1,13 @@ //! Turns accumulated history into plaintext `packfile` manifest records. -use super::record::{PackManifestData, PackManifestDataV1, ParsingError}; -use crate::record::sqlite_store::SqliteStore; use atuin_domain::caps::PackfileCap; use atuin_domain::record::{Host, HostId, Record, RecordTag, RecordVersion}; use thiserror::Error; use tracing::{instrument, trace}; +use super::record::{PackManifestData, PackManifestDataV1, ParsingError}; +use crate::record::sqlite_store::SqliteStore; + #[derive(Debug, Error)] pub enum PackingError { #[error("error accessing the record store: {0}")] @@ -35,10 +36,7 @@ pub async fn try_pack( return Ok(()); }; - let last_pack = store - .last(host, &RecordTag::Packfile) - .await - .map_err(PackingError::Store)?; + let last_pack = store.last(host, &RecordTag::Packfile).await.map_err(PackingError::Store)?; // `start` is the first unpacked source idx; `pack_idx` is the next idx in the *packfile* // stream (a separate sequence). Both come from the same latest manifest record. @@ -48,11 +46,8 @@ pub async fn try_pack( }; let mut pack_idx = last_pack.map_or(0, |record| record.idx + 1); - let Some(ceiling) = store - .last(host, tag) - .await - .map_err(PackingError::Store)? - .map(|record| record.idx) + let Some(ceiling) = + store.last(host, tag).await.map_err(PackingError::Store)?.map(|record| record.idx) else { trace!("no history yet; nothing to pack"); return Ok(()); @@ -66,10 +61,7 @@ pub async fn try_pack( let mut cursor = start; while cursor <= ceiling && ceiling - cursor + 1 >= count { // The loop guard guarantees at least `count` records remain, so each run is exactly `count`. - let run = store - .next(host, tag, cursor, count) - .await - .map_err(PackingError::Store)?; + let run = store.next(host, tag, cursor, count).await.map_err(PackingError::Store)?; let Some(end) = run.last().map(|record| record.idx) else { break; @@ -101,19 +93,17 @@ pub async fn try_pack( #[cfg(test)] mod tests { - use super::*; use atuin_common::utils::uuid_v7; use atuin_domain::record::EncryptedData; use proptest::prelude::*; use rstest::{fixture, rstest}; + use super::*; use crate::settings::test_local_timeout; #[fixture] async fn store() -> SqliteStore { - SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap() + SqliteStore::new(":memory:", test_local_timeout()).await.unwrap() } #[fixture] @@ -181,10 +171,7 @@ mod tests { } fn runtime() -> tokio::runtime::Runtime { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() + tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap() } proptest! { diff --git a/crates/atuin-client/src/packfile/record.rs b/crates/atuin-client/src/packfile/record.rs index 80f7bd14e..708ca8d36 100644 --- a/crates/atuin-client/src/packfile/record.rs +++ b/crates/atuin-client/src/packfile/record.rs @@ -387,12 +387,13 @@ impl<'a> PackManifestRecordView<'a> { #[cfg(test)] mod tests { - use super::*; use atuin_common::utils::uuid_v7; use atuin_domain::record::Host; use rstest::{fixture, rstest}; use uuid::Uuid; + use super::*; + #[fixture] fn key() -> paseto_v4::Key { paseto_v4::Key::from([7u8; 32]) diff --git a/crates/atuin-client/src/packfile/sync.rs b/crates/atuin-client/src/packfile/sync.rs index e0c29a570..8b1cb3832 100644 --- a/crates/atuin-client/src/packfile/sync.rs +++ b/crates/atuin-client/src/packfile/sync.rs @@ -18,11 +18,10 @@ use atuin_common::encryption::paseto_v4; use atuin_domain::record::{EncryptedData, Record, RecordId, RecordTag}; use thiserror::Error; -use crate::{ - api_client::Client, packfile::record::ParsingError, record::sqlite_store::SqliteStore, -}; - use super::record::{PackManifestRecordView, PackingError, UnpackError}; +use crate::api_client::Client; +use crate::packfile::record::ParsingError; +use crate::record::sqlite_store::SqliteStore; #[derive(Debug, Error)] pub enum UploadError { @@ -50,10 +49,7 @@ pub async fn upload_packed( let (blob, ids) = view.pack_records(store, key.clone()).await?; - client - .upload_packfile(view.record.id, &ids, blob) - .await - .map_err(UploadError::Api) + client.upload_packfile(view.record.id, &ids, blob).await.map_err(UploadError::Api) } #[derive(Debug, Error)] @@ -95,10 +91,8 @@ pub async fn download_packed( let view = PackManifestRecordView::new(manifest)?; // Skip if we already have the whole range (history is contiguous, packfiles are prefixes). - let head = store - .last(view.record.host.id, &RecordTag::History) - .await - .map_err(DownloadError::Store)?; + let head = + store.last(view.record.host.id, &RecordTag::History).await.map_err(DownloadError::Store)?; if let Some(head) = head && head.idx >= view.range().end - 1 { @@ -110,18 +104,12 @@ pub async fn download_packed( return Ok(existing.iter().map(|r| r.id).collect()); } - let blob = client - .download_packfile(view.record.id) - .await - .map_err(DownloadError::Api)?; + let blob = client.download_packfile(view.record.id).await.map_err(DownloadError::Api)?; let records = view.unpack_records(blob, key.clone()).await?; let ids: Vec = records.iter().map(|record| record.id).collect(); - store - .push_batch(records.iter()) - .await - .map_err(DownloadError::Store)?; + store.push_batch(records.iter()).await.map_err(DownloadError::Store)?; Ok(ids) } @@ -132,14 +120,13 @@ mod tests { use atuin_common::encryption::paseto_v4; use atuin_common::utils::uuid_v7; + use atuin_domain::caps::PackfileCap; use atuin_domain::record::{DecryptedData, Host, HostId, Record, RecordVersion}; use rstest::*; use wiremock::matchers::{method, path, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; use super::*; - use atuin_domain::caps::PackfileCap; - use crate::api_client::{AuthToken, Client, caps_client}; use crate::packfile::try_pack; use crate::record::sqlite_store::SqliteStore; @@ -155,22 +142,13 @@ mod tests { /// A [`Client`] pointed at a wiremock server, authenticated with a dummy token. fn mock_client(addr: &url::Url) -> Client { let caps = caps_client(addr, &HashMap::new()).unwrap(); - Client::new( - addr.clone(), - AuthToken::Token("t".into()), - 30, - 30, - &HashMap::new(), - caps, - ) - .unwrap() + Client::new(addr.clone(), AuthToken::Token("t".into()), 30, 30, &HashMap::new(), caps) + .unwrap() } /// A fresh in-memory record store. async fn memory_store() -> SqliteStore { - SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap() + SqliteStore::new(":memory:", test_local_timeout()).await.unwrap() } /// Push a contiguous run of `count` encrypted HISTORY records (idx `0..count`). @@ -284,9 +262,7 @@ mod tests { // `.expect(1)` on all three mocks verifies create_packfile + put_packfile + // confirm_packfile each fired exactly once. - upload_packed(&manifest, &store, &key, &client) - .await - .unwrap(); + upload_packed(&manifest, &store, &key, &client).await.unwrap(); } #[rstest] @@ -335,9 +311,7 @@ mod tests { let addr: url::Url = server.uri().parse().unwrap(); let client = mock_client(&addr); - let ids = download_packed(&manifest, &down, &key, &client) - .await - .unwrap(); + let ids = download_packed(&manifest, &down, &key, &client).await.unwrap(); assert_eq!(ids.len(), 5, "all five history records populated"); // History is present locally and decrypts to the same commands. @@ -391,9 +365,7 @@ mod tests { ) .unwrap(); - let ids = download_packed(&manifest, &down, &key, &client) - .await - .unwrap(); + let ids = download_packed(&manifest, &down, &key, &client).await.unwrap(); // Range already present -> no fetch, but the covered ids are still returned so the // id-driven history.db rebuild can re-index them (see download_packed's doc comment). @@ -496,11 +468,7 @@ mod tests { assert_eq!(ids.len(), 3, "the valid manifest's three records expand"); let got = down.next(host, &RecordTag::History, 0, 3).await.unwrap(); - assert_eq!( - got.len(), - 3, - "the valid packfile's history is present locally" - ); + assert_eq!(got.len(), 3, "the valid packfile's history is present locally"); } /// GUARD: a TRANSIENT/systemic fault (here a connection-refused packfile GET) must still diff --git a/crates/atuin-client/src/plugin.rs b/crates/atuin-client/src/plugin.rs index 6f351bf16..a85815122 100644 --- a/crates/atuin-client/src/plugin.rs +++ b/crates/atuin-client/src/plugin.rs @@ -40,9 +40,9 @@ impl OfficialPluginRegistry { OfficialPlugin::new( "update", "Update atuin to the latest version", - "The 'atuin update' command is provided by the atuin-update plugin.\n\ - It is only installed if you used the install script\n \ - If you used a package manager (brew, apt, etc), please continue to use it for updates", + "The 'atuin update' command is provided by the atuin-update plugin.\nIt is only \ + installed if you used the install script\n If you used a package manager (brew, \ + apt, etc), please continue to use it for updates", ), ); } @@ -56,9 +56,7 @@ impl OfficialPluginRegistry { } pub fn get_install_message(&self, name: &str) -> Option<&str> { - self.plugins - .get(name) - .map(|plugin| plugin.install_message.as_str()) + self.plugins.get(name).map(|plugin| plugin.install_message.as_str()) } } diff --git a/crates/atuin-client/src/record/sqlite_store.rs b/crates/atuin-client/src/record/sqlite_store.rs index 771bcead3..88cc506ab 100644 --- a/crates/atuin-client/src/record/sqlite_store.rs +++ b/crates/atuin-client/src/record/sqlite_store.rs @@ -2,25 +2,22 @@ // Multiple stores of multiple types are all stored in one chonky table (for now), and we just index // by tag/host +use std::path::Path; use std::str::FromStr; -use std::{path::Path, time::Duration}; - -use eyre::{Result, eyre}; -use fs_err as fs; - -use sqlx::{ - Row, - sqlite::{ - SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, - SqliteSynchronous, - }, -}; +use std::time::Duration; use atuin_common::encryption::paseto_v4; use atuin_common::utils; use atuin_domain::record::{ Host, HostId, Record, RecordId, RecordIdx, RecordStatus, RecordTag, RecordVersion, }; +use eyre::{Result, eyre}; +use fs_err as fs; +use sqlx::Row; +use sqlx::sqlite::{ + SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, + SqliteSynchronous, +}; use uuid::Uuid; #[derive(Debug, Clone)] @@ -36,7 +33,8 @@ impl SqliteStore { if utils::broken_symlink(path) { eprintln!( - "Atuin: Sqlite db path ({path:?}) is a broken symlink. Unable to read or create replacement." + "Atuin: Sqlite db path ({path:?}) is a broken symlink. Unable to read or create \ + replacement." ); std::process::exit(1); } @@ -118,10 +116,8 @@ impl SqliteStore { } async fn load_all(&self) -> Result>> { - let res = sqlx::query("select * from store ") - .map(Self::query_row) - .fetch_all(&self.pool) - .await?; + let res = + sqlx::query("select * from store ").map(Self::query_row).fetch_all(&self.pool).await?; Ok(res) } @@ -199,9 +195,8 @@ impl SqliteStore { } pub async fn len_all(&self) -> Result { - let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store") - .fetch_one(&self.pool) - .await; + let res: Result<(i64,), sqlx::Error> = + sqlx::query_as("select count(*) from store").fetch_one(&self.pool).await; match res { Err(e) => Err(eyre!("failed to fetch local store len: {}", e)), Ok(v) => Ok(v.0 as u64), @@ -257,7 +252,8 @@ impl SqliteStore { limit: u64, ) -> Result>> { let res = sqlx::query( - "select * from store where idx >= ?1 and host = ?2 and tag = ?3 order by idx asc limit ?4", + "select * from store where idx >= ?1 and host = ?2 and tag = ?3 order by idx asc \ + limit ?4", ) .bind(idx as i64) .bind(host.0.as_hyphenated().to_string()) @@ -379,9 +375,7 @@ impl SqliteStore { pub async fn verify(&self, key: &paseto_v4::Key) -> Result<()> { let all = self.load_all().await?; - all.into_iter() - .map(|record| record.decrypt(key)) - .collect::>>()?; + all.into_iter().map(|record| record.decrypt(key)).collect::>>()?; Ok(()) } @@ -395,10 +389,7 @@ impl SqliteStore { match record.clone().decrypt(key) { Ok(_) => continue, Err(_) => { - println!( - "Failed to decrypt {}, deleting", - record.id.0.as_hyphenated() - ); + println!("Failed to decrypt {}, deleting", record.id.0.as_hyphenated()); self.delete(record.id).await?; } @@ -416,15 +407,12 @@ mod tests { use atuin_domain::record::{DecryptedData, Host, HostId, Record, RecordTag, RecordVersion}; use rstest::{fixture, rstest}; - use crate::settings::test_local_timeout; - use super::SqliteStore; + use crate::settings::test_local_timeout; #[fixture] async fn store() -> SqliteStore { - SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap() + SqliteStore::new(":memory:", test_local_timeout()).await.unwrap() } #[fixture] @@ -474,11 +462,7 @@ mod tests { async fn last(#[future(awt)] store: SqliteStore, record: Record) { store.push(&record).await.unwrap(); let last = store.last(record.host.id, &record.tag).await.unwrap(); - assert_eq!( - last.unwrap().id, - record.id, - "did not get the inserted record" - ); + assert_eq!(last.unwrap().id, record.id, "did not get the inserted record"); } #[rstest] @@ -486,11 +470,7 @@ mod tests { async fn first(#[future(awt)] store: SqliteStore, record: Record) { store.push(&record).await.unwrap(); let first = store.first(record.host.id, &record.tag).await.unwrap(); - assert_eq!( - first.unwrap().id, - record.id, - "did not get the inserted record" - ); + assert_eq!(first.unwrap().id, record.id, "did not get the inserted record"); } #[rstest] @@ -574,11 +554,7 @@ mod tests { 100, "failed to insert 100 records" ); - assert_eq!( - store.len_tag(&tail.tag).await.unwrap(), - 100, - "failed to insert 100 records" - ); + assert_eq!(store.len_tag(&tail.tag).await.unwrap(), 100, "failed to insert 100 records"); } #[rstest] @@ -618,17 +594,11 @@ mod tests { .data(DecryptedData(data.clone())) .build() .encrypt(&key); - store - .push(&record) - .await - .expect("failed to push encrypted record"); + store.push(&record).await.expect("failed to push encrypted record"); } // the data decrypts with the current key - let all = store - .all_tagged(&RecordTag::Other("test".to_owned())) - .await - .unwrap(); + let all = store.all_tagged(&RecordTag::Other("test".to_owned())).await.unwrap(); assert_eq!(all.len(), 10, "failed to fetch all records"); for record in all { let decrypted = record.decrypt(&key).unwrap(); @@ -637,15 +607,9 @@ mod tests { // after re-encrypting: the old key fails, the new key works let new_key = paseto_v4::Key::generate(); - store - .re_encrypt(&key, &new_key) - .await - .expect("failed to re-encrypt store"); + store.re_encrypt(&key, &new_key).await.expect("failed to re-encrypt store"); - let all = store - .all_tagged(&RecordTag::Other("test".to_owned())) - .await - .unwrap(); + let all = store.all_tagged(&RecordTag::Other("test".to_owned())).await.unwrap(); for record in all.iter() { assert!( record.clone().decrypt(&key).is_err(), @@ -657,12 +621,6 @@ mod tests { assert_eq!(decrypted.data.0, data); } - assert_eq!( - store - .len(host_id, &RecordTag::Other("test".to_owned())) - .await - .unwrap(), - 10 - ); + assert_eq!(store.len(host_id, &RecordTag::Other("test".to_owned())).await.unwrap(), 10); } } diff --git a/crates/atuin-client/src/record/sync.rs b/crates/atuin-client/src/record/sync.rs index 079b1153f..b05e4f472 100644 --- a/crates/atuin-client/src/record/sync.rs +++ b/crates/atuin-client/src/record/sync.rs @@ -1,20 +1,19 @@ // do a sync :O -use std::{cmp::Ordering, fmt::Write, sync::Arc}; - -use eyre::Result; -use thiserror::Error; - -use super::sqlite_store::SqliteStore; -use crate::{ - api_client::{Client, caps_client}, - packfile::{download_packed, upload_packed}, - settings::Settings, -}; +use std::cmp::Ordering; +use std::fmt::Write; +use std::sync::Arc; use atuin_common::encryption::paseto_v4; use atuin_domain::caps::{CapClient, PackfileCap}; use atuin_domain::record::{Diff, HostId, RecordId, RecordIdx, RecordStatus, RecordTag}; +use eyre::Result; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; +use thiserror::Error; + +use super::sqlite_store::SqliteStore; +use crate::api_client::{Client, caps_client}; +use crate::packfile::{download_packed, upload_packed}; +use crate::settings::Settings; #[derive(Error, Debug)] pub enum SyncError { @@ -22,22 +21,30 @@ pub enum SyncError { LocalAheadOtherHost, #[error("an issue with the local database occurred: {msg:?}")] - LocalStoreError { msg: String }, + LocalStoreError { + msg: String, + }, #[error("something has gone wrong with the sync logic: {msg:?}")] - SyncLogicError { msg: String }, + SyncLogicError { + msg: String, + }, #[error("operational error: {msg:?}")] - OperationalError { msg: String }, + OperationalError { + msg: String, + }, #[error("a request to the sync server failed: {msg:?}")] - RemoteRequestError { msg: String }, + RemoteRequestError { + msg: String, + }, #[error( - "the encryption key on this machine does not match the data on the server. \ - this usually means a new machine was set up without copying the existing key. \ - to fix: run `atuin key` on a machine that already syncs correctly, then run \ - `atuin store rekey ` on this machine with the value from the other machine" + "the encryption key on this machine does not match the data on the server. this usually \ + means a new machine was set up without copying the existing key. to fix: run `atuin key` \ + on a machine that already syncs correctly, then run `atuin store rekey ` on this \ + machine with the value from the other machine" )] WrongKey, } @@ -90,10 +97,8 @@ pub async fn diff( client: &Client, store: &SqliteStore, ) -> Result<(Vec, RecordStatus), SyncError> { - let local_index = store - .status() - .await - .map_err(|e| SyncError::LocalStoreError { msg: e.to_string() })?; + let local_index = + store.status().await.map_err(|e| SyncError::LocalStoreError { msg: e.to_string() })?; let remote_index = client .record_status() @@ -208,18 +213,20 @@ async fn sync_upload( let mut progress = 0; let pb = ProgressBar::new(expected); - pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {human_pos}/{human_len} ({eta})") + pb.set_style( + ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {human_pos}/{human_len} \ + ({eta})", + ) .unwrap() - .with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) - .progress_chars("#>-")); - - println!( - "Uploading {} records to {}/{}", - expected, - host.0.as_simple(), - tag + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), ); + println!("Uploading {} records to {}/{}", expected, host.0.as_simple(), tag); + while progress < expected { let page = store .next(host, &tag, first_missing_remote + progress, page_size) @@ -236,12 +243,10 @@ async fn sync_upload( if tag == RecordTag::Packfile { for manifest in &page { - upload_packed(manifest, store, key, client) - .await - .map_err(|e| { - error!("failed to upload packfile: {e}"); - SyncError::RemoteRequestError { msg: e.to_string() } - })?; + upload_packed(manifest, store, key, client).await.map_err(|e| { + error!("failed to upload packfile: {e}"); + SyncError::RemoteRequestError { msg: e.to_string() } + })?; } } @@ -312,18 +317,20 @@ async fn sync_download( let mut progress = 0; let mut ret = Vec::new(); - println!( - "Downloading {} records from {}/{}", - expected, - host.0.as_simple(), - tag - ); + println!("Downloading {} records from {}/{}", expected, host.0.as_simple(), tag); let pb = ProgressBar::new(expected); - pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {human_pos}/{human_len} ({eta})") + pb.set_style( + ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {human_pos}/{human_len} \ + ({eta})", + ) .unwrap() - .with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) - .progress_chars("#>-")); + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), + ); while progress < expected { let page = client @@ -397,27 +404,21 @@ pub async fn sync_remote( } => { if tag == RecordTag::Packfile && !packfiles_enabled { debug!( - "server does not advertise PackfileCap; skipping packfile {tag} upload op, loose history covers it" + "server does not advertise PackfileCap; skipping packfile {tag} upload \ + op, loose history covers it" ); continue; } - uploaded += sync_upload( - local_store, - client, - host, - tag, - local, - remote, - page_size, - key, - ) - .await? + uploaded += + sync_upload(local_store, client, host, tag, local, remote, page_size, key) + .await? } Operation::Download { host, tag, remote } => { if tag == RecordTag::Packfile && !packfiles_enabled { debug!( - "server does not advertise PackfileCap; skipping packfile {tag} download op, loose history covers it" + "server does not advertise PackfileCap; skipping packfile {tag} download \ + op, loose history covers it" ); continue; } @@ -459,9 +460,7 @@ pub async fn check_encryption_key( return Ok(()); }; - record - .decrypt(encryption_key) - .map_err(|_| SyncError::WrongKey)?; + record.decrypt(encryption_key).map_err(|_| SyncError::WrongKey)?; Ok(()) } @@ -491,23 +490,15 @@ mod tests { use pretty_assertions::assert_eq; use rstest::rstest; - use crate::{ - record::{ - sqlite_store::SqliteStore, - sync::{self, Operation}, - }, - settings::test_local_timeout, - }; + use crate::record::sqlite_store::SqliteStore; + use crate::record::sync::{self, Operation}; + use crate::settings::test_local_timeout; fn test_record() -> Record { Record::builder() - .host(atuin_domain::record::Host::new(HostId( - atuin_common::utils::uuid_v7(), - ))) + .host(atuin_domain::record::Host::new(HostId(atuin_common::utils::uuid_v7()))) .version("v1".into()) - .tag(RecordTag::Other( - atuin_common::utils::uuid_v7().simple().to_string(), - )) + .tag(RecordTag::Other(atuin_common::utils::uuid_v7().simple().to_string())) .data(EncryptedData { raw: String::new(), cek: String::new(), @@ -560,15 +551,12 @@ mod tests { assert_eq!(operations.len(), 1); - assert_eq!( - operations[0], - Operation::Upload { - host: record.host.id, - tag: record.tag.clone(), - local: record.idx, - remote: None, - } - ); + assert_eq!(operations[0], Operation::Upload { + host: record.host.id, + tag: record.tag.clone(), + local: record.idx, + remote: None, + }); } #[rstest] @@ -592,24 +580,21 @@ mod tests { assert_eq!(operations.len(), 2); - assert_eq!( - operations, - vec![ - // Or in otherwords, local is ahead by one - Operation::Upload { - host: local_ahead.host.id, - tag: local_ahead.tag.clone(), - local: 1, - remote: Some(0), - }, - // Or in other words, remote knows of a record in an entirely new store (tag) - Operation::Download { - host: remote_ahead.host.id, - tag: remote_ahead.tag.clone(), - remote: 0, - }, - ] - ); + assert_eq!(operations, vec![ + // Or in otherwords, local is ahead by one + Operation::Upload { + host: local_ahead.host.id, + tag: local_ahead.tag.clone(), + local: 1, + remote: Some(0), + }, + // Or in other words, remote knows of a record in an entirely new store (tag) + Operation::Download { + host: remote_ahead.host.id, + tag: remote_ahead.tag.clone(), + remote: 0, + }, + ]); } #[rstest] @@ -630,38 +615,27 @@ mod tests { let remote_only = test_record(); let remote_only_20 = test_record(); - let remote_only_21 = remote_only_20 - .append(vec![2, 3, 2]) - .encrypt(&[0; 32].into()); - let remote_only_22 = remote_only_21 - .append(vec![2, 3, 2]) - .encrypt(&[0; 32].into()); - let remote_only_23 = remote_only_22 - .append(vec![2, 3, 2]) - .encrypt(&[0; 32].into()); - let remote_only_24 = remote_only_23 - .append(vec![2, 3, 2]) - .encrypt(&[0; 32].into()); + let remote_only_21 = remote_only_20.append(vec![2, 3, 2]).encrypt(&[0; 32].into()); + let remote_only_22 = remote_only_21.append(vec![2, 3, 2]).encrypt(&[0; 32].into()); + let remote_only_23 = remote_only_22.append(vec![2, 3, 2]).encrypt(&[0; 32].into()); + let remote_only_24 = remote_only_23.append(vec![2, 3, 2]).encrypt(&[0; 32].into()); let second_shared = test_record(); let second_shared_remote_ahead = second_shared.append(vec![1, 2, 3]).encrypt(&[0; 32].into()); - let second_shared_remote_ahead2 = second_shared_remote_ahead - .append(vec![1, 2, 3]) - .encrypt(&[0; 32].into()); + let second_shared_remote_ahead2 = + second_shared_remote_ahead.append(vec![1, 2, 3]).encrypt(&[0; 32].into()); let third_shared = test_record(); let third_shared_local_ahead = third_shared.append(vec![1, 2, 3]).encrypt(&[0; 32].into()); - let third_shared_local_ahead2 = third_shared_local_ahead - .append(vec![1, 2, 3]) - .encrypt(&[0; 32].into()); + let third_shared_local_ahead2 = + third_shared_local_ahead.append(vec![1, 2, 3]).encrypt(&[0; 32].into()); let fourth_shared = test_record(); let fourth_shared_remote_ahead = fourth_shared.append(vec![1, 2, 3]).encrypt(&[0; 32].into()); - let fourth_shared_remote_ahead2 = fourth_shared_remote_ahead - .append(vec![1, 2, 3]) - .encrypt(&[0; 32].into()); + let fourth_shared_remote_ahead2 = + fourth_shared_remote_ahead.append(vec![1, 2, 3]).encrypt(&[0; 32].into()); let local = vec![ shared_record.clone(), @@ -766,21 +740,20 @@ mod tests { #[cfg(test)] mod packfile_download_tests { - use super::*; use std::collections::HashMap; + use atuin_common::encryption::paseto_v4; use atuin_common::utils::uuid_v7; use atuin_domain::record::{DecryptedData, EncryptedData, Host, HostId, Record, RecordId}; use rstest::*; use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; + use super::*; use crate::api_client::{AuthToken, Client, caps_client}; - use crate::packfile::PackManifestRecordView; - use crate::packfile::try_pack; + use crate::packfile::{PackManifestRecordView, try_pack}; use crate::record::sqlite_store::SqliteStore; use crate::settings::test_local_timeout; - use atuin_common::encryption::paseto_v4; /// A single fixed encryption key. The specific bytes are arbitrary in these tests -- each one /// packs and unpacks with the same key -- so one shared value keeps setup uniform. @@ -792,22 +765,13 @@ mod packfile_download_tests { /// A [`Client`] pointed at a wiremock server, authenticated with a dummy token. pub(super) fn mock_client(addr: &url::Url) -> Client { let caps = caps_client(addr, &HashMap::new()).unwrap(); - Client::new( - addr.clone(), - AuthToken::Token("t".into()), - 30, - 30, - &HashMap::new(), - caps, - ) - .unwrap() + Client::new(addr.clone(), AuthToken::Token("t".into()), 30, 30, &HashMap::new(), caps) + .unwrap() } /// A fresh in-memory record store. pub(super) async fn memory_store() -> SqliteStore { - SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap() + SqliteStore::new(":memory:", test_local_timeout()).await.unwrap() } /// Push a contiguous run of `count` encrypted HISTORY records (idx `0..count`). @@ -962,10 +926,7 @@ mod packfile_download_tests { let err = check_encryption_key(&client, &remote_index, &wrong) .await .expect_err("a wrong key on an encrypted record must still be detected"); - assert!( - matches!(err, SyncError::WrongKey), - "expected WrongKey, got {err:?}" - ); + assert!(matches!(err, SyncError::WrongKey), "expected WrongKey, got {err:?}"); } #[rstest] @@ -983,24 +944,11 @@ mod packfile_download_tests { let addr: url::Url = server.uri().parse().unwrap(); let client = mock_client(&addr); - sync_download(&down, &client, host, RecordTag::Packfile, 1, 100, &key) - .await - .unwrap(); + sync_download(&down, &client, host, RecordTag::Packfile, 1, 100, &key).await.unwrap(); // The manifest is stored AND the history it covers was populated. - assert!( - down.last(host, &RecordTag::Packfile) - .await - .unwrap() - .is_some() - ); - assert_eq!( - down.next(host, &RecordTag::History, 0, 3) - .await - .unwrap() - .len(), - 3 - ); + assert!(down.last(host, &RecordTag::Packfile).await.unwrap().is_some()); + assert_eq!(down.next(host, &RecordTag::History, 0, 3).await.unwrap().len(), 3); } #[rstest] @@ -1017,9 +965,8 @@ mod packfile_download_tests { let addr: url::Url = server.uri().parse().unwrap(); let client = mock_client(&addr); - let returned = sync_download(&down, &client, host, RecordTag::Packfile, 1, 100, &key) - .await - .unwrap(); + let returned = + sync_download(&down, &client, host, RecordTag::Packfile, 1, 100, &key).await.unwrap(); for id in &history_ids { assert!( @@ -1078,27 +1025,18 @@ mod packfile_download_tests { let client = mock_client(&addr); // Packfile op first (populates history 0..=2), then the history op. - sync_download(&down, &client, host, RecordTag::Packfile, 1, 100, &key) - .await - .unwrap(); - sync_download(&down, &client, host, RecordTag::History, 3, 100, &key) - .await - .unwrap(); + sync_download(&down, &client, host, RecordTag::Packfile, 1, 100, &key).await.unwrap(); + sync_download(&down, &client, host, RecordTag::History, 3, 100, &key).await.unwrap(); // The history download must have started AFTER the packed prefix (idx 2), i.e. never // requested start=0 for RecordTag::History. let requests = server.received_requests().await.unwrap(); let requested_history_start_0 = requests.iter().any(|r| { r.url.path() == "/api/v0/record/next" - && r.url - .query_pairs() - .any(|(k, v)| k == "tag" && v == RecordTag::History.as_str()) + && r.url.query_pairs().any(|(k, v)| k == "tag" && v == RecordTag::History.as_str()) && r.url.query_pairs().any(|(k, v)| k == "start" && v == "0") }); - assert!( - !requested_history_start_0, - "packed history range must not be re-requested loose" - ); + assert!(!requested_history_start_0, "packed history range must not be re-requested loose"); } #[rstest] @@ -1123,23 +1061,14 @@ mod packfile_download_tests { let client = mock_client(&addr); // remote (2) is BEHIND the live local head (4) -- must not underflow/panic. - let got = sync_download(&down, &client, host, RecordTag::History, 2, 100, &key) - .await - .unwrap(); - assert!( - got.is_empty(), - "nothing to download when local head already exceeds remote" - ); + let got = + sync_download(&down, &client, host, RecordTag::History, 2, 100, &key).await.unwrap(); + assert!(got.is_empty(), "nothing to download when local head already exceeds remote"); } } #[cfg(test)] mod packfile_capability_tests { - use super::packfile_download_tests::{ - memory_store, mock_client, mount_packfile, packed_packfile, seed_history, - }; - use super::{Operation, sync_remote}; - use atuin_common::encryption::paseto_v4; use atuin_common::utils::uuid_v7; use atuin_domain::caps::{CapServer, CapabilitiesCap, PackfileCap}; @@ -1148,6 +1077,11 @@ mod packfile_capability_tests { use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; + use super::packfile_download_tests::{ + memory_store, mock_client, mount_packfile, packed_packfile, seed_history, + }; + use super::{Operation, sync_remote}; + /// A single fixed encryption key, matching the sibling packfile tests. #[fixture] fn key() -> paseto_v4::Key { @@ -1205,31 +1139,12 @@ mod packfile_capability_tests { let addr: url::Url = server.uri().parse().unwrap(); let client = mock_client(&addr); - sync_remote( - &client, - vec![packfile_download_op(host, 3)], - &down, - 100, - &key, - ) - .await - .unwrap(); + sync_remote(&client, vec![packfile_download_op(host, 3)], &down, 100, &key).await.unwrap(); // Cap advertised -> the whole packfile op ran: the manifest was persisted and its history // was expanded into the store. - assert!( - down.last(host, &RecordTag::Packfile) - .await - .unwrap() - .is_some() - ); - assert_eq!( - down.next(host, &RecordTag::History, 0, 3) - .await - .unwrap() - .len(), - 3 - ); + assert!(down.last(host, &RecordTag::Packfile).await.unwrap().is_some()); + assert_eq!(down.next(host, &RecordTag::History, 0, 3).await.unwrap().len(), 3); } #[rstest] @@ -1240,9 +1155,7 @@ mod packfile_capability_tests { let server = MockServer::start().await; // Caps advertised, but NO PackfileCap -> get_server::() == Ok(None). - let caps = CapServer::new() - .add(CapabilitiesCap { version: 1 }) - .unwrap(); + let caps = CapServer::new().add(CapabilitiesCap { version: 1 }).unwrap(); mount_caps_body(&server, caps.body().to_owned()).await; // Tag-scoped packfile mocks (not `mount_packfile`, whose start=0 matcher is tag-agnostic and // would collide with the history op). They exist so that, were the gate broken, the @@ -1289,14 +1202,11 @@ mod packfile_capability_tests { sync_remote( &client, - vec![ - packfile_download_op(host, 3), - Operation::Download { - remote: 3, - host, - tag: RecordTag::History, - }, - ], + vec![packfile_download_op(host, 3), Operation::Download { + remote: 3, + host, + tag: RecordTag::History, + }], &down, 100, &key, @@ -1305,19 +1215,8 @@ mod packfile_capability_tests { .unwrap(); // Packfile op skipped: manifest not persisted. Loose history still synced (no data loss). - assert!( - down.last(host, &RecordTag::Packfile) - .await - .unwrap() - .is_none() - ); - assert_eq!( - down.next(host, &RecordTag::History, 0, 3) - .await - .unwrap() - .len(), - 3 - ); + assert!(down.last(host, &RecordTag::Packfile).await.unwrap().is_none()); + assert_eq!(down.next(host, &RecordTag::History, 0, 3).await.unwrap().len(), 3); } #[rstest] @@ -1333,28 +1232,13 @@ mod packfile_capability_tests { let addr: url::Url = server.uri().parse().unwrap(); let client = mock_client(&addr); - sync_remote( - &client, - vec![packfile_download_op(host, 3)], - &down, - 100, - &key, - ) - .await - .unwrap(); + sync_remote(&client, vec![packfile_download_op(host, 3)], &down, 100, &key).await.unwrap(); // Skipped: manifest not persisted, and no packfile endpoint was ever hit. - assert!( - down.last(host, &RecordTag::Packfile) - .await - .unwrap() - .is_none() - ); + assert!(down.last(host, &RecordTag::Packfile).await.unwrap().is_none()); let requests = server.received_requests().await.unwrap(); assert!( - !requests - .iter() - .any(|r| r.url.path().starts_with("/api/v0/packfiles")), + !requests.iter().any(|r| r.url.path().starts_with("/api/v0/packfiles")), "no /api/v0/packfiles request must be made when packfiles are gated off" ); } @@ -1380,21 +1264,8 @@ mod packfile_capability_tests { let addr: url::Url = server.uri().parse().unwrap(); let client = mock_client(&addr); - sync_remote( - &client, - vec![packfile_download_op(host, 3)], - &down, - 100, - &key, - ) - .await - .unwrap(); + sync_remote(&client, vec![packfile_download_op(host, 3)], &down, 100, &key).await.unwrap(); - assert!( - down.last(host, &RecordTag::Packfile) - .await - .unwrap() - .is_none() - ); + assert!(down.last(host, &RecordTag::Packfile).await.unwrap().is_none()); } } diff --git a/crates/atuin-client/src/register.rs b/crates/atuin-client/src/register.rs index 49451ca0c..48310c91f 100644 --- a/crates/atuin-client/src/register.rs +++ b/crates/atuin-client/src/register.rs @@ -1,7 +1,8 @@ +use atuin_common::encryption::paseto_v4; use eyre::Result; -use crate::{api_client, settings::Settings}; -use atuin_common::encryption::paseto_v4; +use crate::api_client; +use crate::settings::Settings; pub async fn register_classic( settings: &Settings, diff --git a/crates/atuin-client/src/secrets.rs b/crates/atuin-client/src/secrets.rs index fcece0b45..321dbcf0e 100644 --- a/crates/atuin-client/src/secrets.rs +++ b/crates/atuin-client/src/secrets.rs @@ -1,8 +1,9 @@ // This file will probably trigger a lot of scanners. Sorry. -use regex::RegexSet; use std::sync::LazyLock; +use regex::RegexSet; + pub enum TestValue<'a> { Single(&'a str), Multiple(&'a [&'a str]), @@ -153,7 +154,11 @@ mod tests { let re = Regex::new(regex).unwrap_or_else(|_| panic!("Failed to compile regex for {name}")); - let label = if embed { "embedded test" } else { "test" }; + let label = if embed { + "embedded test" + } else { + "test" + }; let wrap = |s: &str| { if embed { format!("some random text {s} some more random text") diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index 1a5d768a1..040a6cf94 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -1,22 +1,20 @@ +use std::collections::HashMap; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; +use std::sync::{LazyLock, OnceLock}; + use atuin_common::logs::LogLevel; use atuin_common::utils; use atuin_domain::record::HostId; use clap::ValueEnum; -use config::{ - Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat, builder::DefaultState, -}; +use config::builder::DefaultState; +use config::{Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat}; use eyre::{Context, Result, eyre}; use fs_err::{File, create_dir_all}; use humantime::parse_duration; use regex::RegexSet; use semver::Version; use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - io::prelude::*, - path::{Path, PathBuf}, - sync::{LazyLock, OnceLock}, -}; use thiserror::Error; use time::OffsetDateTime; use tokio::sync::OnceCell; @@ -370,17 +368,25 @@ pub enum SyncProtocol { pub enum SyncAuth { /// Self-hosted Rust server. Uses `Authorization: Token ` and /// legacy endpoints. - Legacy { token: String }, + Legacy { + token: String, + }, /// Hub with a valid Hub API token (`atapi_*`). Uses /// `Authorization: Bearer ` and v0 endpoints. - Hub { token: String }, + Hub { + token: String, + }, /// Targeting Hub but only has a CLI session token. Uses /// `Authorization: Token ` against compat/record endpoints. /// Sync, password change, and account deletion still work, but the user /// should be nudged to run `atuin login` for full Hub auth. - HubViaCli { token: String }, + HubViaCli { + token: String, + }, /// Not authenticated at all. Contains an actionable user-facing message. - NotLoggedIn { reason: String }, + NotLoggedIn { + reason: String, + }, } #[cfg(feature = "sync")] @@ -1118,8 +1124,7 @@ impl Settings { } pub fn search_mode_shell_up_key_binding(&self) -> Option { - self.requested_search_mode_shell_up_key_binding - .map(Into::into) + self.requested_search_mode_shell_up_key_binding.map(Into::into) } /// Return the active search mode depending on whether Atuin was invoked from the "up" @@ -1136,10 +1141,7 @@ impl Settings { } pub(crate) fn effective_data_dir() -> PathBuf { - DATA_DIR - .get() - .cloned() - .unwrap_or_else(atuin_common::utils::data_dir) + DATA_DIR.get().cloned().unwrap_or_else(atuin_common::utils::data_dir) } // -- Meta store: lazily initialized on first access -- @@ -1278,8 +1280,8 @@ impl Settings { return match meta.session_token().await { Ok(Some(token)) => SyncAuth::Legacy { token }, _ => SyncAuth::NotLoggedIn { - reason: "Not logged in. Run 'atuin login' to authenticate \ - with your sync server." + reason: "Not logged in. Run 'atuin login' to authenticate with your sync \ + server." .into(), }, }; @@ -1310,8 +1312,7 @@ impl Settings { match meta.session_token().await { Ok(Some(token)) => SyncAuth::HubViaCli { token }, _ => SyncAuth::NotLoggedIn { - reason: "Not logged in. Run 'atuin login' or 'atuin register' \ - to authenticate." + reason: "Not logged in. Run 'atuin login' or 'atuin register' to authenticate." .into(), }, } @@ -1414,9 +1415,7 @@ impl Settings { } pub fn builder() -> Result> { - Ok(Self::builder_with_data_dir( - &atuin_common::utils::data_dir(), - )?) + Ok(Self::builder_with_data_dir(&atuin_common::utils::data_dir())?) } fn builder_with_data_dir( @@ -1517,17 +1516,14 @@ impl Settings { .set_default("ai.opening.send_cwd", false)? .set_default("ai.opening.send_last_command", false)? .set_default("ui.syntax_highlight", true)? - .set_default( - "search.filters", - vec![ - "global", - "host", - "session", - "workspace", - "directory", - "session-preload", - ], - )? + .set_default("search.filters", vec![ + "global", + "host", + "session", + "workspace", + "directory", + "session-preload", + ])? .set_default("theme.name", "default")? .set_default("theme.debug", None::)? .set_default("tmux.enabled", false)? @@ -1541,11 +1537,7 @@ impl Settings { .unwrap_or_else(|| config::Value::new(None, config::ValueKind::Boolean(false))), )? .set_default("no_mouse", false)? - .add_source( - Environment::with_prefix("atuin") - .prefix_separator("_") - .separator("__"), - )) + .add_source(Environment::with_prefix("atuin").prefix_separator("_").separator("__"))) } pub fn get_config_path() -> Result { @@ -1582,17 +1574,12 @@ impl Settings { data_dir: Option, } - let config_file_str = config_file - .to_str() - .ok_or_else(|| eyre!("config file path is not valid UTF-8"))?; + let config_file_str = + config_file.to_str().ok_or_else(|| eyre!("config file path is not valid UTF-8"))?; let partial_config = Config::builder() .add_source(ConfigFile::new(config_file_str, FileFormat::Toml)) - .add_source( - Environment::with_prefix("atuin") - .prefix_separator("_") - .separator("__"), - ) + .add_source(Environment::with_prefix("atuin").prefix_separator("_").separator("__")) .build() .ok(); @@ -1620,9 +1607,8 @@ impl Settings { let mut config_builder = Self::builder_with_data_dir(&effective_data_dir)?; config_builder = if config_file.exists() { - let config_file_str = config_file - .to_str() - .ok_or_else(|| eyre!("config file path is not valid UTF-8"))?; + let config_file_str = + config_file.to_str().ok_or_else(|| eyre!("config file path is not valid UTF-8"))?; config_builder.add_source(ConfigFile::new(config_file_str, FileFormat::Toml)) } else { let mut file = File::create(config_file).wrap_err("could not create config file")?; @@ -1695,9 +1681,8 @@ impl Settings { ); } - let value: Value = config - .get(key) - .map_err(|e| eyre!("failed to get config value '{}': {}", key, e))?; + let value: Value = + config.get(key).map_err(|e| eyre!("failed to get config value '{}': {}", key, e))?; Ok(Self::format_resolved_value(&value, key)) } @@ -1715,10 +1700,8 @@ impl Settings { ValueKind::Float(f) => f.to_string(), ValueKind::String(s) => s.clone(), ValueKind::Array(arr) => { - let items: Vec = arr - .iter() - .map(|v| Self::format_resolved_value(v, "")) - .collect(); + let items: Vec = + arr.iter().map(|v| Self::format_resolved_value(v, "")).collect(); format!("[{}]", items.join(", ")) } ValueKind::Table(map) => { @@ -1755,17 +1738,14 @@ impl Settings { pub fn new() -> Result { let config = Self::build_config()?; - let settings: Settings = config - .try_deserialize() - .map_err(|e| eyre!("failed to deserialize: {}", e))?; + let settings: Settings = + config.try_deserialize().map_err(|e| eyre!("failed to deserialize: {}", e))?; // Validate UI settings settings.ui.validate()?; // Register meta store config for lazy initialization on first access - META_CONFIG - .set((settings.meta.db_path.clone(), settings.local_timeout)) - .ok(); + META_CONFIG.set((settings.meta.db_path.clone(), settings.local_timeout)).ok(); Ok(settings) } @@ -1862,12 +1842,12 @@ mod tests { use eyre::Result; use rstest::rstest; + use url::Url; use super::{ AiEndpointProtocol, ConfigFile, FileFormat, FilterMode, RequestedSearchMode, SearchMode, Settings, UtcOffsetSpec, }; - use url::Url; #[rstest] #[case::plus_two_digit_hours("+02", (2, 0, 0))] @@ -1920,10 +1900,7 @@ mod tests { let mut settings = Settings::default(); settings.ai.endpoint_protocol = protocol; - assert_eq!( - settings.is_hub_ai_endpoint(&Url::parse(endpoint).unwrap()), - expected, - ); + assert_eq!(settings.is_hub_ai_endpoint(&Url::parse(endpoint).unwrap()), expected,); } /// Forces both `LazyLock`s, so a typo in either constant fails here rather @@ -1976,21 +1953,12 @@ mod tests { assert_eq!(db_path, custom_dir.join("history.db").to_str().unwrap()); assert_eq!(key_path, custom_dir.join("key").to_str().unwrap()); - assert_eq!( - record_store_path, - custom_dir.join("records.db").to_str().unwrap() - ); + assert_eq!(record_store_path, custom_dir.join("records.db").to_str().unwrap()); assert_eq!(kv_db_path, custom_dir.join("kv.db").to_str().unwrap()); - assert_eq!( - scripts_db_path, - custom_dir.join("scripts.db").to_str().unwrap() - ); + assert_eq!(scripts_db_path, custom_dir.join("scripts.db").to_str().unwrap()); assert_eq!(meta_db_path, custom_dir.join("meta.db").to_str().unwrap()); assert_eq!(daemon_socket_path, None); - assert_eq!( - daemon_pidfile_path, - custom_dir.join("atuin-daemon.pid").to_str().unwrap() - ); + assert_eq!(daemon_pidfile_path, custom_dir.join("atuin-daemon.pid").to_str().unwrap()); assert!(!daemon_autostart); Ok(()) @@ -2014,18 +1982,14 @@ mod tests { "data_dir" )] #[case::more_than_one_expanding_column( - "[ui]\ncolumns = [{ type = \"duration\", expand = true }, { type = \"command\", expand = true }]\n", + "[ui]\ncolumns = [{ type = \"duration\", expand = true }, { type = \"command\", expand = \ + true }]\n", "expand" )] fn validate_rejects(#[case] toml: &str, #[case] expected_err: &str) { - let err = Settings::validate_str(toml) - .expect_err("config should not validate") - .to_string(); + let err = Settings::validate_str(toml).expect_err("config should not validate").to_string(); - assert!( - err.contains(expected_err), - "error should mention `{expected_err}`, got: {err}" - ); + assert!(err.contains(expected_err), "error should mention `{expected_err}`, got: {err}"); } #[test] @@ -2128,10 +2092,7 @@ mod tests { settings.requested_search_mode_shell_up_key_binding, Some(RequestedSearchMode::Skim) ); - assert_eq!( - settings.search_mode_shell_up_key_binding(), - Some(SearchMode::Fuzzy) - ); + assert_eq!(settings.search_mode_shell_up_key_binding(), Some(SearchMode::Fuzzy)); } #[rstest] diff --git a/crates/atuin-client/src/settings/daemon.rs b/crates/atuin-client/src/settings/daemon.rs index fc93abbad..3e22ea7e7 100644 --- a/crates/atuin-client/src/settings/daemon.rs +++ b/crates/atuin-client/src/settings/daemon.rs @@ -1,8 +1,9 @@ -use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[cfg(unix)] use std::{borrow::Cow, path::Path}; +use serde::{Deserialize, Serialize}; + #[cfg(unix)] const SOCKET_NAME: &str = "atuin.sock"; @@ -96,9 +97,8 @@ impl Daemon { fn existing_socket_path_ctx(&self, ctx: impl SocketCtx) -> Cow<'_, Path> { let mut candidates = self.potential_socket_paths_ctx(ctx); - let primary = candidates - .next() - .expect("there is always at least one potential socket path"); + let primary = + candidates.next().expect("there is always at least one potential socket path"); if primary.exists() { return primary; @@ -120,24 +120,13 @@ impl Daemon { (self.default_socket_paths(ctx), usize::MAX) }; - let defaults = defaults - .into_iter() - .take(defaults_limit) - .flatten() - .map(Cow::Owned); + let defaults = defaults.into_iter().take(defaults_limit).flatten().map(Cow::Owned); - self.socket_path - .as_deref() - .map(Cow::Borrowed) - .into_iter() - .chain(defaults) + self.socket_path.as_deref().map(Cow::Borrowed).into_iter().chain(defaults) } fn default_socket_paths(&self, ctx: impl SocketCtx) -> [Option; 3] { - let runtime_path = self - .systemd_socket - .then(|| ctx.runtime_socket_path()) - .flatten(); + let runtime_path = self.systemd_socket.then(|| ctx.runtime_socket_path()).flatten(); // If `runtime_socket_path` is `Some`, `ctx.legacy_socket_path()` will return the same path // (both pointing to `$XDG_RUNTIME_DIR`), so don't include the legacy path in that case. let legacy_path = runtime_path.is_none().then(|| ctx.legacy_socket_path()); @@ -169,9 +158,7 @@ trait SocketCtx: Copy + Sized { } fn default_socket_path(&self) -> PathBuf { - self.tmp_dir() - .join(format!("atuin-{}", self.uid())) - .join(SOCKET_NAME) + self.tmp_dir().join(format!("atuin-{}", self.uid())).join(SOCKET_NAME) } fn runtime_socket_path(&self) -> Option { @@ -179,8 +166,7 @@ trait SocketCtx: Copy + Sized { } fn legacy_socket_path(&self) -> PathBuf { - self.runtime_socket_path() - .unwrap_or_else(|| self.data_dir().join(SOCKET_NAME)) + self.runtime_socket_path().unwrap_or_else(|| self.data_dir().join(SOCKET_NAME)) } } @@ -214,7 +200,10 @@ pub enum CreateSocketDirError { actual_uid: u32, }, #[error("{} has incorrect permissions (expected 700, got {permissions:03o})", .path.display())] - WrongPermissions { path: PathBuf, permissions: u32 }, + WrongPermissions { + path: PathBuf, + permissions: u32, + }, #[error(transparent)] Io(#[from] std::io::Error), } @@ -233,9 +222,7 @@ impl<'a> SocketPath<'a> { return Ok(()); }; - let dir = path - .parent() - .expect("default socket path always has a parent"); + let dir = path.parent().expect("default socket path always has a parent"); match std::fs::DirBuilder::new().mode(0o700).create(dir) { Err(e) if e.kind() == ErrorKind::AlreadyExists => {} result => return result.map_err(Into::into), @@ -310,12 +297,14 @@ impl<'a> From> for Cow<'a, Path> { #[cfg(all(unix, test))] mod unix_tests { - use super::*; - use rstest::*; use std::fs::Permissions; use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink}; + + use rstest::*; use tempfile::TempDir; + use super::*; + #[fixture] fn tmp() -> TempDir { tempfile::tempdir().unwrap() @@ -432,16 +421,10 @@ mod unix_tests { }; assert_eq!( - daemon - .potential_socket_paths_ctx(ctx) - .map(Cow::into_owned) - .collect::>(), + daemon.potential_socket_paths_ctx(ctx).map(Cow::into_owned).collect::>(), expected.iter().map(PathBuf::from).collect::>(), ); - assert_eq!( - daemon.socket_path_ctx(ctx).as_path(), - Path::new(expected[0]) - ); + assert_eq!(daemon.socket_path_ctx(ctx).as_path(), Path::new(expected[0])); } /// The path we connect to is the first one that is actually there, so that a daemon listening @@ -469,10 +452,7 @@ mod unix_tests { fs_err::File::create(path).unwrap(); } - assert_eq!( - daemon(None, false).existing_socket_path_ctx(ctx), - scoped(expected) - ); + assert_eq!(daemon(None, false).existing_socket_path_ctx(ctx), scoped(expected)); } #[rstest] @@ -488,17 +468,11 @@ mod unix_tests { #[rstest] fn default_socket_dir_is_created_privately_then_reused(default_socket: DefaultSocket) { - default_socket - .path() - .create_default_dir_if_needed() - .unwrap(); + default_socket.path().create_default_dir_if_needed().unwrap(); let mode = fs_err::metadata(&default_socket.dir).unwrap().mode(); assert_eq!(mode & 0o777, 0o700); - default_socket - .path() - .create_default_dir_if_needed() - .unwrap(); + default_socket.path().create_default_dir_if_needed().unwrap(); } #[rstest] @@ -509,10 +483,7 @@ mod unix_tests { default_socket: DefaultSocket, #[case] mode: u32, ) { - default_socket - .path() - .create_default_dir_if_needed() - .unwrap(); + default_socket.path().create_default_dir_if_needed().unwrap(); fs_err::set_permissions(&default_socket.dir, Permissions::from_mode(mode)).unwrap(); let Err(CreateSocketDirError::WrongPermissions { permissions, .. }) = diff --git a/crates/atuin-client/src/settings/shells.rs b/crates/atuin-client/src/settings/shells.rs index 30cf745e3..660e46a1f 100644 --- a/crates/atuin-client/src/settings/shells.rs +++ b/crates/atuin-client/src/settings/shells.rs @@ -121,11 +121,12 @@ impl Serialize for Shells { #[cfg(test)] mod tests { - use super::Shells; use atuin_common::filter::{self, OrFilter}; use rstest::rstest; use serde::Deserialize; + use super::Shells; + fn parse(toml: &str) -> Result { Shells::deserialize(toml::de::ValueDeserializer::parse(toml).unwrap()) } diff --git a/crates/atuin-client/src/settings/watcher.rs b/crates/atuin-client/src/settings/watcher.rs index 782527cf9..795800e5d 100644 --- a/crates/atuin-client/src/settings/watcher.rs +++ b/crates/atuin-client/src/settings/watcher.rs @@ -21,17 +21,13 @@ //! } //! ``` -use std::{ - path::PathBuf, - sync::{Arc, OnceLock}, - time::Duration, -}; +use std::path::PathBuf; +use std::sync::{Arc, OnceLock}; +use std::time::Duration; use eyre::{Result, WrapErr}; -use notify::{ - Config as NotifyConfig, RecommendedWatcher, RecursiveMode, Watcher, - event::{EventKind, ModifyKind}, -}; +use notify::event::{EventKind, ModifyKind}; +use notify::{Config as NotifyConfig, RecommendedWatcher, RecursiveMode, Watcher}; use tokio::sync::watch; use tracing::{debug, error, info, warn}; @@ -188,10 +184,7 @@ impl SettingsWatcher { // Defensive: ensure watch path exists before trying to watch if !watch_path.exists() { - warn!( - "config directory does not exist, creating it: {:?}", - watch_path - ); + warn!("config directory does not exist, creating it: {:?}", watch_path); std::fs::create_dir_all(watch_path) .wrap_err_with(|| format!("failed to create config directory: {:?}", watch_path))?; } @@ -228,10 +221,7 @@ impl SettingsWatcher { // Defensive: check if config file exists before reloading // (handles case where file was deleted - we'll get notified when it's recreated) if !config_path.exists() { - debug!( - "config file does not exist, skipping reload: {:?}", - config_path - ); + debug!("config file does not exist, skipping reload: {:?}", config_path); continue; } diff --git a/crates/atuin-client/src/theme.rs b/crates/atuin-client/src/theme.rs index d5e4e8406..1d0d33a1e 100644 --- a/crates/atuin-client/src/theme.rs +++ b/crates/atuin-client/src/theme.rs @@ -1,12 +1,13 @@ -use config::{Config, File as ConfigFile, FileFormat}; -use palette::named; -use serde::{Deserialize, Serialize}; -use serde_json; use std::collections::HashMap; use std::error; use std::io::{Error, ErrorKind}; use std::path::PathBuf; use std::sync::LazyLock; + +use config::{Config, File as ConfigFile, FileFormat}; +use palette::named; +use serde::{Deserialize, Serialize}; +use serde_json; use strum_macros; static DEFAULT_MAX_DEPTH: u8 = 10; @@ -138,7 +139,10 @@ impl Theme { *name, StyleFactory::from_fg_string(color).unwrap_or_else(|err| { if debug { - tracing::warn!("Tried to load string as a color unsuccessfully: ({name}={color}) {err}"); + tracing::warn!( + "Tried to load string as a color unsuccessfully: ({name}={color}) \ + {err}" + ); } ContentStyle::default() }), @@ -268,26 +272,11 @@ static DEFAULT_THEME: LazyLock = LazyLock::new(|| { "default".to_string(), None, HashMap::from([ - ( - Meaning::AlertError, - StyleFactory::from_fg_color(Color::DarkRed), - ), - ( - Meaning::AlertWarn, - StyleFactory::from_fg_color(Color::DarkYellow), - ), - ( - Meaning::AlertInfo, - StyleFactory::from_fg_color(Color::DarkGreen), - ), - ( - Meaning::Annotation, - StyleFactory::from_fg_color(Color::DarkGrey), - ), - ( - Meaning::Guidance, - StyleFactory::from_fg_color(Color::DarkBlue), - ), + (Meaning::AlertError, StyleFactory::from_fg_color(Color::DarkRed)), + (Meaning::AlertWarn, StyleFactory::from_fg_color(Color::DarkYellow)), + (Meaning::AlertInfo, StyleFactory::from_fg_color(Color::DarkGreen)), + (Meaning::Annotation, StyleFactory::from_fg_color(Color::DarkGrey)), + (Meaning::Guidance, StyleFactory::from_fg_color(Color::DarkBlue)), ( Meaning::Important, StyleFactory::from_fg_color_and_attributes( @@ -299,28 +288,13 @@ static DEFAULT_THEME: LazyLock = LazyLock::new(|| { (Meaning::Base, ContentStyle::default()), // Syntax highlighting uses ANSI palette colors, so they follow // the user's terminal color scheme out of the box. - ( - Meaning::SyntaxCommand, - StyleFactory::from_fg_color(Color::Green), - ), - ( - Meaning::SyntaxFlag, - StyleFactory::from_fg_color(Color::DarkCyan), - ), - ( - Meaning::SyntaxString, - StyleFactory::from_fg_color(Color::DarkYellow), - ), - ( - Meaning::SyntaxVariable, - StyleFactory::from_fg_color(Color::Magenta), - ), + (Meaning::SyntaxCommand, StyleFactory::from_fg_color(Color::Green)), + (Meaning::SyntaxFlag, StyleFactory::from_fg_color(Color::DarkCyan)), + (Meaning::SyntaxString, StyleFactory::from_fg_color(Color::DarkYellow)), + (Meaning::SyntaxVariable, StyleFactory::from_fg_color(Color::Magenta)), // Operators keep the terminal's default foreground (Meaning::SyntaxOperator, ContentStyle::default()), - ( - Meaning::SyntaxComment, - StyleFactory::from_fg_color(Color::DarkGrey), - ), + (Meaning::SyntaxComment, StyleFactory::from_fg_color(Color::DarkGrey)), ]), ) }); @@ -339,42 +313,21 @@ static BUILTIN_THEMES: LazyLock> = LazyLock::new(|| ( "autumn", HashMap::from([ - ( - Meaning::AlertError, - StyleFactory::known_fg_string("saddlebrown"), - ), - ( - Meaning::AlertWarn, - StyleFactory::known_fg_string("darkorange"), - ), + (Meaning::AlertError, StyleFactory::known_fg_string("saddlebrown")), + (Meaning::AlertWarn, StyleFactory::known_fg_string("darkorange")), (Meaning::AlertInfo, StyleFactory::known_fg_string("gold")), - ( - Meaning::Annotation, - StyleFactory::from_fg_color(Color::DarkGrey), - ), + (Meaning::Annotation, StyleFactory::from_fg_color(Color::DarkGrey)), (Meaning::Guidance, StyleFactory::known_fg_string("brown")), ]), ), ( "marine", HashMap::from([ - ( - Meaning::AlertError, - StyleFactory::known_fg_string("yellowgreen"), - ), + (Meaning::AlertError, StyleFactory::known_fg_string("yellowgreen")), (Meaning::AlertWarn, StyleFactory::known_fg_string("cyan")), - ( - Meaning::AlertInfo, - StyleFactory::known_fg_string("turquoise"), - ), - ( - Meaning::Annotation, - StyleFactory::known_fg_string("steelblue"), - ), - ( - Meaning::Base, - StyleFactory::known_fg_string("lightsteelblue"), - ), + (Meaning::AlertInfo, StyleFactory::known_fg_string("turquoise")), + (Meaning::Annotation, StyleFactory::known_fg_string("steelblue")), + (Meaning::Base, StyleFactory::known_fg_string("lightsteelblue")), (Meaning::Guidance, StyleFactory::known_fg_string("teal")), ]), ), @@ -437,10 +390,8 @@ impl ThemeManager { let mut config_builder = Config::builder(); - config_builder = config_builder.add_source(ConfigFile::new( - theme_file.to_str().unwrap(), - FileFormat::Toml, - )); + config_builder = config_builder + .add_source(ConfigFile::new(theme_file.to_str().unwrap(), FileFormat::Toml)); let config = config_builder.build()?; self.load_theme_from_config(name, config, max_depth) @@ -520,29 +471,24 @@ impl ThemeManager { #[cfg(test)] mod theme_tests { - use super::*; use atuin_common::test_utils::capture_logs; use rstest::*; + use super::*; + #[fixture] fn manager(#[default(false)] debug: bool) -> ThemeManager { ThemeManager::new(Some(debug), Some("".to_string())) } fn theme_config(toml: &str) -> Config { - Config::builder() - .add_source(ConfigFile::from_str(toml, FileFormat::Toml)) - .build() - .unwrap() + Config::builder().add_source(ConfigFile::from_str(toml, FileFormat::Toml)).build().unwrap() } #[rstest] fn test_can_load_builtin_theme(mut manager: ThemeManager) { let theme = manager.load_theme("autumn", None); - assert_eq!( - theme.as_style(Meaning::Guidance).foreground_color, - from_string("brown").ok() - ); + assert_eq!(theme.as_style(Meaning::Guidance).foreground_color, from_string("brown").ok()); } #[rstest] @@ -550,10 +496,7 @@ mod theme_tests { let mytheme = Theme::new( "mytheme".to_string(), None, - HashMap::from([( - Meaning::AlertError, - StyleFactory::known_fg_string("yellowgreen"), - )]), + HashMap::from([(Meaning::AlertError, StyleFactory::known_fg_string("yellowgreen"))]), ); manager.loaded_themes.insert("mytheme".to_string(), mytheme); let theme = manager.load_theme("mytheme", None); @@ -579,15 +522,10 @@ mod theme_tests { AlertInfo = \"zomp\" ", ); - let theme = manager - .load_theme_from_config("config_theme", config, 1) - .unwrap(); + let theme = manager.load_theme_from_config("config_theme", config, 1).unwrap(); // Correctly picks overridden color. - assert_eq!( - theme.as_style(Meaning::Guidance).foreground_color, - from_string("white").ok() - ); + assert_eq!(theme.as_style(Meaning::Guidance).foreground_color, from_string("white").ok()); // Does not fall back to any color. assert_eq!(theme.as_style(Meaning::AlertInfo).foreground_color, None); @@ -596,10 +534,7 @@ mod theme_tests { assert_eq!(theme.as_style(Meaning::Base).foreground_color, None); // Falls back to red as meaning missing from theme, so picks base default. - assert_eq!( - theme.as_style(Meaning::AlertError).foreground_color, - Some(Color::DarkRed) - ); + assert_eq!(theme.as_style(Meaning::AlertError).foreground_color, Some(Color::DarkRed)); // Falls back to Important as Title not available. assert_eq!( @@ -617,14 +552,9 @@ mod theme_tests { AlertInfo = \"zomp\" ", ); - let title_theme = manager - .load_theme_from_config("title_theme", title_config, 1) - .unwrap(); + let title_theme = manager.load_theme_from_config("title_theme", title_config, 1).unwrap(); - assert_eq!( - title_theme.as_style(Meaning::Title).foreground_color, - Some(Color::White) - ); + assert_eq!(title_theme.as_style(Meaning::Title).foreground_color, Some(Color::White)); } #[test] @@ -639,19 +569,10 @@ mod theme_tests { fn test_can_get_colors_via_convenience_functions(#[with(true)] mut manager: ThemeManager) { let theme = manager.load_theme("default", None); assert_eq!(theme.get_error().foreground_color.unwrap(), Color::DarkRed); - assert_eq!( - theme.get_warning().foreground_color.unwrap(), - Color::DarkYellow - ); + assert_eq!(theme.get_warning().foreground_color.unwrap(), Color::DarkYellow); assert_eq!(theme.get_info().foreground_color.unwrap(), Color::DarkGreen); assert_eq!(theme.get_base().foreground_color, None); - assert_eq!( - theme - .get_alert(tracing::Level::ERROR) - .foreground_color - .unwrap(), - Color::DarkRed - ) + assert_eq!(theme.get_alert(tracing::Level::ERROR).foreground_color.unwrap(), Color::DarkRed) } #[rstest] @@ -669,14 +590,10 @@ mod theme_tests { AlertInfo = \"pink\" ", ); - let solarized_theme = manager - .load_theme_from_config("solarized", solarized, 1) - .unwrap(); + let solarized_theme = manager.load_theme_from_config("solarized", solarized, 1).unwrap(); assert_eq!( - solarized_theme - .as_style(Meaning::AlertInfo) - .foreground_color, + solarized_theme.as_style(Meaning::AlertInfo).foreground_color, from_string("pink").ok() ); @@ -691,23 +608,18 @@ mod theme_tests { AlertInfo = \"red\" ", ); - let unsolarized_theme = manager - .load_theme_from_config("unsolarized", unsolarized, 1) - .unwrap(); + let unsolarized_theme = + manager.load_theme_from_config("unsolarized", unsolarized, 1).unwrap(); // It will take its own values assert_eq!( - unsolarized_theme - .as_style(Meaning::AlertInfo) - .foreground_color, + unsolarized_theme.as_style(Meaning::AlertInfo).foreground_color, from_string("red").ok() ); // ...or fall back to the parent assert_eq!( - unsolarized_theme - .as_style(Meaning::Guidance) - .foreground_color, + unsolarized_theme.as_style(Meaning::Guidance).foreground_color, from_string("white").ok() ); @@ -725,22 +637,17 @@ mod theme_tests { AlertInfo = \"red\" ", ); - let nunsolarized_theme = manager - .load_theme_from_config("nunsolarized", nunsolarized, 1) - .unwrap(); + let nunsolarized_theme = + manager.load_theme_from_config("nunsolarized", nunsolarized, 1).unwrap(); - assert_eq!( - nunsolarized_theme - .as_style(Meaning::Guidance) - .foreground_color, - None - ); + assert_eq!(nunsolarized_theme.as_style(Meaning::Guidance).foreground_color, None); let captured_logs = logs.get(); assert_eq!(captured_logs.len(), 1); assert_eq!( captured_logs[0].message, - "Could not load theme nonsolarized: Empty theme directory override and could not find theme elsewhere" + "Could not load theme nonsolarized: Empty theme directory override and could not find \ + theme elsewhere" ); assert_eq!(captured_logs[0].level, tracing::Level::WARN) } @@ -760,20 +667,20 @@ mod theme_tests { AlertInfo = \"xinetic\" ", ); - manager - .load_theme_from_config("config_theme", config, 1) - .unwrap(); + manager.load_theme_from_config("config_theme", config, 1).unwrap(); let captured_logs = logs.get(); if debug { assert_eq!(captured_logs.len(), 2); assert_eq!( captured_logs[0].message, - "Your theme config name is not the name of your loaded theme config_theme != mytheme" + "Your theme config name is not the name of your loaded theme config_theme != \ + mytheme" ); assert_eq!(captured_logs[0].level, tracing::Level::WARN); assert_eq!( captured_logs[1].message, - "Tried to load string as a color unsuccessfully: (AlertInfo=xinetic) No such color in palette" + "Tried to load string as a color unsuccessfully: (AlertInfo=xinetic) No such \ + color in palette" ); assert_eq!(captured_logs[1].level, tracing::Level::WARN); } else { @@ -812,9 +719,7 @@ mod theme_tests { fn rejects_at_prefixed_color_string(#[case] input: &str) { assert_eq!( from_string(input), - Err(format!( - "Could not convert color name {input} to Crossterm color" - )) + Err(format!("Could not convert color name {input} to Crossterm color")) ); } } diff --git a/crates/atuin-common/src/ansi.rs b/crates/atuin-common/src/ansi.rs index f61550b35..92b5e63d1 100644 --- a/crates/atuin-common/src/ansi.rs +++ b/crates/atuin-common/src/ansi.rs @@ -116,10 +116,12 @@ pub fn onlcr>(bytes: impl IntoIterator) -> impl Iterator #[cfg(test)] mod tests { - use super::*; + use std::num::NonZeroU16; + use proptest::prelude::*; use rstest::rstest; - use std::num::NonZeroU16; + + use super::*; fn nz(cols: u16) -> NonZeroU16 { NonZeroU16::new(cols).expect("test column width must be nonzero") @@ -127,9 +129,7 @@ mod tests { fn assert_no_terminal_controls(text: &str) { assert!( - !text - .chars() - .any(|ch| ch.is_control() && ch != '\n' && ch != '\t'), + !text.chars().any(|ch| ch.is_control() && ch != '\n' && ch != '\t'), "text still contains terminal controls: {text:?}" ); } diff --git a/crates/atuin-common/src/docs.rs b/crates/atuin-common/src/docs.rs index f074f7320..a16729786 100644 --- a/crates/atuin-common/src/docs.rs +++ b/crates/atuin-common/src/docs.rs @@ -44,9 +44,10 @@ const fn version_segment(version: &str) -> &str { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::stable_patch_zero("18.17.0", "18.17")] #[case::stable_patch_one("18.17.1", "18.17")] diff --git a/crates/atuin-common/src/encryption/paseto_v4.rs b/crates/atuin-common/src/encryption/paseto_v4.rs index fd3b50dc8..312f8ff58 100644 --- a/crates/atuin-common/src/encryption/paseto_v4.rs +++ b/crates/atuin-common/src/encryption/paseto_v4.rs @@ -1,20 +1,17 @@ //! PASETO v4 / PASERK envelope encryption for atuin records. //! //! See [`encrypt_sync`] for the encryption description. -use std::{ - array::TryFromSliceError, - fs, - io::{Read, Write}, - path::Path, -}; +use std::array::TryFromSliceError; +use std::fs; +use std::io::{Read, Write}; +use std::path::Path; -use base64::{ - Engine, - engine::general_purpose::{STANDARD as B64_STANDARD, URL_SAFE_NO_PAD as B64_URL_SAFE_NO_PAD}, +use base64::Engine; +use base64::engine::general_purpose::{ + STANDARD as B64_STANDARD, URL_SAFE_NO_PAD as B64_URL_SAFE_NO_PAD, }; use crypto_secretbox::{KeyInit, XSalsa20Poly1305, aead}; -use rusty_paseto::Paseto; -use rusty_paseto::core as rusty_paseto; +use rusty_paseto::{Paseto, core as rusty_paseto}; use serde::{Deserialize, Serialize}; use thiserror::Error; use zeroize::Zeroize; @@ -645,9 +642,10 @@ pub fn reencrypt_sync( #[cfg(test)] mod test { - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[fixture] fn key() -> Key { Key::from([ @@ -726,19 +724,14 @@ mod test { let old = Key::from([0x11u8; 32]); let new = Key::from([0x22u8; 32]); - old.try_write_path(&path) - .expect("first write creates the file"); + old.try_write_path(&path).expect("first write creates the file"); // try_write_path refuses to replace a *different* key (correct for create-if-missing)... - assert!(matches!( - new.try_write_path(&path), - Err(KeyFileStoringError::AlreadyExists) - )); + assert!(matches!(new.try_write_path(&path), Err(KeyFileStoringError::AlreadyExists))); assert_eq!(Key::try_load_from_path(&path).unwrap(), old); // ...but overwrite_path deliberately replaces it, as key rotation requires. - new.overwrite_path(&path) - .expect("overwrite replaces the key"); + new.overwrite_path(&path).expect("overwrite replaces the key"); assert_eq!(Key::try_load_from_path(&path).unwrap(), new); let _ = fs::remove_dir_all(&dir); diff --git a/crates/atuin-common/src/filter.rs b/crates/atuin-common/src/filter.rs index 600e9e961..35aa8ec2e 100644 --- a/crates/atuin-common/src/filter.rs +++ b/crates/atuin-common/src/filter.rs @@ -1,7 +1,9 @@ -use crate::slice::{SortedDedupedSliceComparer, partition_dedup}; -use itertools::Itertools; use std::borrow::Borrow; +use itertools::Itertools; + +use crate::slice::{SortedDedupedSliceComparer, partition_dedup}; + /// Represents a filter with "or" semantics. /// /// The filter contains one or more items. An item is allowed through the filter if and only if it @@ -35,20 +37,10 @@ impl OrFilter { /// Create a [`OrFilter`] from a sorted, deduped, non-empty list. pub fn new_unchecked(sorted_deduped: L) -> Self { + debug_assert!(!sorted_deduped.as_ref().is_empty(), "`sorted` cannot be empty"); + debug_assert!(sorted_deduped.as_ref().is_sorted(), "`sorted` must be sorted"); debug_assert!( - !sorted_deduped.as_ref().is_empty(), - "`sorted` cannot be empty" - ); - debug_assert!( - sorted_deduped.as_ref().is_sorted(), - "`sorted` must be sorted" - ); - debug_assert!( - !sorted_deduped - .as_ref() - .iter() - .tuple_windows() - .any(|(a, b)| a == b), + !sorted_deduped.as_ref().iter().tuple_windows().any(|(a, b)| a == b), "`sorted` must not contain duplicates", ); Self { @@ -117,10 +109,7 @@ impl OrFilter { if self.is_all() { return true; } - self.inner - .as_ref() - .binary_search_by_key(&item, |item| item.borrow()) - .is_ok() + self.inner.as_ref().binary_search_by_key(&item, |item| item.borrow()).is_ok() } /// Turn this filter into a list of the items in the filter. @@ -316,10 +305,12 @@ mod sealed { #[cfg(test)] mod tests { - use super::*; + use std::collections::BTreeSet; + use proptest::prelude::*; use rstest::rstest; - use std::collections::BTreeSet; + + use super::*; /// Build a filter from a nonempty list of items. fn filter(items: &[&str]) -> OrFilter> { @@ -361,10 +352,7 @@ mod tests { #[test] fn from_list_sorts_and_dedupes() { let expected = ["", "bash", "zsh"].map(str::to_owned); - assert_eq!( - filter(&["zsh", "bash", "zsh", ""]).items(), - Items::Some(expected.as_slice()) - ); + assert_eq!(filter(&["zsh", "bash", "zsh", ""]).items(), Items::Some(expected.as_slice())); } #[rstest] @@ -446,12 +434,7 @@ mod tests { } fn sort_dedup(items: &[String]) -> Vec { - items - .iter() - .cloned() - .collect::>() - .into_iter() - .collect() + items.iter().cloned().collect::>().into_iter().collect() } proptest! { diff --git a/crates/atuin-common/src/logs.rs b/crates/atuin-common/src/logs.rs index e11190509..b78dc6932 100644 --- a/crates/atuin-common/src/logs.rs +++ b/crates/atuin-common/src/logs.rs @@ -1,7 +1,8 @@ -use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; + /// Log level for file logging. Maps to tracing's LevelFilter. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] diff --git a/crates/atuin-common/src/os/unix.rs b/crates/atuin-common/src/os/unix.rs index e552531ca..d72899543 100644 --- a/crates/atuin-common/src/os/unix.rs +++ b/crates/atuin-common/src/os/unix.rs @@ -1,8 +1,9 @@ //! Unix-specific utilities. -use rustix::fs; use std::path::Path; +use rustix::fs; + /// Get the current UID. pub fn uid() -> std::ffi::c_uint { rustix::process::getuid().as_raw() @@ -30,10 +31,11 @@ pub fn touch_file(path: &Path) -> std::io::Result<()> { #[cfg(test)] mod tests { - use super::*; use std::os::unix::fs::{MetadataExt, symlink}; use std::os::unix::net::UnixListener; + use super::*; + /// Set a file's timestamp to the Unix epoch. fn backdate(path: &Path) { let epoch = fs::Timespec { diff --git a/crates/atuin-common/src/path/display_rich.rs b/crates/atuin-common/src/path/display_rich.rs index 949763ff0..33eed8f46 100644 --- a/crates/atuin-common/src/path/display_rich.rs +++ b/crates/atuin-common/src/path/display_rich.rs @@ -135,10 +135,12 @@ impl + ?Sized> DisplayRichExt for T { #[cfg(test)] mod tests { - use super::DisplayRichExt; - use rstest::rstest; use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf}; + use rstest::rstest; + + use super::DisplayRichExt; + /// With no options, output is byte-identical to `Path::display()`. #[rstest] #[case(Path::new("relative").join("some").join("dir"))] @@ -163,41 +165,17 @@ mod tests { #[case] enabled: bool, #[case] expected: String, ) { - assert_eq!( - input.display_rich().trailing_slash(enabled).to_string(), - expected - ); + assert_eq!(input.display_rich().trailing_slash(enabled).to_string(), expected); } /// `display_rich()` is available on every `AsRef` input type. #[rstest] fn works_for_all_asref_path_types() { let expected = format!("bar{MAIN_SEPARATOR_STR}"); - assert_eq!( - "bar".display_rich().trailing_slash(true).to_string(), - expected - ); - assert_eq!( - String::from("bar") - .display_rich() - .trailing_slash(true) - .to_string(), - expected - ); - assert_eq!( - Path::new("bar") - .display_rich() - .trailing_slash(true) - .to_string(), - expected - ); - assert_eq!( - PathBuf::from("bar") - .display_rich() - .trailing_slash(true) - .to_string(), - expected - ); + assert_eq!("bar".display_rich().trailing_slash(true).to_string(), expected); + assert_eq!(String::from("bar").display_rich().trailing_slash(true).to_string(), expected); + assert_eq!(Path::new("bar").display_rich().trailing_slash(true).to_string(), expected); + assert_eq!(PathBuf::from("bar").display_rich().trailing_slash(true).to_string(), expected); } /// `relative_to(base)` strips `base` when the path is under it (empty when @@ -253,10 +231,7 @@ mod tests { fn relative_to_takes_priority_over_tilde() { let dir = Path::new("home").join("user"); let p = dir.join("proj"); - assert_eq!( - p.display_rich().relative_to(&dir).tilde(&dir).to_string(), - "proj" - ); + assert_eq!(p.display_rich().relative_to(&dir).tilde(&dir).to_string(), "proj"); } /// Enrichments compose with `trailing_slash`. The `relative_to`-to-base diff --git a/crates/atuin-common/src/rmp/decode.rs b/crates/atuin-common/src/rmp/decode.rs index 9166d8af8..2436ca5a8 100644 --- a/crates/atuin-common/src/rmp/decode.rs +++ b/crates/atuin-common/src/rmp/decode.rs @@ -1,10 +1,10 @@ use rmp::Marker; pub use rmp::decode::bytes::{Bytes, BytesReadError}; -pub use rmp::decode::{DecodeStringError, NumValueReadError, RmpRead, RmpReadErr, ValueReadError}; -pub use rmp::decode::{read_array_len, read_bin_len, read_map_len, read_str_len}; -pub use rmp::decode::{read_bool, read_int, read_str_from_slice}; -pub use rmp::decode::{read_i8, read_i16, read_i32, read_i64}; -pub use rmp::decode::{read_u8, read_u16, read_u32, read_u64}; +pub use rmp::decode::{ + DecodeStringError, NumValueReadError, RmpRead, RmpReadErr, ValueReadError, read_array_len, + read_bin_len, read_bool, read_i8, read_i16, read_i32, read_i64, read_int, read_map_len, + read_str_from_slice, read_str_len, read_u8, read_u16, read_u32, read_u64, +}; /// An error encountered while trying to decode a message with [`rmp`]. /// @@ -85,9 +85,7 @@ pub fn read_string<'a>(bytes: &mut Bytes<'a>) -> Result> if let DecodeStringError::TypeMismatch(_) = e { // The decode functions in `rmp::decode` consume the marker byte when there's a // type mismatch; make sure we do that too, as `read_optional` depends on it. - bytes - .read_u8() - .expect("TypeMismatch implies stream contains a marker byte"); + bytes.read_u8().expect("TypeMismatch implies stream contains a marker byte"); } return Err(e.into()); } @@ -247,7 +245,8 @@ where #[allow( unsafe_code, - reason = "Doing this without unsafe code is much less efficient. We would have to create an + reason = "Doing this without unsafe code is much less efficient. We would have to create \ + an `[Option; N]`, which could be up to twice the size, fill in each element, and then use `array::map` to call `Option::unwrap` on each element. Besides the overhead of `unwrap`, `array::map` is noted as being inefficient on large arrays." diff --git a/crates/atuin-common/src/rmp/encode.rs b/crates/atuin-common/src/rmp/encode.rs index f820392dc..d46b98fad 100644 --- a/crates/atuin-common/src/rmp/encode.rs +++ b/crates/atuin-common/src/rmp/encode.rs @@ -1,12 +1,11 @@ use std::convert::Infallible; use rmp::Marker; -pub use rmp::encode::{ByteBuf, RmpWrite, RmpWriteErr, ValueWriteError}; -pub use rmp::encode::{write_array_len, write_bin_len, write_map_len, write_str_len}; -pub use rmp::encode::{write_bin, write_bool, write_nil, write_str}; -pub use rmp::encode::{write_i8, write_i16, write_i32, write_i64}; -pub use rmp::encode::{write_sint, write_uint, write_uint8}; -pub use rmp::encode::{write_u8, write_u16, write_u32, write_u64}; +pub use rmp::encode::{ + ByteBuf, RmpWrite, RmpWriteErr, ValueWriteError, write_array_len, write_bin, write_bin_len, + write_bool, write_i8, write_i16, write_i32, write_i64, write_map_len, write_nil, write_sint, + write_str, write_str_len, write_u8, write_u16, write_u32, write_u64, write_uint, write_uint8, +}; /// An error encountered while trying to encode a message with [`rmp`]. /// @@ -74,11 +73,7 @@ where writer, bytes.into_iter().map(Ok::<_, Infallible>), write_bin_len, - |writer, byte| { - writer - .write_u8(byte) - .map_err(ValueWriteError::InvalidDataWrite) - }, + |writer, byte| writer.write_u8(byte).map_err(ValueWriteError::InvalidDataWrite), ) .map_err(Into::into) } @@ -164,9 +159,6 @@ where write(writer, item).map_err(|e| TryEncodeError::from(e.into())) })?; - assert_eq!( - len, count, - "programming error: incorrect implementation of ExactSizeIterator" - ); + assert_eq!(len, count, "programming error: incorrect implementation of ExactSizeIterator"); Ok(()) } diff --git a/crates/atuin-common/src/shell.rs b/crates/atuin-common/src/shell.rs index b45e51aa0..b47edceb5 100644 --- a/crates/atuin-common/src/shell.rs +++ b/crates/atuin-common/src/shell.rs @@ -1,4 +1,6 @@ -use std::{ffi::OsStr, path::Path, process::Command}; +use std::ffi::OsStr; +use std::path::Path; +use std::process::Command; use serde::Serialize; use sysinfo::{Process, System, get_current_pid}; @@ -52,9 +54,8 @@ impl Shell { } pub fn from_env() -> Shell { - std::env::var("ATUIN_SHELL").map_or(Shell::Unknown, |shell| { - Shell::from_string(shell.trim().to_lowercase()) - }) + std::env::var("ATUIN_SHELL") + .map_or(Shell::Unknown, |shell| Shell::from_string(shell.trim().to_lowercase())) } pub fn config_file(&self) -> Option { @@ -83,7 +84,7 @@ impl Shell { let path = if sys.contains("darwin") { // This works in my testing so far Shell::Sh.run_interactive([ - "dscl localhost -read \"/Local/Default/Users/$USER\" shell | awk '{print $2}'", + "dscl localhost -read \"/Local/Default/Users/$USER\" shell | awk '{print $2}'" ])? } else if cfg!(windows) { return Ok(Shell::Powershell); @@ -98,9 +99,7 @@ impl Shell { return Err(ShellError::NotSupported); } - Ok(Shell::from_string( - shell.unwrap().to_string_lossy().to_string(), - )) + Ok(Shell::from_string(shell.unwrap().to_string_lossy().to_string())) } pub fn from_string(name: String) -> Shell { diff --git a/crates/atuin-common/src/slice.rs b/crates/atuin-common/src/slice.rs index 6a9d65934..6b2204f53 100644 --- a/crates/atuin-common/src/slice.rs +++ b/crates/atuin-common/src/slice.rs @@ -46,10 +46,7 @@ where /// /// `sorted` must be sorted and contain no duplicates. pub fn new(sorted: &'a [T], iter: I) -> Self { - debug_assert!( - sorted.is_sorted_by_key(|s| s.borrow()), - "`sorted` must be sorted", - ); + debug_assert!(sorted.is_sorted_by_key(|s| s.borrow()), "`sorted` must be sorted",); debug_assert_eq!( { let mut vec = sorted.iter().collect::>(); @@ -97,10 +94,12 @@ where #[cfg(test)] mod tests { - use super::*; + use std::collections::BTreeSet; + use proptest::prelude::*; use rstest::rstest; - use std::collections::BTreeSet; + + use super::*; #[rstest] #[case(&[], &[], &[])] @@ -157,10 +156,7 @@ mod tests { )] _stack_size: [(); STACK_SIZE], ) { - assert_eq!( - SortedDedupedSliceComparer::new(sorted, iter).eq::(), - expected, - ); + assert_eq!(SortedDedupedSliceComparer::new(sorted, iter).eq::(), expected,); } fn sorted_deduped_slice() -> impl Strategy> { diff --git a/crates/atuin-common/src/string.rs b/crates/atuin-common/src/string.rs index bb77d5cd6..722556b00 100644 --- a/crates/atuin-common/src/string.rs +++ b/crates/atuin-common/src/string.rs @@ -12,7 +12,6 @@ pub use align::{AlignExt, Alignment}; pub use ellipsis::EllipsizeExt; pub use escape_non_printable_posix_ext::EscapeNonPrintablePosixExt; pub use non_nul_str::{ContainsNul, NonNulStr}; - #[cfg(feature = "unicode")] use unicode_width::UnicodeWidthStr; diff --git a/crates/atuin-common/src/string/align.rs b/crates/atuin-common/src/string/align.rs index 72758180d..5916e43d4 100644 --- a/crates/atuin-common/src/string/align.rs +++ b/crates/atuin-common/src/string/align.rs @@ -47,11 +47,12 @@ impl> AlignExt for T {} #[cfg(test)] mod tests { - use super::{AlignExt, Alignment}; - use crate::string::Measure; use pretty_assertions::assert_eq; use rstest::rstest; + use super::{AlignExt, Alignment}; + use crate::string::Measure; + #[rstest] #[case::start_pads_on_the_end("hi", Measure::Columns(5), Alignment::Start, "hi ")] #[case::end_pads_on_the_start("hi", Measure::Columns(5), Alignment::End, " hi")] @@ -87,9 +88,6 @@ mod tests { #[case] expected_borrowed: bool, ) { let result = input.pad_to(budget, align); - assert_eq!( - matches!(result, std::borrow::Cow::Borrowed(_)), - expected_borrowed - ); + assert_eq!(matches!(result, std::borrow::Cow::Borrowed(_)), expected_borrowed); } } diff --git a/crates/atuin-common/src/string/ellipsis.rs b/crates/atuin-common/src/string/ellipsis.rs index bc2c46c49..2805395be 100644 --- a/crates/atuin-common/src/string/ellipsis.rs +++ b/crates/atuin-common/src/string/ellipsis.rs @@ -124,7 +124,10 @@ pub struct Ellipsized<'a>(Repr<'a>); enum Repr<'a> { /// The whole result is one contiguous slice of the source; `source_offset` /// is the slice's byte offset in the source. - Contiguous { text: &'a str, source_offset: usize }, + Contiguous { + text: &'a str, + source_offset: usize, + }, /// A head slice, an indicator, and a tail slice - always with a marker. Spliced { head: &'a str, @@ -267,13 +270,14 @@ fn suffix_boundary(s: &str, max: usize, cost: impl Fn(&str) -> usize) -> usize { #[cfg(test)] mod tests { - use super::{EllipsizeExt, Indicator, Measure, Pos}; - use crate::string::align::Alignment; use pretty_assertions::assert_eq; use proptest::prelude::*; use rstest::rstest; use unicode_width::UnicodeWidthStr; + use super::{EllipsizeExt, Indicator, Measure, Pos}; + use crate::string::align::Alignment; + fn amount(b: Measure) -> usize { match b { Measure::Bytes(n) => n, @@ -514,12 +518,7 @@ mod tests { #[case] align: Alignment, #[case] expected: &str, ) { - assert_eq!( - input - .pad_ellipsize(budget, side, Indicator::UNICODE, align) - .as_ref(), - expected - ); + assert_eq!(input.pad_ellipsize(budget, side, Indicator::UNICODE, align).as_ref(), expected); } #[test] @@ -536,12 +535,7 @@ mod tests { )); // Padding needed -> owned. assert!(matches!( - "hi".pad_ellipsize( - Measure::Columns(5), - Pos::End, - Indicator::UNICODE, - Alignment::Start - ), + "hi".pad_ellipsize(Measure::Columns(5), Pos::End, Indicator::UNICODE, Alignment::Start), std::borrow::Cow::Owned(_) )); } @@ -555,10 +549,7 @@ mod tests { } fn any_budget() -> impl Strategy { - prop_oneof![ - (0usize..40).prop_map(Measure::Bytes), - (0usize..40).prop_map(Measure::Columns), - ] + prop_oneof![(0usize..40).prop_map(Measure::Bytes), (0usize..40).prop_map(Measure::Columns),] } proptest! { @@ -661,10 +652,7 @@ mod tests { #[test] fn source_index_fits_is_identity() { let e = "hi".ellipsize(Measure::Columns(10), Pos::Middle, Indicator::ASCII); - assert!(matches!( - std::borrow::Cow::from(e), - std::borrow::Cow::Borrowed(_) - )); + assert!(matches!(std::borrow::Cow::from(e), std::borrow::Cow::Borrowed(_))); assert_eq!(e.source_index(0), Some(0)); assert_eq!(e.source_index(1), Some(1)); } @@ -673,15 +661,9 @@ mod tests { fn display_writes_without_allocating_via_cow() { let e = "hello world".ellipsize(Measure::Columns(8), Pos::End, Indicator::ASCII); assert_eq!(e.to_string(), "hello..."); - assert!(matches!( - std::borrow::Cow::from(e), - std::borrow::Cow::Owned(_) - )); + assert!(matches!(std::borrow::Cow::from(e), std::borrow::Cow::Owned(_))); let fits = "hi".ellipsize(Measure::Columns(8), Pos::End, Indicator::ASCII); - assert!(matches!( - std::borrow::Cow::from(fits), - std::borrow::Cow::Borrowed(_) - )); + assert!(matches!(std::borrow::Cow::from(fits), std::borrow::Cow::Borrowed(_))); } } diff --git a/crates/atuin-common/src/string/escape_non_printable_posix_ext.rs b/crates/atuin-common/src/string/escape_non_printable_posix_ext.rs index c884bd7d4..014e77bab 100644 --- a/crates/atuin-common/src/string/escape_non_printable_posix_ext.rs +++ b/crates/atuin-common/src/string/escape_non_printable_posix_ext.rs @@ -50,10 +50,12 @@ impl> EscapeNonPrintablePosixExt for T {} #[cfg(test)] mod tests { - use super::EscapeNonPrintablePosixExt; + use std::borrow::Cow; + use proptest::prelude::*; use rstest::rstest; - use std::borrow::Cow; + + use super::EscapeNonPrintablePosixExt; /// Table-driven examples of the exact `cat -v` mapping. Each case is its own /// test, so a failure names precisely which input broke. diff --git a/crates/atuin-common/src/string/non_nul_str.rs b/crates/atuin-common/src/string/non_nul_str.rs index d43170bf9..e2774babd 100644 --- a/crates/atuin-common/src/string/non_nul_str.rs +++ b/crates/atuin-common/src/string/non_nul_str.rs @@ -70,10 +70,11 @@ where #[cfg(test)] mod tests { - use super::{ContainsNul, NonNulStr}; use proptest::prelude::*; use rstest::rstest; + use super::{ContainsNul, NonNulStr}; + #[rstest] #[case("echo hello")] #[case("")] diff --git a/crates/atuin-common/src/time/duration.rs b/crates/atuin-common/src/time/duration.rs index ac6b743a5..e65693667 100644 --- a/crates/atuin-common/src/time/duration.rs +++ b/crates/atuin-common/src/time/duration.rs @@ -165,9 +165,7 @@ impl DurationExt for std::time::Duration { fn try_new(secs: u64, nsecs: u64) -> Result { let carry = nsecs / 1_000_000_000; let nanos = (nsecs % 1_000_000_000) as u32; - let secs = secs - .checked_add(carry) - .ok_or(DurationOverflow { secs, nsecs })?; + let secs = secs.checked_add(carry).ok_or(DurationOverflow { secs, nsecs })?; Ok(std::time::Duration::new(secs, nanos)) } @@ -195,18 +193,17 @@ impl DurationExt for time::Duration { fn display(self) -> DurationDisplay { // negative durations are not renderable; clamp rather than invent a sign - std::time::Duration::try_from(self) - .unwrap_or_default() - .display() + std::time::Duration::try_from(self).unwrap_or_default().display() } } #[cfg(test)] mod tests { - use super::*; use proptest::prelude::*; use rstest::rstest; + use super::*; + #[rstest] #[case::zero(0, 0)] #[case::positive(1_500_000_000, 1_500_000_000)] @@ -214,10 +211,7 @@ mod tests { #[case::min_clamps_to_zero(i64::MIN, 0)] #[case::max(i64::MAX, i64::MAX as u128)] fn saturating_from_nanos_i64_clamps(#[case] nanos: i64, #[case] expected: u128) { - assert_eq!( - std::time::Duration::saturating_from_nanos_i64(nanos).as_nanos(), - expected - ); + assert_eq!(std::time::Duration::saturating_from_nanos_i64(nanos).as_nanos(), expected); assert_eq!( >::saturating_from_nanos_i64(nanos) .whole_nanoseconds(), @@ -230,16 +224,9 @@ mod tests { #[case::carries_up(1, 2_500_000_000, 3_500_000_000)] #[case::only_nanos(0, 1, 1)] fn try_new_sums_components(#[case] secs: u64, #[case] nsecs: u64, #[case] expected: u128) { + assert_eq!(std::time::Duration::try_new(secs, nsecs).unwrap().as_nanos(), expected); assert_eq!( - std::time::Duration::try_new(secs, nsecs) - .unwrap() - .as_nanos(), - expected - ); - assert_eq!( - >::try_new(secs, nsecs) - .unwrap() - .whole_nanoseconds(), + >::try_new(secs, nsecs).unwrap().whole_nanoseconds(), expected as i128 ); } @@ -249,10 +236,7 @@ mod tests { #[case::largest_nanos_without_carry(u64::MAX, 999_999_999, true)] #[case::carry_overflows_u64_seconds(u64::MAX, 1_000_000_000, false)] fn std_try_new_range(#[case] secs: u64, #[case] nsecs: u64, #[case] representable: bool) { - assert_eq!( - std::time::Duration::try_new(secs, nsecs).is_ok(), - representable - ); + assert_eq!(std::time::Duration::try_new(secs, nsecs).is_ok(), representable); } #[rstest] @@ -262,10 +246,7 @@ mod tests { #[case::past_u64_seconds(u64::MAX, 0, false)] #[case::carry_overflows_u64_seconds(u64::MAX, 1_000_000_000, false)] fn time_try_new_range(#[case] secs: u64, #[case] nsecs: u64, #[case] representable: bool) { - assert_eq!( - >::try_new(secs, nsecs).is_ok(), - representable - ); + assert_eq!(>::try_new(secs, nsecs).is_ok(), representable); } #[rstest] @@ -277,10 +258,7 @@ mod tests { #[case::truncates_not_rounds(7_199_000_000_000, "1h")] fn format_duration_shows_most_significant_unit(#[case] nanos: u64, #[case] expected: &str) { assert_eq!( - std::time::Duration::from_nanos(nanos) - .display() - .largest_unit() - .to_string(), + std::time::Duration::from_nanos(nanos).display().largest_unit().to_string(), expected ); } @@ -298,10 +276,7 @@ mod tests { #[case::days_stay_in_hours(259_200_000_000_000, "72h0m0s")] fn stopwatch_keeps_subsecond_resolution(#[case] nanos: i64, #[case] expected: &str) { assert_eq!( - std::time::Duration::saturating_from_nanos_i64(nanos) - .display() - .stopwatch() - .to_string(), + std::time::Duration::saturating_from_nanos_i64(nanos).display().stopwatch().to_string(), expected ); } diff --git a/crates/atuin-common/src/time/offset_date_time.rs b/crates/atuin-common/src/time/offset_date_time.rs index 9d6bd8d3c..bd3f2616f 100644 --- a/crates/atuin-common/src/time/offset_date_time.rs +++ b/crates/atuin-common/src/time/offset_date_time.rs @@ -2,7 +2,9 @@ use core::fmt; -use time::{OffsetDateTime, format_description::FormatItem, macros::format_description}; +use time::OffsetDateTime; +use time::format_description::FormatItem; +use time::macros::format_description; /// Lowest `time::OffsetDateTime` can represent (unix) `-9999-01-01 00:00:00 UTC`. const MIN_UNIX_NANOS: i128 = -377_705_116_800 * 1_000_000_000; @@ -113,14 +115,7 @@ impl fmt::Display for OffsetDateTimeDisplay { write!(f, "{year:04}")?; } - write!( - f, - "-{:02}-{:02} {:02}:{:02}", - t.month() as u8, - t.day(), - t.hour(), - t.minute() - )?; + write!(f, "-{:02}-{:02} {:02}:{:02}", t.month() as u8, t.day(), t.hour(), t.minute())?; match self.style { OffsetDateTimeStyle::YmdHms => write!(f, ":{:02}", t.second()), @@ -199,11 +194,12 @@ pub static YMD_HM: &[FormatItem<'_>] = #[cfg(test)] mod tests { - use super::*; use proptest::prelude::*; use rstest::rstest; use time::macros::datetime; + use super::*; + fn expected_nanos(secs: i128, nsecs: i128) -> Option { secs.checked_mul(1_000_000_000)?.checked_add(nsecs) } @@ -237,10 +233,7 @@ mod tests { #[case::i128_overflow_low(i128::MIN, 0, false)] #[case::i128_overflow_nsecs(0, i128::MAX, false)] fn from_timespec_range(#[case] secs: i128, #[case] nsecs: i128, #[case] representable: bool) { - assert_eq!( - OffsetDateTime::from_timespec(secs, nsecs).is_ok(), - representable - ); + assert_eq!(OffsetDateTime::from_timespec(secs, nsecs).is_ok(), representable); } #[rstest] @@ -280,10 +273,7 @@ mod tests { ) { let now = OffsetDateTime::from_unix_nanos_i64(now_secs * 1_000_000_000); let earlier = OffsetDateTime::from_unix_nanos_i64(earlier_secs * 1_000_000_000); - assert_eq!( - now.saturating_duration_since(earlier).as_secs(), - expected_secs - ); + assert_eq!(now.saturating_duration_since(earlier).as_secs(), expected_secs); } #[rstest] @@ -330,10 +320,7 @@ mod tests { #[case::negative_year(datetime!(-0044-03-15 00:00:00 UTC))] #[case::max(datetime!(9999-12-31 23:59:59 UTC))] fn display_matches_the_format_descriptors(#[case] t: OffsetDateTime) { - assert_eq!( - t.display().ymd_hms().to_string(), - t.format(YMD_HMS).unwrap() - ); + assert_eq!(t.display().ymd_hms().to_string(), t.format(YMD_HMS).unwrap()); assert_eq!(t.display().ymd_hm().to_string(), t.format(YMD_HM).unwrap()); } diff --git a/crates/atuin-common/src/time/utc_offset.rs b/crates/atuin-common/src/time/utc_offset.rs index 6f11a548b..244f030e1 100644 --- a/crates/atuin-common/src/time/utc_offset.rs +++ b/crates/atuin-common/src/time/utc_offset.rs @@ -4,7 +4,9 @@ use std::str::FromStr; use serde::Serialize; use serde_with::DeserializeFromStr; -use time::{UtcOffset, format_description::FormatItem, macros::format_description}; +use time::UtcOffset; +use time::format_description::FormatItem; +use time::macros::format_description; use tracing::warn; /// Extensions to [`UtcOffset`]. @@ -79,7 +81,8 @@ impl FromStr for UtcOffsetSpec { /// format: `<+|->[:[:]]` static OFFSET_FMT: &[FormatItem<'_>] = format_description!( - "[offset_hour sign:mandatory padding:none][optional [:[offset_minute padding:none][optional [:[offset_second padding:none]]]]]" + "[offset_hour sign:mandatory padding:none][optional [:[offset_minute padding:none][optional \ + [:[offset_second padding:none]]]]]" ); #[derive(Debug, Clone, Copy, thiserror::Error)] @@ -92,9 +95,10 @@ pub enum TimezoneDecodingError { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::no_sign("09:30")] #[case::garbage("not-a-timezone")] diff --git a/crates/atuin-common/src/url.rs b/crates/atuin-common/src/url.rs index cf6398052..540a54ccb 100644 --- a/crates/atuin-common/src/url.rs +++ b/crates/atuin-common/src/url.rs @@ -73,14 +73,12 @@ impl UrlAppendExt for Url { I: IntoIterator, S: AsRef, { - let trailing_empty = self.path_segments().map_or(0, |segments| { - segments.rev().take_while(|s| s.is_empty()).count() - }); + let trailing_empty = self + .path_segments() + .map_or(0, |segments| segments.rev().take_while(|s| s.is_empty()).count()); let mut url = self.clone(); - let mut path = url - .path_segments_mut() - .map_err(|()| UrlAppendError::NonSplittable)?; + let mut path = url.path_segments_mut().map_err(|()| UrlAppendError::NonSplittable)?; for _ in 0..trailing_empty { path.pop_if_empty(); @@ -101,9 +99,10 @@ impl UrlAppendExt for Url { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + fn parse(s: &str) -> Url { Url::parse(s).unwrap() } @@ -114,24 +113,12 @@ mod tests { #[case("https://host.example/atuin", "api", "https://host.example/atuin/api")] #[case("https://host.example/atuin/", "api", "https://host.example/atuin/api")] #[case("https://h.example", "a/b", "https://h.example/a%2Fb")] - #[case( - "https://h.example/x", - "../../etc", - "https://h.example/x/..%2F..%2Fetc" - )] + #[case("https://h.example/x", "../../etc", "https://h.example/x/..%2F..%2Fetc")] #[case("https://h.example", "john doe", "https://h.example/john%20doe")] #[case("https://h.example", "a?b#c", "https://h.example/a%3Fb%23c")] #[case("https://h.example:8443/x", "y", "https://h.example:8443/x/y")] - #[case( - "https://host.example/atuin//", - "api", - "https://host.example/atuin/api" - )] - #[case( - "https://host.example/atuin////", - "api", - "https://host.example/atuin/api" - )] + #[case("https://host.example/atuin//", "api", "https://host.example/atuin/api")] + #[case("https://host.example/atuin////", "api", "https://host.example/atuin/api")] #[case("https://h.example//", "me", "https://h.example/me")] #[case("https://h.example////", "me", "https://h.example/me")] #[case("https://h.example/a//b", "me", "https://h.example/a//b/me")] @@ -165,10 +152,7 @@ mod tests { #[test] fn dot_lookalikes_are_still_appended() { assert_eq!( - parse("https://h.example") - .append(["...", ".hidden", "a.b"]) - .unwrap() - .as_str(), + parse("https://h.example").append(["...", ".hidden", "a.b"]).unwrap().as_str(), "https://h.example/.../.hidden/a.b", ); } @@ -178,44 +162,26 @@ mod tests { // Two segments: each is encoded on its own, so the space in "john doe" // becomes %20 but does not merge with "user". assert_eq!( - parse("https://h.example") - .append(["user", "john doe"]) - .unwrap() - .as_str(), + parse("https://h.example").append(["user", "john doe"]).unwrap().as_str(), "https://h.example/user/john%20doe", ); // A `/` inside one element still doesn't act as a separator: it's // encoded to %2F, landing as part of that single segment. assert_eq!( - parse("https://h.example") - .append(["a", "b/c"]) - .unwrap() - .as_str(), + parse("https://h.example").append(["a", "b/c"]).unwrap().as_str(), "https://h.example/a/b%2Fc", ); } #[rstest] #[case("https://api.atuin.sh", "api/v0/me", "https://api.atuin.sh/api/v0/me")] - #[case( - "https://host.example/atuin", - "api/v0/me", - "https://host.example/atuin/api/v0/me" - )] - #[case( - "https://host.example/atuin/", - "api/v0/me", - "https://host.example/atuin/api/v0/me" - )] + #[case("https://host.example/atuin", "api/v0/me", "https://host.example/atuin/api/v0/me")] + #[case("https://host.example/atuin/", "api/v0/me", "https://host.example/atuin/api/v0/me")] #[case("https://h.example", "/api/v0/me", "https://h.example/api/v0/me")] #[case("https://h.example", "account", "https://h.example/account")] #[case("https://h.example//", "api/v0/me", "https://h.example/api/v0/me")] - #[case( - "https://host.example/atuin//", - "api/v0/me", - "https://host.example/atuin/api/v0/me" - )] + #[case("https://host.example/atuin//", "api/v0/me", "https://host.example/atuin/api/v0/me")] fn append_path_cases(#[case] base: &str, #[case] path: &'static str, #[case] expected: &str) { assert_eq!(parse(base).append_path(path).unwrap().as_str(), expected); } diff --git a/crates/atuin-common/src/utils.rs b/crates/atuin-common/src/utils.rs index ad140e8c6..94edbcb22 100644 --- a/crates/atuin-common/src/utils.rs +++ b/crates/atuin-common/src/utils.rs @@ -2,9 +2,8 @@ use std::env; use std::ffi::OsString; use std::path::{Path, PathBuf}; -use eyre::{Result, eyre}; - use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine}; +use eyre::{Result, eyre}; use getrandom::fill; use uuid::Uuid; @@ -120,9 +119,7 @@ pub fn env_nonempty(name: &str) -> Option { /// This is usually done in the name of XDG-compliance which requires that paths given through /// environment variables are absolute. pub fn env_abspath(name: &str) -> Option { - env_nonempty(name) - .map(PathBuf::from) - .filter(|s| s.is_absolute()) + env_nonempty(name).map(PathBuf::from).filter(|s| s.is_absolute()) } pub fn config_dir() -> PathBuf { @@ -209,10 +206,11 @@ where #[allow(unsafe_code)] #[cfg(test)] mod tests { - use super::*; use pretty_assertions::assert_ne; use rstest::rstest; + use super::*; + #[cfg(not(windows))] #[test] fn test_dirs() { @@ -231,10 +229,7 @@ mod tests { unsafe { env::remove_var("HOME") }; // TODO: Audit that the environment access only happens in single-threaded code. unsafe { env::set_var("XDG_CONFIG_HOME", "/home/user/custom_config") }; - assert_eq!( - config_dir(), - PathBuf::from("/home/user/custom_config/atuin") - ); + assert_eq!(config_dir(), PathBuf::from("/home/user/custom_config/atuin")); // TODO: Audit that the environment access only happens in single-threaded code. unsafe { env::remove_var("XDG_CONFIG_HOME") }; } diff --git a/crates/atuin-daemon/build.rs b/crates/atuin-daemon/build.rs index 7808a07b8..bd30fa35c 100644 --- a/crates/atuin-daemon/build.rs +++ b/crates/atuin-daemon/build.rs @@ -1,4 +1,5 @@ -use std::{env, fs, path::PathBuf}; +use std::path::PathBuf; +use std::{env, fs}; use protox::prost::Message; diff --git a/crates/atuin-daemon/src/client.rs b/crates/atuin-daemon/src/client.rs index 5431bb4df..d6b638635 100644 --- a/crates/atuin-daemon/src/client.rs +++ b/crates/atuin-daemon/src/client.rs @@ -1,41 +1,41 @@ +#[cfg(unix)] +use std::path::PathBuf; + use atuin_client::database::Context; +use atuin_client::history::History; use atuin_client::settings::{FilterMode, Settings}; use atuin_common::filter::{self, OrFilter}; use eyre::{Context as EyreContext, Result}; +use hyper_util::rt::TokioIo; #[cfg(windows)] use tokio::net::TcpStream; +#[cfg(unix)] +use tokio::net::UnixStream; use tonic::Code; use tonic::transport::{Channel, Endpoint, Uri}; use tower::service_fn; - -use hyper_util::rt::TokioIo; - -#[cfg(unix)] -use std::path::PathBuf; -#[cfg(unix)] -use tokio::net::UnixStream; - -use atuin_client::history::History; use tracing::{Level, instrument, span}; -use crate::control::HistoryRebuiltEvent; +use crate::control::control_client::ControlClient as ControlServiceClient; use crate::control::{ - ForceSyncEvent, HistoryDeletedEvent, HistoryPrunedEvent, SendEventRequest, - SettingsReloadedEvent, ShutdownEvent, control_client::ControlClient as ControlServiceClient, + ForceSyncEvent, HistoryDeletedEvent, HistoryPrunedEvent, HistoryRebuiltEvent, SendEventRequest, + SettingsReloadedEvent, ShutdownEvent, }; use crate::events::DaemonEvent; +use crate::history::history_client::HistoryClient as HistoryServiceClient; use crate::history::{ CancelHistoryReply, CancelHistoryRequest, EndHistoryReply, EndHistoryRequest, ShutdownRequest, StartHistoryReply, StartHistoryRequest, StatusReply, StatusRequest, TailHistoryReply, - TailHistoryRequest, history_client::HistoryClient as HistoryServiceClient, + TailHistoryRequest, }; +use crate::search::search_client::SearchClient as SearchServiceClient; use crate::search::{ FilterMode as RpcFilterMode, PrepareIndexRequest, SearchContext as RpcSearchContext, - SearchRequest, SearchResponse, search_client::SearchClient as SearchServiceClient, + SearchRequest, SearchResponse, }; +use crate::semantic::semantic_client::SemanticClient as SemanticServiceClient; use crate::semantic::{ CommandCapture, CommandOutputReply, CommandOutputRequest, OutputRange, RecordCommandsReply, - semantic_client::SemanticClient as SemanticServiceClient, }; pub struct HistoryClient { @@ -158,11 +158,7 @@ impl HistoryClient { } pub async fn tail_history(&mut self) -> Result> { - Ok(self - .client - .tail_history(TailHistoryRequest {}) - .await? - .into_inner()) + Ok(self.client.tail_history(TailHistoryRequest {}).await?.into_inner()) } pub async fn shutdown(&mut self) -> Result { @@ -301,9 +297,7 @@ impl From for RpcSearchContext { cwd: context.cwd, hostname: context.hostname, host_id: context.host_id, - git_root: context - .git_root - .map(|path| path.to_string_lossy().to_string()), + git_root: context.git_root.map(|path| path.to_string_lossy().to_string()), } } } @@ -385,10 +379,7 @@ impl SemanticClient { ) -> Result { let request = CommandOutputRequest { history_id, - ranges: ranges - .into_iter() - .map(|(start, end)| OutputRange { start, end }) - .collect(), + ranges: ranges.into_iter().map(|(start, end)| OutputRange { start, end }).collect(), }; Ok(self.client.command_output(request).await?.into_inner()) diff --git a/crates/atuin-daemon/src/components/history.rs b/crates/atuin-daemon/src/components/history.rs index 46fffe242..858c9a9ff 100644 --- a/crates/atuin-daemon/src/components/history.rs +++ b/crates/atuin-daemon/src/components/history.rs @@ -2,14 +2,14 @@ //! //! Handles command history lifecycle (start/end) and provides the History gRPC service. -use std::{pin::Pin, sync::Arc}; +use std::pin::Pin; +use std::sync::Arc; -use atuin_client::{ - database::Database, - history::{History, HistoryId, store::HistoryStore}, - packfile, - settings::Settings, -}; +use atuin_client::database::Database; +use atuin_client::history::store::HistoryStore; +use atuin_client::history::{History, HistoryId}; +use atuin_client::packfile; +use atuin_client::settings::Settings; use atuin_common::time::OffsetDateTimeExt; use atuin_domain::caps::PackfileCap; use atuin_domain::record::RecordTag; @@ -20,15 +20,13 @@ use tokio_stream::Stream; use tonic::{Request, Response, Status}; use tracing::{Level, instrument}; -use crate::{ - daemon::{Component, DaemonHandle}, - events::DaemonEvent, - history::{ - CancelHistoryReply, CancelHistoryRequest, EndHistoryReply, EndHistoryRequest, HistoryEntry, - HistoryEventKind, ShutdownReply, ShutdownRequest, StartHistoryReply, StartHistoryRequest, - StatusReply, StatusRequest, TailHistoryReply, TailHistoryRequest, - history_server::{History as HistorySvc, HistoryServer}, - }, +use crate::daemon::{Component, DaemonHandle}; +use crate::events::DaemonEvent; +use crate::history::history_server::{History as HistorySvc, HistoryServer}; +use crate::history::{ + CancelHistoryReply, CancelHistoryRequest, EndHistoryReply, EndHistoryRequest, HistoryEntry, + HistoryEventKind, ShutdownReply, ShutdownRequest, StartHistoryReply, StartHistoryRequest, + StatusReply, StatusRequest, TailHistoryReply, TailHistoryRequest, }; const DAEMON_PROTOCOL_VERSION: u32 = 1; @@ -91,11 +89,8 @@ impl Component for HistoryComponent { async fn start(&mut self, handle: DaemonHandle) -> Result<()> { // Create the history store let host_id = Settings::host_id().await?; - let history_store = HistoryStore::new( - handle.store().clone(), - host_id, - handle.encryption_key().clone(), - ); + let history_store = + HistoryStore::new(handle.store().clone(), host_id, handle.encryption_key().clone()); *self.inner.history_store.write().await = Some(history_store); *self.inner.handle.write().await = Some(handle); @@ -222,11 +217,7 @@ impl HistorySvc for HistoryGrpcService { .await .map_err(|e| Status::internal(format!("failed to write to db: {e:?}")))?; - tracing::info!( - id = id.0.to_string(), - duration = history.duration, - "end history" - ); + tracing::info!(id = id.0.to_string(), duration = history.duration, "end history"); // Push to record store let (record_id, idx) = history_store @@ -237,12 +228,7 @@ impl HistorySvc for HistoryGrpcService { if let Err(e) = packfile::try_pack( &history_store.store, history_store.host_id, - handle - .caps() - .get_server::() - .await - .ok() - .flatten(), + handle.caps().get_server::().await.ok().flatten(), &RecordTag::History, ) .await @@ -263,9 +249,7 @@ impl HistorySvc for HistoryGrpcService { return Ok(Response::new(reply)); } - Err(Status::not_found(format!( - "could not find history with id: {id}" - ))) + Err(Status::not_found(format!("could not find history with id: {id}"))) } #[instrument(skip_all, level = Level::INFO)] @@ -281,9 +265,7 @@ impl HistorySvc for HistoryGrpcService { protocol: DAEMON_PROTOCOL_VERSION, })) } else { - Err(Status::not_found(format!( - "could not find history with id: {id}" - ))) + Err(Status::not_found(format!("could not find history with id: {id}"))) } } diff --git a/crates/atuin-daemon/src/components/search.rs b/crates/atuin-daemon/src/components/search.rs index 7c465c04a..ff84b52e3 100644 --- a/crates/atuin-daemon/src/components/search.rs +++ b/crates/atuin-daemon/src/components/search.rs @@ -4,7 +4,8 @@ //! with frecency-based ranking and dynamic filtering. use std::ops::Deref; -use std::{pin::Pin, sync::Arc}; +use std::pin::Pin; +use std::sync::Arc; use atuin_client::database::Database; use atuin_common::filter::OrFilter; @@ -15,14 +16,12 @@ use tokio_stream::Stream; use tonic::{Request, Response, Status, Streaming}; use tracing::{Level, debug, error, info, instrument, span, trace}; -use crate::{ - daemon::{Component, DaemonHandle}, - events::DaemonEvent, - search::{ - FilterMode, IndexFilterMode, PrepareIndexRequest, PrepareIndexResponse, SearchIndex, - SearchRequest, SearchResponse, - search_server::{Search as SearchSvc, SearchServer}, - }, +use crate::daemon::{Component, DaemonHandle}; +use crate::events::DaemonEvent; +use crate::search::search_server::{Search as SearchSvc, SearchServer}; +use crate::search::{ + FilterMode, IndexFilterMode, PrepareIndexRequest, PrepareIndexResponse, SearchIndex, + SearchRequest, SearchResponse, }; const PAGE_SIZE: usize = 5000; @@ -45,19 +44,13 @@ where F: Fn() -> R, R: Future>, { - info!( - "Loading history into search index; page size = {}", - PAGE_SIZE - ); + info!("Loading history into search index; page size = {}", PAGE_SIZE); let db = handle.history_db(); let mut pager = db.all_paged(PAGE_SIZE, false, true); loop { match pager.next().await { Ok(Some(histories)) => { - info!( - "Loading {} history entries into search index", - histories.len() - ); + info!("Loading {} history entries into search index", histories.len()); index().await.add_histories(&histories); } Ok(None) => { @@ -160,10 +153,7 @@ impl SearchComponent { return; } - info!( - "Search index rebuild complete; {} unique commands", - new_index.command_count() - ); + info!("Search index rebuild complete; {} unique commands", new_index.command_count()); *self.index.write().await = new_index; } } @@ -191,13 +181,8 @@ impl Component for SearchComponent { // will use the value of the `ATUIN_SHELL` environment variable. This variable might be // correct if the daemon was autostarted by the shell hooks, but if it's unset or // incorrect, we'll simply rebuild the index upon receipt of the first request. - let shells = handle_for_loader - .settings() - .await - .search - .shells - .to_filter() - .to_vec_filter(); + let shells = + handle_for_loader.settings().await.search.shells.to_filter().to_vec_filter(); index.write().await.shells = shells; let _ = build_index(|| index.read(), &handle_for_loader).await; })); @@ -246,10 +231,7 @@ impl Component for SearchComponent { self.rebuild_index_only().await; } DaemonEvent::HistoryDeleted { ids } => { - info!( - count = ids.len(), - "History deleted, rebuilding search index" - ); + info!(count = ids.len(), "History deleted, rebuilding search index"); // For now, just rebuild the entire index. A more efficient implementation // would remove specific items from the index. self.rebuild_index_only().await; @@ -315,10 +297,7 @@ impl SearchGrpcService { let new_index = SearchIndex::new(shells); build_index(async || &new_index, &self.handle).await?; - info!( - "Search index rebuild complete; {} unique commands", - new_index.command_count() - ); + info!("Search index rebuild complete; {} unique commands", new_index.command_count()); Ok(Some(new_index)) } } @@ -351,10 +330,8 @@ impl SearchSvc for SearchGrpcService { let query = search_req.query; let query_id = search_req.query_id; - let filter_mode: FilterMode = search_req - .filter_mode - .try_into() - .unwrap_or(FilterMode::Global); + let filter_mode: FilterMode = + search_req.filter_mode.try_into().unwrap_or(FilterMode::Global); let proto_context = search_req.context; debug!( @@ -378,9 +355,7 @@ impl SearchSvc for SearchGrpcService { } Ok(None) => this.index.read().await, Err(()) => { - let _ = tx - .send(Err(Status::internal("failed to build index"))) - .await; + let _ = tx.send(Err(Status::internal("failed to build index"))).await; break; } }; @@ -388,10 +363,7 @@ impl SearchSvc for SearchGrpcService { // Perform the search let history_ids: Vec> = span!(Level::TRACE, "daemon_search_query", %query, query_id).in_scope(|| { - index - .search(&query, index_filter, RESULTS_LIMIT) - .map(Vec::from) - .collect() + index.search(&query, index_filter, RESULTS_LIMIT).map(Vec::from).collect() }); drop(index); diff --git a/crates/atuin-daemon/src/components/semantic.rs b/crates/atuin-daemon/src/components/semantic.rs index 8e4cdfa25..751d590da 100644 --- a/crates/atuin-daemon/src/components/semantic.rs +++ b/crates/atuin-daemon/src/components/semantic.rs @@ -13,13 +13,11 @@ use tokio::sync::Mutex; use tonic::{Request, Response, Status, Streaming}; use tracing::{Level, instrument}; -use crate::{ - daemon::{Component, DaemonHandle}, - events::DaemonEvent, - semantic::{ - CommandCapture, CommandOutputReply, CommandOutputRequest, OutputLine, RecordCommandsReply, - semantic_server::{Semantic as SemanticSvc, SemanticServer}, - }, +use crate::daemon::{Component, DaemonHandle}; +use crate::events::DaemonEvent; +use crate::semantic::semantic_server::{Semantic as SemanticSvc, SemanticServer}; +use crate::semantic::{ + CommandCapture, CommandOutputReply, CommandOutputRequest, OutputLine, RecordCommandsReply, }; const MAX_SESSIONS: usize = 20; @@ -206,10 +204,7 @@ impl SemanticState { if let Some(capture_ref) = self.history_index.get(&history_id).cloned() { if let Some(stored) = self.stored_capture_mut(&capture_ref) { stored.record.history = Some(history); - log_record( - &stored.record, - "associated semantic command capture with history", - ); + log_record(&stored.record, "associated semantic command capture with history"); return; } @@ -246,16 +241,11 @@ impl SemanticState { capture_ref: &CaptureRef, ranges: &[crate::semantic::OutputRange], ) -> Option { - let stored = self - .sessions - .get(&capture_ref.session_id)? - .stored_capture(capture_ref.capture_id)?; + let stored = + self.sessions.get(&capture_ref.session_id)?.stored_capture(capture_ref.capture_id)?; let output = &stored.record.capture.output; - let output_observed_bytes = stored - .record - .capture - .output_observed_bytes - .max(output.len() as u64); + let output_observed_bytes = + stored.record.capture.output_observed_bytes.max(output.len() as u64); Some(CommandOutputReply { found: true, @@ -326,28 +316,19 @@ impl SemanticState { history_id: &HistoryId, capture_id: CaptureId, ) { - if self - .history_index - .get(history_id) - .is_some_and(|capture_ref| { - &capture_ref.session_id == session_id && capture_ref.capture_id == capture_id - }) - { + if self.history_index.get(history_id).is_some_and(|capture_ref| { + &capture_ref.session_id == session_id && capture_ref.capture_id == capture_id + }) { self.history_index.remove(history_id); } } fn stored_capture_mut(&mut self, capture_ref: &CaptureRef) -> Option<&mut StoredCapture> { - self.sessions - .get_mut(&capture_ref.session_id)? - .stored_capture_mut(capture_ref.capture_id) + self.sessions.get_mut(&capture_ref.session_id)?.stored_capture_mut(capture_ref.capture_id) } fn record_count(&self) -> usize { - self.sessions - .values() - .map(|session| session.records.len()) - .sum() + self.sessions.values().map(|session| session.records.len()).sum() } } @@ -357,12 +338,7 @@ impl SessionCaptures { history_id: HistoryId, record: SemanticCommandRecord, ) -> (CaptureId, Vec) { - self.push_with_limits( - history_id, - record, - MAX_COMMANDS_PER_SESSION, - MAX_BYTES_PER_SESSION, - ) + self.push_with_limits(history_id, record, MAX_COMMANDS_PER_SESSION, MAX_BYTES_PER_SESSION) } fn push_with_limits( @@ -383,10 +359,7 @@ impl SessionCaptures { record, }); - ( - capture_id, - self.evict_to_limits(max_commands, max_output_bytes), - ) + (capture_id, self.evict_to_limits(max_commands, max_output_bytes)) } fn evict_to_limits( @@ -413,9 +386,7 @@ impl SessionCaptures { } fn stored_capture_mut(&mut self, capture_id: CaptureId) -> Option<&mut StoredCapture> { - self.records - .iter_mut() - .find(|record| record.id == capture_id) + self.records.iter_mut().find(|record| record.id == capture_id) } } @@ -486,17 +457,12 @@ fn take_pending_history( histories: &mut VecDeque, history_id: &HistoryId, ) -> Option { - let index = histories - .iter() - .position(|history| &history.id == history_id)?; + let index = histories.iter().position(|history| &history.id == history_id)?; histories.remove(index) } fn push_pending_history(histories: &mut VecDeque, history: History) { - if let Some(index) = histories - .iter() - .position(|pending| pending.id == history.id) - { + if let Some(index) = histories.iter().position(|pending| pending.id == history.id) { histories.remove(index); } @@ -553,21 +519,26 @@ fn select_output_ranges(output: &str, ranges: &[crate::semantic::OutputRange]) - merged .into_iter() .flat_map(|(start, end)| { - lines[start..=end] - .iter() - .enumerate() - .map(move |(offset, line)| OutputLine { - line_number: (start + offset + 1) as u64, - content: (*line).to_string(), - }) + lines[start..=end].iter().enumerate().map(move |(offset, line)| OutputLine { + line_number: (start + offset + 1) as u64, + content: (*line).to_string(), + }) }) .collect() } fn normalize_line_range(start: i64, end: i64, line_count: usize) -> Option<(usize, usize)> { let line_count = i64::try_from(line_count).ok()?; - let start = if start < 0 { line_count + start } else { start }; - let end = if end < 0 { line_count + end } else { end }; + let start = if start < 0 { + line_count + start + } else { + start + }; + let end = if end < 0 { + line_count + end + } else { + end + }; if end < 0 || start >= line_count { return None; @@ -581,16 +552,10 @@ fn normalize_line_range(start: i64, end: i64, line_count: usize) -> Option<(usiz fn log_record(record: &SemanticCommandRecord, message: &'static str) { let history_id = record.capture.history_id.as_deref().unwrap_or(""); - let associated_history_id = record - .history - .as_ref() - .map(|history| history.id.to_string()); + let associated_history_id = record.history.as_ref().map(|history| history.id.to_string()); let exit = record.history.as_ref().map(|history| history.exit); let duration = record.history.as_ref().map(|history| history.duration); - let author = record - .history - .as_ref() - .map(|history| history.author.as_str()); + let author = record.history.as_ref().map(|history| history.author.as_str()); let session_id = record.capture.session_id.as_deref(); tracing::debug!( @@ -612,10 +577,11 @@ fn log_record(record: &SemanticCommandRecord, message: &'static str) { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; use time::OffsetDateTime; + use super::*; + fn history(id: &str, session: &str, command: &str) -> History { History { id: HistoryId(id.to_string()), @@ -689,11 +655,7 @@ mod tests { state.record_history(history("id-1", "session-from-history", "cargo test")); assert!(state.record_capture(capture(Some("id-1"), None, "output"))); - assert!( - state - .sessions - .contains_key(&SessionId("session-from-history".to_string())) - ); + assert!(state.sessions.contains_key(&SessionId("session-from-history".to_string()))); assert!(command_output(&mut state, "id-1").found); } @@ -704,10 +666,7 @@ mod tests { assert!(state.record_capture(capture(Some("id-1"), Some("session-1"), "output"))); state.record_history(history("id-1", "session-1", "different command")); - let capture_ref = state - .history_index - .get(&HistoryId("id-1".to_string())) - .unwrap(); + let capture_ref = state.history_index.get(&HistoryId("id-1".to_string())).unwrap(); let stored = state .sessions .get(&capture_ref.session_id) @@ -849,10 +808,7 @@ mod tests { #[test] fn output_ranges_merge_overlaps_and_adjacent_ranges() { - let output = (0..100) - .map(|n| format!("line {n}")) - .collect::>() - .join("\n"); + let output = (0..100).map(|n| format!("line {n}")).collect::>().join("\n"); let ranges = vec![ crate::semantic::OutputRange { start: 0, end: 100 }, crate::semantic::OutputRange { @@ -870,10 +826,7 @@ mod tests { #[test] fn empty_output_ranges_default_to_first_thousand_lines() { - let output = (0..1001) - .map(|n| format!("line {n}")) - .collect::>() - .join("\n"); + let output = (0..1001).map(|n| format!("line {n}")).collect::>().join("\n"); let selected = select_output_ranges(&output, &[]); diff --git a/crates/atuin-daemon/src/components/sync.rs b/crates/atuin-daemon/src/components/sync.rs index a578bdc8d..a13b91856 100644 --- a/crates/atuin-daemon/src/components/sync.rs +++ b/crates/atuin-daemon/src/components/sync.rs @@ -2,25 +2,24 @@ //! //! Handles periodic synchronization with the Atuin cloud server. -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; +use std::time::Duration; +use atuin_client::history::HistoryId; +use atuin_client::history::store::HistoryStore; +use atuin_client::record::sync; +use atuin_client::settings::Settings; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use eyre::Result; -use futures::{StreamExt, TryStreamExt, stream::TryChunksError}; +use futures::stream::TryChunksError; +use futures::{StreamExt, TryStreamExt}; use rand::Rng; use tokio::sync::mpsc; use tokio::time::{self, MissedTickBehavior}; -use atuin_client::{ - history::{HistoryId, store::HistoryStore}, - record::sync, - settings::Settings, -}; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; - -use crate::{ - daemon::{Component, DaemonHandle}, - events::DaemonEvent, -}; +use crate::daemon::{Component, DaemonHandle}; +use crate::events::DaemonEvent; /// Commands that can be sent to the sync task. enum SyncCommand { @@ -217,13 +216,8 @@ async fn do_sync_tick( } // Perform the sync - let res = sync::sync( - settings, - handle.store(), - handle.encryption_key(), - handle.caps().clone(), - ) - .await; + let res = + sync::sync(settings, handle.store(), handle.encryption_key(), handle.caps().clone()).await; match res { Err(e) => { diff --git a/crates/atuin-daemon/src/control/service.rs b/crates/atuin-daemon/src/control/service.rs index 2e7403ce9..78f75fe25 100644 --- a/crates/atuin-daemon/src/control/service.rs +++ b/crates/atuin-daemon/src/control/service.rs @@ -7,12 +7,11 @@ use atuin_client::history::HistoryId; use tonic::{Request, Response, Status}; use tracing::{Level, info, instrument}; -use super::{ - SendEventRequest, SendEventResponse, - control_server::{Control, ControlServer}, - send_event_request::Event, -}; -use crate::{daemon::DaemonHandle, events::DaemonEvent}; +use super::control_server::{Control, ControlServer}; +use super::send_event_request::Event; +use super::{SendEventRequest, SendEventResponse}; +use crate::daemon::DaemonHandle; +use crate::events::DaemonEvent; /// The Control gRPC service. /// @@ -43,9 +42,7 @@ impl Control for ControlService { ) -> Result, Status> { let req = request.into_inner(); - let event = req - .event - .ok_or_else(|| Status::invalid_argument("event is required"))?; + let event = req.event.ok_or_else(|| Status::invalid_argument("event is required"))?; let daemon_event = proto_event_to_daemon_event(event)?; diff --git a/crates/atuin-daemon/src/daemon.rs b/crates/atuin-daemon/src/daemon.rs index 3d3b2ec38..6d0130d76 100644 --- a/crates/atuin-daemon/src/daemon.rs +++ b/crates/atuin-daemon/src/daemon.rs @@ -11,10 +11,10 @@ use std::sync::Arc; -use atuin_client::{ - api_client::caps_client, database::Sqlite as HistoryDatabase, - record::sqlite_store::SqliteStore, settings::Settings, -}; +use atuin_client::api_client::caps_client; +use atuin_client::database::Sqlite as HistoryDatabase; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use atuin_common::encryption::paseto_v4; use atuin_domain::caps::CapClient; use enum_dispatch::enum_dispatch; @@ -227,10 +227,7 @@ impl std::fmt::Debug for DaemonHandle { /// } /// ``` #[enum_dispatch] -#[allow( - async_fn_in_trait, - reason = "only used within our code and we don't need it to be Send" -)] +#[allow(async_fn_in_trait, reason = "only used within our code and we don't need it to be Send")] pub trait Component: Send + Sync + Into { /// Human-readable name for logging and debugging. fn name(&self) -> &'static str; @@ -334,10 +331,7 @@ impl Daemon { self.dispatch_event(&event).await; } Err(broadcast::error::RecvError::Lagged(n)) => { - tracing::warn!( - skipped = n, - "event receiver lagged, some events were dropped" - ); + tracing::warn!(skipped = n, "event receiver lagged, some events were dropped"); } Err(broadcast::error::RecvError::Closed) => { tracing::info!("event bus closed, stopping daemon"); @@ -453,9 +447,7 @@ impl DaemonBuilder { /// This loads the encryption key and creates the daemon state. pub async fn build(self) -> Result { let store = self.store.ok_or_else(|| eyre::eyre!("store is required"))?; - let history_db = self - .history_db - .ok_or_else(|| eyre::eyre!("history_db is required"))?; + let history_db = self.history_db.ok_or_else(|| eyre::eyre!("history_db is required"))?; // Load encryption key let encryption_key = paseto_v4::Key::try_load_or_generate(&self.settings.key_path) diff --git a/crates/atuin-daemon/src/lib.rs b/crates/atuin-daemon/src/lib.rs index ede619f58..86d8e3103 100644 --- a/crates/atuin-daemon/src/lib.rs +++ b/crates/atuin-daemon/src/lib.rs @@ -1,6 +1,7 @@ use atuin_client::database::Sqlite as HistoryDatabase; use atuin_client::record::sqlite_store::SqliteStore; -use atuin_client::settings::{Settings, watcher::global_settings_watcher}; +use atuin_client::settings::Settings; +use atuin_client::settings::watcher::global_settings_watcher; use eyre::Result; pub mod client; @@ -14,14 +15,12 @@ pub mod semantic; pub mod server; // Re-export core daemon types for convenience -pub use daemon::{AnyComponent, Daemon, DaemonBuilder, DaemonHandle}; -pub use events::DaemonEvent; - -// Re-export components -pub use components::{HistoryComponent, SearchComponent, SemanticComponent, SyncComponent}; - // Re-export client helpers pub use client::{ControlClient, SemanticClient, emit_event, emit_event_with_settings}; +// Re-export components +pub use components::{HistoryComponent, SearchComponent, SemanticComponent, SyncComponent}; +pub use daemon::{AnyComponent, Daemon, DaemonBuilder, DaemonHandle}; +pub use events::DaemonEvent; /// Boot the daemon using the new component-based architecture. /// @@ -130,7 +129,5 @@ async fn shutdown_signal() { /// Wait for a shutdown signal (Ctrl+C). #[cfg(not(unix))] async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("failed to listen for ctrl+c"); + tokio::signal::ctrl_c().await.expect("failed to listen for ctrl+c"); } diff --git a/crates/atuin-daemon/src/search/index.rs b/crates/atuin-daemon/src/search/index.rs index a0436c175..39b15bbe5 100644 --- a/crates/atuin-daemon/src/search/index.rs +++ b/crates/atuin-daemon/src/search/index.rs @@ -7,7 +7,6 @@ //! - Frecency-based ranking (frequency + recency) //! - Dynamic filtering by directory, host, session, etc. -use super::normalize_diacritics; use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashSet; @@ -23,6 +22,8 @@ use time::OffsetDateTime; use tracing::{Level, instrument}; use uuid::Uuid; +use super::normalize_diacritics; + /// Parse a UUID string into a 16-byte array. /// Returns None if the string is not a valid UUID. fn parse_uuid_bytes(s: &str) -> Option<[u8; 16]> { @@ -194,9 +195,7 @@ impl CommandData { /// Check if any invocation matches a directory prefix (workspace/git root). /// O(n) where n = number of unique directories for this command. pub fn has_invocation_in_workspace(&self, prefix: &str, interner: &ThreadedRodeo) -> bool { - self.directories - .iter() - .any(|&spur| interner.resolve(&spur).starts_with(prefix)) + self.directories.iter().any(|&spur| interner.resolve(&spur).starts_with(prefix)) } /// Check if any invocation matches an interned hostname. @@ -244,13 +243,13 @@ impl IndexFilterMode { fn compile(&self, interner: &ThreadedRodeo) -> CompiledFilter<'_> { match self { Self::Global => CompiledFilter::All, - Self::Directory(dir) => interner - .get(dir) - .map_or(CompiledFilter::Nothing, CompiledFilter::Directory), + Self::Directory(dir) => { + interner.get(dir).map_or(CompiledFilter::Nothing, CompiledFilter::Directory) + } Self::Workspace(prefix) => CompiledFilter::Workspace(prefix), - Self::Host(hostname) => interner - .get(hostname) - .map_or(CompiledFilter::Nothing, CompiledFilter::Host), + Self::Host(hostname) => { + interner.get(hostname).map_or(CompiledFilter::Nothing, CompiledFilter::Host) + } Self::Session(session) => { parse_uuid_bytes(session).map_or(CompiledFilter::Nothing, CompiledFilter::Session) } @@ -373,10 +372,7 @@ impl SearchIndex { if is_known_agent(&history.author) { return; } - if !self - .shells - .contains(history.shell.as_deref().unwrap_or_default()) - { + if !self.shells.contains(history.shell.as_deref().unwrap_or_default()) { return; } @@ -473,10 +469,7 @@ impl SearchIndex { let candidate_frecency = |candidate_index: usize| { let hay_idx = candidates[candidate_index] as usize; - frecency_map - .as_ref() - .and_then(|f| f.get(hay_idx).copied()) - .unwrap_or(0) + frecency_map.as_ref().and_then(|f| f.get(hay_idx).copied()).unwrap_or(0) }; let config = frizbee::Config::default() @@ -497,10 +490,8 @@ impl SearchIndex { } else { // This is a vec of `&Arc` instead of `&str` because `&Arc` is the size of one // pointer while `&str` is the size of two. - let normalized_commands: Vec<&Arc> = candidates - .iter() - .map(|i| &haystack[*i as usize].normalized) - .collect(); + let normalized_commands: Vec<&Arc> = + candidates.iter().map(|i| &haystack[*i as usize].normalized).collect(); // Use all cores when the number of commands is sufficiently large. let threads = std::thread::available_parallelism().map_or(1, |n| n.get()); let matches = tracing::span!(Level::TRACE, "index_search_match").in_scope(|| { @@ -565,8 +556,7 @@ impl SearchIndex { .map(|hay| { self.commands.get(hay.original.as_ref()).map_or(0, |data| { let frecency = - data.global_frecency - .compute(now, recency_mul, frequency_mul); + data.global_frecency.compute(now, recency_mul, frequency_mul); // Apply overall frecency multiplier and round to u32 (frecency as f64 * frecency_mul).round() as u32 }) @@ -586,17 +576,13 @@ impl Default for SearchIndex { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; use time::macros::datetime; + use super::*; + fn make_history(command: &str, cwd: &str, timestamp: OffsetDateTime) -> History { - History::import() - .timestamp(timestamp) - .command(command) - .cwd(cwd) - .build() - .into() + History::import().timestamp(timestamp).command(command).cwd(cwd).build().into() } #[test] @@ -712,56 +698,29 @@ mod tests { let (check1, check2, check3) = if cfg!(windows) { ( - "C:\\Users\\User\\project" - .display_rich() - .trailing_slash(true) - .to_string(), - "C:\\Users\\User\\other" - .display_rich() - .trailing_slash(true) - .to_string(), - "C:\\Users\\User\\missing" - .display_rich() - .trailing_slash(true) - .to_string(), + "C:\\Users\\User\\project".display_rich().trailing_slash(true).to_string(), + "C:\\Users\\User\\other".display_rich().trailing_slash(true).to_string(), + "C:\\Users\\User\\missing".display_rich().trailing_slash(true).to_string(), ) } else { ( - "/home/user/project" - .display_rich() - .trailing_slash(true) - .to_string(), - "/home/user/other" - .display_rich() - .trailing_slash(true) - .to_string(), - "/home/user/missing" - .display_rich() - .trailing_slash(true) - .to_string(), + "/home/user/project".display_rich().trailing_slash(true).to_string(), + "/home/user/other".display_rich().trailing_slash(true).to_string(), + "/home/user/missing".display_rich().trailing_slash(true).to_string(), ) }; - let in_dir = |dir: &str| { - interner - .get(dir) - .is_some_and(|spur| data.has_invocation_in_dir(spur)) - }; + let in_dir = + |dir: &str| interner.get(dir).is_some_and(|spur| data.has_invocation_in_dir(spur)); assert!(in_dir(&check1)); assert!(in_dir(&check2)); assert!(!in_dir(&check3)); let (check1, check2, check3) = if cfg!(windows) { ( - "C:\\Users\\User" - .display_rich() - .trailing_slash(true) - .to_string(), + "C:\\Users\\User".display_rich().trailing_slash(true).to_string(), "C:\\Users".display_rich().trailing_slash(true).to_string(), - "C:\\Users\\User\\var" - .display_rich() - .trailing_slash(true) - .to_string(), + "C:\\Users\\User\\var".display_rich().trailing_slash(true).to_string(), ) } else { ( @@ -780,21 +739,13 @@ mod tests { fn search_index_add_and_search() { let index = SearchIndex::default(); - let h1 = make_history( - "git status", - "/home/user/project", - datetime!(2024-01-01 10:00 UTC), - ); + let h1 = make_history("git status", "/home/user/project", datetime!(2024-01-01 10:00 UTC)); let h2 = make_history( "git commit -m 'test'", "/home/user/project", datetime!(2024-01-01 10:05 UTC), ); - let h3 = make_history( - "ls -la", - "/home/user/other", - datetime!(2024-01-01 10:10 UTC), - ); + let h3 = make_history("ls -la", "/home/user/other", datetime!(2024-01-01 10:10 UTC)); index.add_history(&h1); index.add_history(&h2); @@ -811,10 +762,7 @@ mod tests { .search( "", IndexFilterMode::Directory( - "/home/user/project" - .display_rich() - .trailing_slash(true) - .to_string(), + "/home/user/project".display_rich().trailing_slash(true).to_string(), ), 10, ) @@ -836,27 +784,18 @@ mod tests { // scattered match (b..a..r spread over "build-analyzer-report"), run // 200 times so its frecency dwarfs any fuzzy score difference for _ in 0..200 { - let h = make_history( - "foo build-analyzer-report", - "/tmp", - datetime!(2024-01-01 10:00 UTC), - ); + let h = + make_history("foo build-analyzer-report", "/tmp", datetime!(2024-01-01 10:00 UTC)); index.add_history(&h); } index.rebuild_frecency(&Search::default()); - let results: Vec<_> = index - .search("foo bar", IndexFilterMode::Global, 10) - .collect(); + let results: Vec<_> = index.search("foo bar", IndexFilterMode::Global, 10).collect(); assert_eq!(results.len(), 2); assert_eq!( results[0], - index - .commands - .get("foo bar --baz") - .unwrap() - .most_recent_id(), + index.commands.get("foo bar --baz").unwrap().most_recent_id(), "contiguous match must rank above the high-frecency scattered match" ); } @@ -868,11 +807,7 @@ mod tests { fn equal_matches_order_by_frecency() { let index = SearchIndex::default(); - index.add_history(&make_history( - "echo alpha", - "/tmp", - datetime!(2024-01-01 10:00 UTC), - )); + index.add_history(&make_history("echo alpha", "/tmp", datetime!(2024-01-01 10:00 UTC))); // same fuzzy score for the query, much higher frecency for _ in 0..50 { let h = make_history("echo beta", "/tmp", datetime!(2024-01-01 10:00 UTC)); @@ -898,16 +833,8 @@ mod tests { fn diacritics_normalized_for_matching() { let index = SearchIndex::default(); - index.add_history(&make_history( - "echo déjà-vu", - "/tmp", - datetime!(2024-01-01 10:00 UTC), - )); - index.add_history(&make_history( - "echo plain", - "/tmp", - datetime!(2024-01-01 10:00 UTC), - )); + index.add_history(&make_history("echo déjà-vu", "/tmp", datetime!(2024-01-01 10:00 UTC))); + index.add_history(&make_history("echo plain", "/tmp", datetime!(2024-01-01 10:00 UTC))); let expected = index.commands.get("echo déjà-vu").unwrap().most_recent_id(); @@ -932,9 +859,7 @@ mod tests { "kubectl get pods -n", "make -j", ]; - (0..12_000) - .map(|i| format!("{} run-{i}", prefixes[i % prefixes.len()])) - .collect() + (0..12_000).map(|i| format!("{} run-{i}", prefixes[i % prefixes.len()])).collect() } /// The parallel-matching switch must be invisible in the results: @@ -977,10 +902,7 @@ mod tests { let ts = datetime!(2024-01-01 00:00 UTC) + time::Duration::minutes((i % 1440) as i64); index.add_history(&make_history(command, "/tmp", ts)); } - assert!( - index.command_count() > 10_000, - "corpus must cross threshold" - ); + assert!(index.command_count() > 10_000, "corpus must cross threshold"); index.rebuild_frecency(&Search::default()); for query in ["git", "git p", "docker compose up", "deja", ""] { @@ -997,16 +919,10 @@ mod tests { #[test] fn long_query_truncated_not_panicking() { let index = SearchIndex::default(); - index.add_history(&make_history( - "echo hello", - "/tmp", - datetime!(2024-01-01 10:00 UTC), - )); + index.add_history(&make_history("echo hello", "/tmp", datetime!(2024-01-01 10:00 UTC))); let long_query = "a".repeat(5000); - let results: Vec<_> = index - .search(&long_query, IndexFilterMode::Global, 10) - .collect(); + let results: Vec<_> = index.search(&long_query, IndexFilterMode::Global, 10).collect(); assert!(results.is_empty()); } diff --git a/crates/atuin-daemon/src/server.rs b/crates/atuin-daemon/src/server.rs index 2c8a07346..f62483730 100644 --- a/crates/atuin-daemon/src/server.rs +++ b/crates/atuin-daemon/src/server.rs @@ -1,16 +1,16 @@ +use atuin_client::settings::Settings; use eyre::Result; use crate::components::history::HistoryGrpcService; use crate::components::search::SearchGrpcService; use crate::components::semantic::SemanticGrpcService; -use crate::control::{ControlService, control_server::ControlServer}; +use crate::control::ControlService; +use crate::control::control_server::ControlServer; use crate::daemon::DaemonHandle; use crate::history::history_server::HistoryServer; use crate::search::search_server::SearchServer; use crate::semantic::semantic_server::SemanticServer; -use atuin_client::settings::Settings; - /// How often to update the socket's modification time so it doesn't get automatically deleted by /// temporary file cleaners. #[cfg(unix)] @@ -37,9 +37,10 @@ pub async fn run_grpc_server( let (uds, cleanup_path) = if cfg!(target_os = "linux") && settings.daemon.systemd_socket { #[cfg(target_os = "linux")] { - use eyre::{OptionExt, WrapErr}; use std::os::unix::net::SocketAddr; use std::path::PathBuf; + + use eyre::{OptionExt, WrapErr}; tracing::info!("getting systemd socket"); let listener = listenfd::ListenFd::from_env() .take_unix_listener(0)? @@ -66,7 +67,7 @@ pub async fn run_grpc_server( Err(err) => { tracing::warn!( "could not detect systemd socket path, ensure that it's at the configured \ - path: {:?}, error: {err:?}", + path: {:?}, error: {err:?}", socket_path.as_path(), ); } @@ -78,10 +79,7 @@ pub async fn run_grpc_server( } else { socket_path.create_default_dir_if_needed()?; tracing::info!("listening on unix socket {:?}", socket_path.as_path()); - ( - UnixListener::bind(&socket_path)?, - Some(socket_path.into_owned()), - ) + (UnixListener::bind(&socket_path)?, Some(socket_path.into_owned())) }; let uds_stream = UnixListenerStream::new(uds); diff --git a/crates/atuin-daemon/tests/lifecycle.rs b/crates/atuin-daemon/tests/lifecycle.rs index d298bf0ed..d40dfe732 100644 --- a/crates/atuin-daemon/tests/lifecycle.rs +++ b/crates/atuin-daemon/tests/lifecycle.rs @@ -140,10 +140,7 @@ mod unix { let start_reply = client.start_history(history).await.unwrap(); assert!(!start_reply.id.is_empty()); - let end_reply = client - .end_history(start_reply.id, 1_000_000, 0) - .await - .unwrap(); + let end_reply = client.end_history(start_reply.id, 1_000_000, 0).await.unwrap(); assert!(!end_reply.id.is_empty()); } @@ -173,10 +170,7 @@ mod unix { let start_reply = client.start_history(history).await.unwrap(); let started = stream.message().await.unwrap().unwrap(); - assert_eq!( - HistoryEventKind::try_from(started.kind).unwrap(), - HistoryEventKind::Started - ); + assert_eq!(HistoryEventKind::try_from(started.kind).unwrap(), HistoryEventKind::Started); let started_history = started.history.unwrap(); assert_eq!(started_history.id, start_reply.id); assert_eq!(started_history.command, "git status"); @@ -185,16 +179,10 @@ mod unix { assert_eq!(started_history.author, "claude"); assert_eq!(started_history.intent, "inspect repository state"); - client - .end_history(start_reply.id.clone(), 1_000_000, 0) - .await - .unwrap(); + client.end_history(start_reply.id.clone(), 1_000_000, 0).await.unwrap(); let ended = stream.message().await.unwrap().unwrap(); - assert_eq!( - HistoryEventKind::try_from(ended.kind).unwrap(), - HistoryEventKind::Ended - ); + assert_eq!(HistoryEventKind::try_from(ended.kind).unwrap(), HistoryEventKind::Ended); let ended_history = ended.history.unwrap(); assert_eq!(ended_history.id, start_reply.id); assert_eq!(ended_history.exit, 0); @@ -208,9 +196,7 @@ mod unix { ) { let (mut client, _handle, _tmp) = daemon.await; - let result = client - .end_history("nonexistent-id".to_string(), 1000, 0) - .await; + let result = client.end_history("nonexistent-id".to_string(), 1000, 0).await; assert!(result.is_err()); } diff --git a/crates/atuin-daemon/tests/search.rs b/crates/atuin-daemon/tests/search.rs index 2a71a54f4..6b1392537 100644 --- a/crates/atuin-daemon/tests/search.rs +++ b/crates/atuin-daemon/tests/search.rs @@ -169,10 +169,8 @@ mod unix { } fn ids_of(entries: &[&History]) -> Vec { - let mut ids: Vec = entries - .iter() - .map(|h| Uuid::parse_str(&h.id.0).unwrap().to_string()) - .collect(); + let mut ids: Vec = + entries.iter().map(|h| Uuid::parse_str(&h.id.0).unwrap().to_string()).collect(); ids.sort(); ids } @@ -187,13 +185,7 @@ mod unix { // alpha: bash, host-a, session A, inside the workspace // beta: zsh, host-b, session B, outside the workspace // gamma: unknown shell, host-a, session A, workspace root - let alpha = seed_history( - "echo alpha", - "/work/repo/sub", - "host-a", - SESSION_A, - Some("bash"), - ); + let alpha = seed_history("echo alpha", "/work/repo/sub", "host-a", SESSION_A, Some("bash")); let beta = seed_history("echo beta", "/elsewhere", "host-b", SESSION_B, Some("zsh")); let gamma = seed_history("echo gamma", "/work/repo", "host-a", SESSION_A, None); let seeded = [alpha.clone(), beta.clone(), gamma.clone()]; @@ -206,15 +198,8 @@ mod unix { let deadline = tokio::time::Instant::now() + Duration::from_secs(10); loop { query_id += 1; - let results = search( - &mut client, - query_id, - "echo", - FilterMode::Global, - ctx(), - &[], - ) - .await; + let results = + search(&mut client, query_id, "echo", FilterMode::Global, ctx(), &[]).await; if results.len() == 3 { break; } @@ -227,15 +212,8 @@ mod unix { // Directory: exact cwd match only. query_id += 1; - let results = search( - &mut client, - query_id, - "echo", - FilterMode::Directory, - ctx(), - &[], - ) - .await; + let results = + search(&mut client, query_id, "echo", FilterMode::Directory, ctx(), &[]).await; assert_eq!(sorted(results), ids_of(&[&gamma]), "directory filter"); // Workspace: everything under the git root. @@ -266,15 +244,7 @@ mod unix { // Session: both session-A commands. query_id += 1; - let results = search( - &mut client, - query_id, - "echo", - FilterMode::Session, - ctx(), - &[], - ) - .await; + let results = search(&mut client, query_id, "echo", FilterMode::Session, ctx(), &[]).await; assert_eq!(sorted(results), ids_of(&[&alpha, &gamma]), "session"); // Unknown filter targets match nothing rather than erroring. @@ -299,15 +269,8 @@ mod unix { (vec![], ids_of(&[&alpha, &beta, &gamma]), "back to all"), ] { query_id += 1; - let results = search( - &mut client, - query_id, - "echo", - FilterMode::Global, - ctx(), - &shells, - ) - .await; + let results = + search(&mut client, query_id, "echo", FilterMode::Global, ctx(), &shells).await; assert_eq!(sorted(results), expected, "shell filter: {label}"); } diff --git a/crates/atuin-domain/src/api.rs b/crates/atuin-domain/src/api.rs index 65af1a8a7..495cef7c4 100644 --- a/crates/atuin-domain/src/api.rs +++ b/crates/atuin-domain/src/api.rs @@ -1,9 +1,9 @@ -use semver::Version; -use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashMap; use std::sync::LazyLock; +use semver::Version; +use serde::{Deserialize, Serialize}; use url::Url; // the usage of X- has been deprecated for quite along time, it turns out diff --git a/crates/atuin-domain/src/caps/axum.rs b/crates/atuin-domain/src/caps/axum.rs index e932c928a..4bc4f6930 100644 --- a/crates/atuin-domain/src/caps/axum.rs +++ b/crates/atuin-domain/src/caps/axum.rs @@ -7,23 +7,19 @@ use std::sync::Arc; -use axum::{ - Router, - extract::{Request, State}, - http::{HeaderName, HeaderValue, StatusCode, header::CONTENT_TYPE}, - middleware::Next, - response::{IntoResponse, Response}, -}; +use axum::Router; +use axum::extract::{Request, State}; +use axum::http::header::CONTENT_TYPE; +use axum::http::{HeaderName, HeaderValue, StatusCode}; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; use super::http::{AVAILABLE_HEADER, ENFORCE_HEADER, KNOWN_HEADER}; use super::{CapServer, Negotiation}; /// `GET /api/v0/capabilities` -- serve the pre-serialized capability document. pub async fn get(State(caps): State>) -> Response { - ( - [(CONTENT_TYPE, HeaderValue::from_static("application/json"))], - caps.body().to_owned(), - ) + ([(CONTENT_TYPE, HeaderValue::from_static("application/json"))], caps.body().to_owned()) .into_response() } @@ -40,10 +36,7 @@ pub async fn get(State(caps): State>) -> Response { async fn check_token(State(caps): State>, request: Request, next: Next) -> Response { let enforce = request.headers().contains_key(ENFORCE_HEADER); let negotiation = { - let known = request - .headers() - .get(KNOWN_HEADER) - .and_then(|value| value.to_str().ok()); + let known = request.headers().get(KNOWN_HEADER).and_then(|value| value.to_str().ok()); caps.negotiate(known) }; @@ -60,9 +53,7 @@ async fn check_token(State(caps): State>, request: Request, next: next.run(request).await }; if let Some(value) = available { - response - .headers_mut() - .insert(HeaderName::from_static(AVAILABLE_HEADER), value); + response.headers_mut().insert(HeaderName::from_static(AVAILABLE_HEADER), value); } response } @@ -87,19 +78,18 @@ where #[cfg(test)] mod tests { - use super::{CapabilitiesRouterExt, get}; use std::sync::Arc; - use crate::caps::CapServer; - use crate::caps::http::{AVAILABLE_HEADER, ENFORCE_HEADER, KNOWN_HEADER}; - use axum::{ - Router, - body::Body, - http::{HeaderValue, Request, StatusCode}, - routing::get as axum_get, - }; + use axum::Router; + use axum::body::Body; + use axum::http::{HeaderValue, Request, StatusCode}; + use axum::routing::get as axum_get; use rstest::{fixture, rstest}; - use tower::ServiceExt; // oneshot + use tower::ServiceExt; + + use super::{CapabilitiesRouterExt, get}; + use crate::caps::CapServer; + use crate::caps::http::{AVAILABLE_HEADER, ENFORCE_HEADER, KNOWN_HEADER}; // oneshot /// An empty capability set -- advertises nothing, but still issues a stable token. #[fixture] @@ -110,47 +100,29 @@ mod tests { #[rstest] #[tokio::test] async fn endpoint_serves_the_document(caps: Arc) { - let app: Router = Router::new() - .route("/api/v0/capabilities", axum_get(get)) - .with_state(caps.clone()); + let app: Router = + Router::new().route("/api/v0/capabilities", axum_get(get)).with_state(caps.clone()); let resp = app - .oneshot( - Request::builder() - .uri("/api/v0/capabilities") - .body(Body::empty()) - .unwrap(), - ) + .oneshot(Request::builder().uri("/api/v0/capabilities").body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get("content-type").unwrap(), - "application/json" - ); - let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX) - .await - .unwrap(); + assert_eq!(resp.headers().get("content-type").unwrap(), "application/json"); + let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); assert_eq!(bytes.as_ref(), caps.body().as_bytes()); } fn negotiating_app(caps: Arc) -> Router { - Router::new() - .route("/probe", axum_get(|| async { "ok" })) - .negotiate_capabilities(caps) + Router::new().route("/probe", axum_get(|| async { "ok" })).negotiate_capabilities(caps) } #[rstest] #[tokio::test] async fn absent_known_header_passes(caps: Arc) { let resp = negotiating_app(caps) - .oneshot( - Request::builder() - .uri("/probe") - .body(Body::empty()) - .unwrap(), - ) + .oneshot(Request::builder().uri("/probe").body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -206,14 +178,7 @@ mod tests { .await .unwrap(); assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED); - assert_eq!( - resp.headers() - .get(AVAILABLE_HEADER) - .unwrap() - .to_str() - .unwrap(), - caps.token() - ); + assert_eq!(resp.headers().get(AVAILABLE_HEADER).unwrap().to_str().unwrap(), caps.token()); } #[rstest] @@ -232,13 +197,6 @@ mod tests { // No enforce header -> the request is served despite the stale token... assert_eq!(resp.status(), StatusCode::OK); // ...but the server still advertises its token so the client can refresh. - assert_eq!( - resp.headers() - .get(AVAILABLE_HEADER) - .unwrap() - .to_str() - .unwrap(), - caps.token() - ); + assert_eq!(resp.headers().get(AVAILABLE_HEADER).unwrap().to_str().unwrap(), caps.token()); } } diff --git a/crates/atuin-domain/src/caps/client.rs b/crates/atuin-domain/src/caps/client.rs index a341d6018..f41855de3 100644 --- a/crates/atuin-domain/src/caps/client.rs +++ b/crates/atuin-domain/src/caps/client.rs @@ -1,11 +1,14 @@ -use super::{CapKey, Capability, CapsBundle, DuplicateCapability}; -use crate::api::CapabilitiesResponse; +use std::collections::HashMap; +use std::sync::Arc; + use parking_lot::RwLock; use serde::de::DeserializeOwned; -use std::{collections::HashMap, sync::Arc}; use tokio; use url::Url; +use super::{CapKey, Capability, CapsBundle, DuplicateCapability}; +use crate::api::CapabilitiesResponse; + /// Client-side capability set: advertises its own capabilities and can read the server's. /// /// The server's capabilities are populated by [`CapClient::refresh`], which the crate drives at @@ -131,22 +134,13 @@ impl CapClient { /// Whether our cached server token differs from `available` (or we have not fetched yet). fn is_stale(&self, available: &str) -> bool { - self.server - .read() - .as_ref() - .map(|caps| caps.version.as_str()) - != Some(available) + self.server.read().as_ref().map(|caps| caps.version.as_str()) != Some(available) } /// Fetch and decode the server's capabilities document. async fn fetch_server_caps(&self) -> reqwest::Result { - let resp: CapabilitiesResponse = self - .http - .get(self.capabilities_url.clone()) - .send() - .await? - .json() - .await?; + let resp: CapabilitiesResponse = + self.http.get(self.capabilities_url.clone()).send().await?.json().await?; Ok(ServerCaps::from(resp)) } @@ -170,12 +164,12 @@ impl CapClient { return Ok(None); }; - serde_json::from_value(raw.clone()) - .map(Some) - .map_err(|source| ServerSupportError::Malformed { + serde_json::from_value(raw.clone()).map(Some).map_err(|source| { + ServerSupportError::Malformed { name: C::static_name(), source, - }) + } + }) } /// Whether the server's capabilities have been fetched at least once. @@ -194,12 +188,13 @@ impl CapClient { #[cfg(test)] mod tests { - use super::*; - use crate::caps::{CapServer, CapabilitiesCap}; use rstest::{fixture, rstest}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; + use super::*; + use crate::caps::{CapServer, CapabilitiesCap}; + /// A plain reqwest client for the network tests. #[fixture] fn http_client() -> reqwest::Client { @@ -219,9 +214,7 @@ mod tests { .mount(&server) .await; - let caps_url: Url = format!("{}/api/v0/capabilities", server.uri()) - .parse() - .unwrap(); + let caps_url: Url = format!("{}/api/v0/capabilities", server.uri()).parse().unwrap(); let client = CapClient::new(caps_url, http_client); // Nothing fetched yet. @@ -237,9 +230,7 @@ mod tests { #[tokio::test] async fn client_observes_the_capability_the_server_advertises(http_client: reqwest::Client) { // Serve the exact wire body a real server would produce for the capabilities capability. - let advertised = CapServer::new() - .add(CapabilitiesCap { version: 1 }) - .unwrap(); + let advertised = CapServer::new().add(CapabilitiesCap { version: 1 }).unwrap(); let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/api/v0/capabilities")) @@ -247,9 +238,7 @@ mod tests { .mount(&server) .await; - let caps_url: Url = format!("{}/api/v0/capabilities", server.uri()) - .parse() - .unwrap(); + let caps_url: Url = format!("{}/api/v0/capabilities", server.uri()).parse().unwrap(); let client = CapClient::new(caps_url, http_client); assert_eq!( diff --git a/crates/atuin-domain/src/caps/middleware.rs b/crates/atuin-domain/src/caps/middleware.rs index 05c61c918..618ef937a 100644 --- a/crates/atuin-domain/src/caps/middleware.rs +++ b/crates/atuin-domain/src/caps/middleware.rs @@ -57,16 +57,13 @@ impl Middleware for CapMiddleware { // rather than panicking the request path. let known = self.caps.known_token(); if let Some(value) = known.as_deref().and_then(|t| HeaderValue::from_str(t).ok()) { - req.headers_mut() - .insert(HeaderName::from_static(KNOWN_HEADER), value); + req.headers_mut().insert(HeaderName::from_static(KNOWN_HEADER), value); } // In `Error` mode, ask the server to reject a stale token rather than serve the request. if self.on_mismatch == CapMismatch::Error { - req.headers_mut().insert( - HeaderName::from_static(ENFORCE_HEADER), - HeaderValue::from_static("1"), - ); + req.headers_mut() + .insert(HeaderName::from_static(ENFORCE_HEADER), HeaderValue::from_static("1")); } let response = next.run(req, ext).await?; @@ -112,22 +109,20 @@ impl CapabilitiesExt for Client { caps: Arc, on_mismatch: CapMismatch, ) -> ClientWithMiddleware { - let middleware = CapMiddleware::builder() - .caps(caps) - .on_mismatch(on_mismatch) - .build(); + let middleware = CapMiddleware::builder().caps(caps).on_mismatch(on_mismatch).build(); ClientBuilder::new(self).with(middleware).build() } } #[cfg(test)] mod tests { - use super::*; - use crate::caps::CapClient; use rstest::{fixture, rstest}; use wiremock::matchers::{header, header_exists, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; + use super::*; + use crate::caps::CapClient; + /// A plain reqwest client for the network tests. #[fixture] fn http_client() -> reqwest::Client { @@ -203,9 +198,7 @@ mod tests { } fn cap_client(server: &MockServer) -> Arc { - let caps_url = format!("{}/api/v0/capabilities", server.uri()) - .parse() - .unwrap(); + let caps_url = format!("{}/api/v0/capabilities", server.uri()).parse().unwrap(); CapClient::new(caps_url, reqwest::Client::new()) } @@ -219,11 +212,7 @@ mod tests { let client = http_client.with_capabilities(cap_client(&server), CapMismatch::Continue); // Our token is unknown -> stale, but the server serves the request anyway. - let response = client - .get(format!("{}/protected", server.uri())) - .send() - .await - .unwrap(); + let response = client.get(format!("{}/protected", server.uri())).send().await.unwrap(); assert_eq!(response.status(), 200); assert_eq!(response.text().await.unwrap(), "ok"); @@ -241,11 +230,7 @@ mod tests { let server = negotiating_server.await; let client = http_client.with_capabilities(cap_client(&server), CapMismatch::Error); - let response = client - .get(format!("{}/protected", server.uri())) - .send() - .await - .unwrap(); + let response = client.get(format!("{}/protected", server.uri())).send().await.unwrap(); assert_eq!(response.status(), 412); // The only caps fetch is the eager warm-up on construction; `Error` mode adds no refresh. @@ -262,9 +247,7 @@ mod tests { .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; - let caps_url = format!("{}/api/v0/capabilities", server.uri()) - .parse() - .unwrap(); + let caps_url = format!("{}/api/v0/capabilities", server.uri()).parse().unwrap(); let middleware = CapMiddleware::builder() .caps(CapClient::new(caps_url, http_client.clone())) @@ -272,11 +255,7 @@ mod tests { .build(); let client = ClientBuilder::new(http_client).with(middleware).build(); - let response = client - .get(format!("{}/missing", server.uri())) - .send() - .await - .unwrap(); + let response = client.get(format!("{}/missing", server.uri())).send().await.unwrap(); assert_eq!(response.status(), 404); // The eager warm-up is the only caps fetch; a plain 404 adds no further refresh. @@ -299,9 +278,7 @@ mod tests { .respond_with(ResponseTemplate::new(412)) .mount(&server) .await; - let caps_url = format!("{}/api/v0/capabilities", server.uri()) - .parse() - .unwrap(); + let caps_url = format!("{}/api/v0/capabilities", server.uri()).parse().unwrap(); let middleware = CapMiddleware::builder() .caps(CapClient::new(caps_url, http_client.clone())) @@ -309,11 +286,7 @@ mod tests { .build(); let client = ClientBuilder::new(http_client).with(middleware).build(); - let response = client - .get(format!("{}/precondition", server.uri())) - .send() - .await - .unwrap(); + let response = client.get(format!("{}/precondition", server.uri())).send().await.unwrap(); assert_eq!(response.status(), 412); // The eager warm-up is the only caps fetch; a 412 without the caps header adds no refresh. @@ -334,11 +307,7 @@ mod tests { let server = negotiating_server.await; let client = http_client.with_capabilities(cap_client(&server), CapMismatch::Continue); - let response = client - .get(format!("{}/protected", server.uri())) - .send() - .await - .unwrap(); + let response = client.get(format!("{}/protected", server.uri())).send().await.unwrap(); assert_eq!(response.status(), 200); assert_eq!(response.text().await.unwrap(), "ok"); @@ -357,9 +326,8 @@ mod tests { for _ in 0..20 { let client = client.clone(); let url = format!("{}/protected", server.uri()); - handles.push(tokio::spawn(async move { - client.get(url).send().await.unwrap().status() - })); + handles + .push(tokio::spawn(async move { client.get(url).send().await.unwrap().status() })); } for handle in handles { assert_eq!(handle.await.unwrap(), 200); diff --git a/crates/atuin-domain/src/caps/mod.rs b/crates/atuin-domain/src/caps/mod.rs index a51a56bfc..647fd8ae3 100644 --- a/crates/atuin-domain/src/caps/mod.rs +++ b/crates/atuin-domain/src/caps/mod.rs @@ -39,8 +39,13 @@ //! server only sends that header on a 412, so a stale client currently learns of a capability //! change on its next rejected request rather than preemptively from an earlier response. +use std::any::Any; +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::collections::BTreeSet; +use std::fmt; + use parking_lot::RwLock; -use std::{any::Any, borrow::Borrow, cmp::Ordering, collections::BTreeSet, fmt}; pub mod http; @@ -189,10 +194,7 @@ impl CapsBundle { .read() .iter() .map(|entry| { - let value = entry - .0 - .json() - .expect("a capability value must be JSON-serializable"); + let value = entry.0.json().expect("a capability value must be JSON-serializable"); (entry.name().to_string(), value) }) .collect(); @@ -203,8 +205,6 @@ impl CapsBundle { impl fmt::Debug for CapsBundle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // `dyn Any` is not `Debug`; show which capabilities are present, not their contents. - f.debug_set() - .entries(self.caps.read().iter().map(CapEntry::name)) - .finish() + f.debug_set().entries(self.caps.read().iter().map(CapEntry::name)).finish() } } diff --git a/crates/atuin-domain/src/caps/server.rs b/crates/atuin-domain/src/caps/server.rs index 972dfe28d..3aff63f53 100644 --- a/crates/atuin-domain/src/caps/server.rs +++ b/crates/atuin-domain/src/caps/server.rs @@ -124,11 +124,12 @@ impl Default for CapServer { #[cfg(test)] mod tests { + use rstest::{fixture, rstest}; + use serde::{Deserialize, Serialize}; + use super::*; use crate::api::CapabilitiesResponse; use crate::caps::{CapabilitiesCap, Capability}; - use rstest::{fixture, rstest}; - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] struct TestCap { @@ -182,11 +183,7 @@ mod tests { fn token_is_16_char_lowercase_hex(empty: CapServer) { let token = empty.token(); assert_eq!(token.len(), 16); - assert!( - token - .chars() - .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()) - ); + assert!(token.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())); } #[rstest] @@ -213,10 +210,7 @@ mod tests { let caps = CapServer::new().add(TestCap { n }).unwrap(); let resp: CapabilitiesResponse = serde_json::from_str(caps.body()).unwrap(); assert_eq!(resp.version, caps.token()); - assert_eq!( - resp.capabilities.get("test/cap"), - Some(&serde_json::json!({ "n": n })) - ); + assert_eq!(resp.capabilities.get("test/cap"), Some(&serde_json::json!({ "n": n }))); } #[rstest] @@ -230,11 +224,8 @@ mod tests { assert_eq!(many.caps().get::().unwrap().m, 2); // A batch bakes to the same set -- and therefore the same token -- as chained `add`s. - let chained = CapServer::new() - .add(TestCap { n: 1 }) - .unwrap() - .add(OtherCap { m: 2 }) - .unwrap(); + let chained = + CapServer::new().add(TestCap { n: 1 }).unwrap().add(OtherCap { m: 2 }).unwrap(); assert_eq!(many.token(), chained.token()); assert_eq!(many.body(), chained.body()); @@ -252,11 +243,8 @@ mod tests { #[rstest] fn add_rejects_a_duplicate_name() { - let err = CapServer::new() - .add(TestCap { n: 1 }) - .unwrap() - .add(TestCap { n: 2 }) - .unwrap_err(); + let err = + CapServer::new().add(TestCap { n: 1 }).unwrap().add(TestCap { n: 2 }).unwrap_err(); assert_eq!(err.name, "test/cap"); } diff --git a/crates/atuin-domain/src/record/mod.rs b/crates/atuin-domain/src/record/mod.rs index 8948f04bf..99ba3377b 100644 --- a/crates/atuin-domain/src/record/mod.rs +++ b/crates/atuin-domain/src/record/mod.rs @@ -277,16 +277,14 @@ impl RecordStatus { #[cfg(test)] mod tests { - use super::*; use atuin_common::encryption::paseto_v4; use atuin_common::utils::uuid_v7; - - use super::{DecryptedData, Diff, Record, RecordStatus}; + use paseto_v4::ImplicitAssertion; use pretty_assertions::assert_eq; use rstest::{fixture, rstest}; use uuid::Uuid; - use paseto_v4::ImplicitAssertion; + use super::{DecryptedData, Diff, Record, RecordStatus, *}; /// Serialize `AdditionalData` into the JSON used as the paseto implicit assertion. /// @@ -362,9 +360,7 @@ mod tests { }, }; - let decrypted = record - .decrypt(&key) - .expect("frozen record blob must still decrypt"); + let decrypted = record.decrypt(&key).expect("frozen record blob must still decrypt"); assert_eq!(decrypted.data.0, [1, 2, 3, 4, 5]); } @@ -376,11 +372,7 @@ mod tests { let tail = index.get(record.host.id, &record.tag); - assert_eq!( - record.idx, - tail.expect("tail not in store"), - "tail in store did not match" - ); + assert_eq!(record.idx, tail.expect("tail not in store"), "tail in store did not match"); } #[rstest] @@ -393,11 +385,7 @@ mod tests { let tail = index.get(record.host.id, &record.tag); - assert_eq!( - child.idx, - tail.expect("tail not in store"), - "tail in store did not match" - ); + assert_eq!(child.idx, tail.expect("tail not in store"), "tail in store did not match"); } #[rstest] @@ -430,15 +418,12 @@ mod tests { let diff = index1.diff(&index2); assert_eq!(1, diff.len(), "expected single diff"); - assert_eq!( - diff[0], - Diff { - host: record2.host.id, - tag: record2.tag, - remote: Some(1), - local: Some(0) - } - ); + assert_eq!(diff[0], Diff { + host: record2.host.id, + tag: record2.tag, + remote: Some(1), + local: Some(0) + }); } #[rstest] @@ -571,10 +556,7 @@ mod tests { .unwrap_err(); let message = error.to_string(); - assert!( - message.contains("bad key"), - "unexpected error message: {message}" - ); + assert!(message.contains("bad key"), "unexpected error message: {message}"); assert!( message.contains(&format!( "encrypted key id: {}, given decryption key: {}", @@ -600,9 +582,7 @@ mod tests { paseto_v4::decrypt_sync(&encrypted, None::, &key).unwrap_err(); assert!( - error - .to_string() - .contains("failed to deserialize the given key"), + error.to_string().contains("failed to deserialize the given key"), "unexpected error message: {error}" ); } @@ -665,12 +645,9 @@ mod tests { assert_eq!(encrypted1.raw, encrypted2.raw); assert_ne!(encrypted1.cek, encrypted2.cek); - let decrypted = paseto_v4::decrypt_sync( - &encrypted2, - Some(ImplicitAssertion::from(aj.as_str())), - &key2, - ) - .unwrap(); + let decrypted = + paseto_v4::decrypt_sync(&encrypted2, Some(ImplicitAssertion::from(aj.as_str())), &key2) + .unwrap(); assert_eq!(DecryptedData(decrypted), data); } @@ -695,15 +672,13 @@ mod tests { let mut enc1 = encrypted.clone(); enc1.host = Host::new(HostId(uuid_v7())); - let _ = enc1 - .decrypt(&key) - .expect_err("tampering with the host should result in auth failure"); + let _ = + enc1.decrypt(&key).expect_err("tampering with the host should result in auth failure"); let mut enc2 = encrypted; enc2.id = RecordId(uuid_v7()); - let _ = enc2 - .decrypt(&key) - .expect_err("tampering with the id should result in auth failure"); + let _ = + enc2.decrypt(&key).expect_err("tampering with the id should result in auth failure"); } #[test] @@ -720,10 +695,7 @@ mod tests { let round: RecordStatus = serde_json::from_str(&json).unwrap(); assert_eq!(round.get(host, &RecordTag::History), Some(6)); - assert_eq!( - round.get(host, &RecordTag::Other("custom".to_owned())), - Some(2) - ); + assert_eq!(round.get(host, &RecordTag::Other("custom".to_owned())), Some(2)); } /// Do *not* modify this test if it fails! It means the serialization of [`AdditionalData`] diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs index 0d2192f01..5fbe03a34 100644 --- a/crates/atuin-dotfiles/src/shell.rs +++ b/crates/atuin-dotfiles/src/shell.rs @@ -1,9 +1,8 @@ +use atuin_common::shell::{Shell, ShellError}; use eyre::{Result, ensure, eyre}; use rmp::{decode, encode}; use serde::Serialize; -use atuin_common::shell::{Shell, ShellError}; - use crate::store::AliasStore; pub mod bash; @@ -87,7 +86,13 @@ pub fn parse_alias(line: &str) -> Option { let parts: Vec<&str> = if is_fish { line.split(' ') .enumerate() - .filter_map(|(n, i)| if n == 0 { None } else { Some(i) }) + .filter_map(|(n, i)| { + if n == 0 { + None + } else { + Some(i) + } + }) .collect() } else { line.split('=').collect() @@ -165,15 +170,13 @@ mod tests { #[rstest] #[case::simple("foo=bar", "foo", "bar")] - #[case::quoted( - "emacs='TERM=xterm-24bits emacs -nw'", - "emacs", - "'TERM=xterm-24bits emacs -nw'" - )] + #[case::quoted("emacs='TERM=xterm-24bits emacs -nw'", "emacs", "'TERM=xterm-24bits emacs -nw'")] #[case::quoted_git( - "gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'", + "gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify \ + --no-gpg-sign --message \"--wip-- [skip ci]\"'", "gwip", - "'git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'" + "'git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify \ + --no-gpg-sign --message \"--wip-- [skip ci]\"'" )] #[case::quoted_equals( "emacs='TERM=xterm-24bits emacs -nw --foo=bar'", diff --git a/crates/atuin-dotfiles/src/shell/bash.rs b/crates/atuin-dotfiles/src/shell/bash.rs index 2b9b4c885..e05930e31 100644 --- a/crates/atuin-dotfiles/src/shell/bash.rs +++ b/crates/atuin-dotfiles/src/shell/bash.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; -use crate::store::{AliasStore, var::VarStore}; +use crate::store::AliasStore; +use crate::store::var::VarStore; async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { match tokio::fs::read_to_string(path).await { diff --git a/crates/atuin-dotfiles/src/shell/fish.rs b/crates/atuin-dotfiles/src/shell/fish.rs index 6d472f677..214972bd5 100644 --- a/crates/atuin-dotfiles/src/shell/fish.rs +++ b/crates/atuin-dotfiles/src/shell/fish.rs @@ -1,7 +1,8 @@ // Configuration for fish use std::path::PathBuf; -use crate::store::{AliasStore, var::VarStore}; +use crate::store::AliasStore; +use crate::store::var::VarStore; async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { match tokio::fs::read_to_string(path).await { diff --git a/crates/atuin-dotfiles/src/shell/powershell.rs b/crates/atuin-dotfiles/src/shell/powershell.rs index bb6c1f9e8..f3a083a5c 100644 --- a/crates/atuin-dotfiles/src/shell/powershell.rs +++ b/crates/atuin-dotfiles/src/shell/powershell.rs @@ -1,7 +1,9 @@ -use crate::shell::{Alias, Var}; -use crate::store::{AliasStore, var::VarStore}; use std::path::PathBuf; +use crate::shell::{Alias, Var}; +use crate::store::AliasStore; +use crate::store::var::VarStore; + async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { match tokio::fs::read_to_string(path).await { Ok(aliases) => aliases, @@ -90,7 +92,11 @@ pub fn format_alias(alias: &Alias) -> String { pub fn format_var(var: &Var) -> String { secure_command(&format!( "${}{} = '{}'", - if var.export { "env:" } else { "" }, + if var.export { + "env:" + } else { + "" + }, var.name, var.value.replace("'", "''") )) @@ -99,17 +105,15 @@ pub fn format_var(var: &Var) -> String { /// Wraps the given command in an Invoke-Expression to ensure the outer script is not halted /// if the inner command contains a syntax error. fn secure_command(command: &str) -> String { - format!( - "Invoke-Expression -ErrorAction Continue -Command '{}'\n", - command.replace("'", "''") - ) + format!("Invoke-Expression -ErrorAction Continue -Command '{}'\n", command.replace("'", "''")) } #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::simple("gp", "git push", "function gp {\n git push @args\n}")] #[case::quoted_path( diff --git a/crates/atuin-dotfiles/src/shell/xonsh.rs b/crates/atuin-dotfiles/src/shell/xonsh.rs index 1e56fc1d9..8be556dff 100644 --- a/crates/atuin-dotfiles/src/shell/xonsh.rs +++ b/crates/atuin-dotfiles/src/shell/xonsh.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; -use crate::store::{AliasStore, var::VarStore}; +use crate::store::AliasStore; +use crate::store::var::VarStore; async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { match tokio::fs::read_to_string(path).await { diff --git a/crates/atuin-dotfiles/src/shell/zsh.rs b/crates/atuin-dotfiles/src/shell/zsh.rs index 117e94031..170fd0b0f 100644 --- a/crates/atuin-dotfiles/src/shell/zsh.rs +++ b/crates/atuin-dotfiles/src/shell/zsh.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; -use crate::store::{AliasStore, var::VarStore}; +use crate::store::AliasStore; +use crate::store::var::VarStore; async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String { match tokio::fs::read_to_string(path).await { diff --git a/crates/atuin-dotfiles/src/store.rs b/crates/atuin-dotfiles/src/store.rs index 35eac9f4e..7374891fe 100644 --- a/crates/atuin-dotfiles/src/store.rs +++ b/crates/atuin-dotfiles/src/store.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use atuin_client::record::sqlite_store::SqliteStore; +use atuin_common::encryption::paseto_v4; // Sync aliases // This will be noticeable similar to the kv store, though I expect the two shall diverge // While we will support a range of shell config, I'd rather have a larger number of small records @@ -9,8 +10,6 @@ use atuin_common::utils::unquote; use atuin_domain::record::{DecryptedData, Host, HostId, RecordTag, RecordVersion}; use eyre::{Result, bail, ensure, eyre}; -use atuin_common::encryption::paseto_v4; - use crate::shell::Alias; const CONFIG_SHELL_ALIAS_FIELD_MAX_LEN: usize = 20000; // 20kb max total len, way more than should be needed. @@ -66,10 +65,7 @@ impl AliasRecord { // create 0 => { let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?; - ensure!( - nfields == 2, - "too many entries in v0 shell alias create record" - ); + ensure!(nfields == 2, "too many entries in v0 shell alias create record"); let bytes = bytes.remaining_slice(); @@ -91,10 +87,7 @@ impl AliasRecord { // delete 1 => { let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?; - ensure!( - nfields == 1, - "too many entries in v0 shell alias delete record" - ); + ensure!(nfields == 1, "too many entries in v0 shell alias delete record"); let bytes = bytes.remaining_slice(); @@ -244,9 +237,7 @@ impl AliasStore { .data(bytes) .build(); - self.store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.store.push(&record.encrypt(&self.encryption_key)).await?; // set mutates shell config, so build again self.build().await?; @@ -280,9 +271,7 @@ impl AliasStore { .data(bytes) .build(); - self.store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.store.push(&record.encrypt(&self.encryption_key)).await?; // delete mutates shell config, so build again self.build().await?; @@ -350,22 +339,18 @@ pub(crate) fn test_local_timeout() -> f64 { #[cfg(test)] mod tests { + use atuin_client::record::sqlite_store::SqliteStore; + use atuin_domain::record::{RecordTag, RecordVersion}; + use crypto_secretbox::{KeyInit, XSalsa20Poly1305}; use rand::rngs::OsRng; use rstest::*; - use atuin_client::record::sqlite_store::SqliteStore; - - use crate::shell::Alias; - use super::{AliasRecord, AliasStore, test_local_timeout}; - use atuin_domain::record::{RecordTag, RecordVersion}; - use crypto_secretbox::{KeyInit, XSalsa20Poly1305}; + use crate::shell::Alias; #[fixture] async fn alias_store() -> (AliasStore, SqliteStore) { - let store = SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap(); + let store = SqliteStore::new(":memory:", test_local_timeout()).await.unwrap(); let key: [u8; 32] = XSalsa20Poly1305::generate_key(&mut OsRng).into(); let host_id = atuin_domain::record::HostId(atuin_common::utils::uuid_v7()); @@ -396,10 +381,7 @@ mod tests { alias.set("k", "kubectl").await.unwrap(); alias.set("gp", "git push").await.unwrap(); - alias - .set("kgap", "'kubectl get pods --all-namespaces'") - .await - .unwrap(); + alias.set("kgap", "'kubectl get pods --all-namespaces'").await.unwrap(); let mut aliases = alias.aliases().await.unwrap(); @@ -407,29 +389,20 @@ mod tests { assert_eq!(aliases.len(), 3); - assert_eq!( - aliases[0], - Alias { - name: String::from("gp"), - value: String::from("git push") - } - ); + assert_eq!(aliases[0], Alias { + name: String::from("gp"), + value: String::from("git push") + }); - assert_eq!( - aliases[1], - Alias { - name: String::from("k"), - value: String::from("kubectl") - } - ); + assert_eq!(aliases[1], Alias { + name: String::from("k"), + value: String::from("kubectl") + }); - assert_eq!( - aliases[2], - Alias { - name: String::from("kgap"), - value: String::from("'kubectl get pods --all-namespaces'") - } - ); + assert_eq!(aliases[2], Alias { + name: String::from("kgap"), + value: String::from("'kubectl get pods --all-namespaces'") + }); let build = alias.posix().await.expect("failed to build aliases"); @@ -462,20 +435,14 @@ alias kgap='kubectl get pods --all-namespaces' .data(DecryptedData(vec![1, 2, 3])) .build(); - store - .push(&corrupt.encrypt(&corrupt_key.into())) - .await - .unwrap(); + store.push(&corrupt.encrypt(&corrupt_key.into())).await.unwrap(); let aliases = alias.aliases().await.unwrap(); assert_eq!(aliases.len(), 1); - assert_eq!( - aliases[0], - Alias { - name: String::from("k"), - value: String::from("kubectl") - } - ); + assert_eq!(aliases[0], Alias { + name: String::from("k"), + value: String::from("kubectl") + }); } } diff --git a/crates/atuin-dotfiles/src/store/var.rs b/crates/atuin-dotfiles/src/store/var.rs index d8132b934..fe96b69c6 100644 --- a/crates/atuin-dotfiles/src/store/var.rs +++ b/crates/atuin-dotfiles/src/store/var.rs @@ -5,11 +5,10 @@ use std::collections::BTreeMap; use atuin_client::record::sqlite_store::SqliteStore; +use atuin_common::encryption::paseto_v4; use atuin_domain::record::{DecryptedData, Host, HostId, RecordTag, RecordVersion}; use eyre::{Result, bail, ensure, eyre}; -use atuin_common::encryption::paseto_v4; - use crate::shell::Var; const DOTFILES_VAR_LEN: usize = 20000; // 20kb max total len, way more than should be needed. @@ -66,10 +65,7 @@ impl VarRecord { // delete 1 => { let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?; - ensure!( - nfields == 1, - "too many entries in v0 dotfiles var delete record" - ); + ensure!(nfields == 1, "too many entries in v0 dotfiles var delete record"); let bytes = bytes.remaining_slice(); @@ -263,10 +259,7 @@ impl VarStore { pub async fn set(&self, name: &str, value: &str, export: bool) -> Result<()> { if name.len() + value.len() > DOTFILES_VAR_LEN { - return Err(eyre!( - "var record too large: max len {} bytes", - DOTFILES_VAR_LEN - )); + return Err(eyre!("var record too large: max len {} bytes", DOTFILES_VAR_LEN)); } let record = VarRecord::Create(Var { @@ -291,9 +284,7 @@ impl VarStore { .data(bytes) .build(); - self.store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.store.push(&record.encrypt(&self.encryption_key)).await?; // set mutates shell config, so build again self.build().await?; @@ -303,10 +294,7 @@ impl VarStore { pub async fn delete(&self, name: &str) -> Result<()> { if name.len() > DOTFILES_VAR_LEN { - return Err(eyre!( - "var record too large: max len {} bytes", - DOTFILES_VAR_LEN, - )); + return Err(eyre!("var record too large: max len {} bytes", DOTFILES_VAR_LEN,)); } let record = VarRecord::Delete(name.to_string()); @@ -327,9 +315,7 @@ impl VarStore { .data(bytes) .build(); - self.store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.store.push(&record.encrypt(&self.encryption_key)).await?; // delete mutates shell config, so build again self.build().await?; @@ -385,22 +371,19 @@ impl VarStore { #[cfg(test)] mod tests { - use rand::rngs::OsRng; - use atuin_client::record::sqlite_store::SqliteStore; - - use crate::{shell::Var, store::test_local_timeout}; - - use super::{VarRecord, VarStore}; use atuin_domain::record::RecordVersion; use crypto_secretbox::{KeyInit, XSalsa20Poly1305}; + use rand::rngs::OsRng; use rstest::*; + use super::{VarRecord, VarStore}; + use crate::shell::Var; + use crate::store::test_local_timeout; + #[fixture] async fn var_store() -> VarStore { - let store = SqliteStore::new(":memory:", test_local_timeout()) - .await - .unwrap(); + let store = SqliteStore::new(":memory:", test_local_timeout()).await.unwrap(); let key: [u8; 32] = XSalsa20Poly1305::generate_key(&mut OsRng).into(); let host_id = atuin_domain::record::HostId(atuin_common::utils::uuid_v7()); @@ -416,9 +399,7 @@ mod tests { }; let record = VarRecord::Create(record); - let snapshot = [ - 204, 0, 147, 164, 66, 69, 69, 80, 164, 98, 111, 111, 112, 194, - ]; + let snapshot = [204, 0, 147, 164, 66, 69, 69, 80, 164, 98, 111, 111, 112, 194]; let encoded = record.serialize().unwrap(); let decoded = VarRecord::deserialize(&encoded, &RecordVersion::V0).unwrap(); @@ -485,23 +466,17 @@ mod tests { assert_eq!(env_vars.len(), 2); - assert_eq!( - env_vars[0], - Var { - name: String::from("BEEP"), - value: String::from("boop"), - export: false, - } - ); + assert_eq!(env_vars[0], Var { + name: String::from("BEEP"), + value: String::from("boop"), + export: false, + }); - assert_eq!( - env_vars[1], - Var { - name: String::from("HOMEBREW_NO_AUTO_UPDATE"), - value: String::from("1"), - export: true, - } - ); + assert_eq!(env_vars[1], Var { + name: String::from("HOMEBREW_NO_AUTO_UPDATE"), + value: String::from("1"), + export: true, + }); } #[rstest] diff --git a/crates/atuin-history/benches/smart_sort.rs b/crates/atuin-history/benches/smart_sort.rs index 24656ab48..248d0e247 100644 --- a/crates/atuin-history/benches/smart_sort.rs +++ b/crates/atuin-history/benches/smart_sort.rs @@ -1,9 +1,7 @@ use atuin_client::history::History; use atuin_history::sort::sort; - -use rand::Rng; -use rand::SeedableRng; use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; use time::OffsetDateTime; use time::macros::datetime; @@ -20,9 +18,7 @@ const SEED_NOW: OffsetDateTime = datetime!(2026-01-01 12:59:59 -5); #[divan::bench(args=[100, 200, 400, 800, 1600, 10000])] fn smart_sort(bencher: divan::Bencher, lines: usize) { // Generating the history is not part of what we measure, so it happens as an input. - bencher - .with_inputs(|| history(lines)) - .bench_values(|commands| sort("curl", commands)); + bencher.with_inputs(|| history(lines)).bench_values(|commands| sort("curl", commands)); } /// Generate a few different sizes of "history". This will use a whole bunch of memory, sorry. diff --git a/crates/atuin-history/src/stats.rs b/crates/atuin-history/src/stats.rs index f0cd673d5..3f077aa8f 100644 --- a/crates/atuin-history/src/stats.rs +++ b/crates/atuin-history/src/stats.rs @@ -1,11 +1,12 @@ use std::collections::{HashMap, HashSet}; +use atuin_client::history::History; +use atuin_client::settings::Settings; +use atuin_client::theme::{Meaning, Theme}; use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor}; use serde::{Deserialize, Serialize}; use unicode_segmentation::UnicodeSegmentation; -use atuin_client::{history::History, settings::Settings, theme::Meaning, theme::Theme}; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Stats { pub total_commands: usize, @@ -172,10 +173,7 @@ pub fn pretty_print(stats: Stats, ngram_size: usize, theme: &Theme) { .iter() .map(|(commands, _)| commands.iter().map(|c| c.len()).collect::>()) .fold(vec![0; ngram_size], |acc, item| { - acc.iter() - .zip(item.iter()) - .map(|(a, i)| *std::cmp::max(a, i)) - .collect() + acc.iter().zip(item.iter()).map(|(a, i)| *std::cmp::max(a, i)).collect() }); for (command, count) in stats.top { @@ -232,7 +230,8 @@ pub fn pretty_print(stats: Stats, ngram_size: usize, theme: &Theme) { .join(" | "); println!( - "{ResetColor}] {gray}{count:num_pad$}{ResetColor} {bold}{formatted_command}{ResetColor}" + "{ResetColor}] {gray}{count:num_pad$}{ResetColor} \ + {bold}{formatted_command}{ResetColor}" ); } println!("Total commands: {}", stats.total_commands); @@ -304,8 +303,7 @@ mod tests { use rstest::*; use time::OffsetDateTime; - use super::compute; - use super::{interesting_command, split_at_pipe, strip_leading_env_vars}; + use super::{compute, interesting_command, split_at_pipe, strip_leading_env_vars}; #[test] fn ignored_env_vars() { @@ -328,11 +326,7 @@ mod tests { settings.stats.ignored_commands.push("cd".to_string()); let history = [ - History::import() - .timestamp(OffsetDateTime::now_utc()) - .command("cd foo") - .build() - .into(), + History::import().timestamp(OffsetDateTime::now_utc()).command("cd foo").build().into(), History::import() .timestamp(OffsetDateTime::now_utc()) .command("cargo build stuff") @@ -351,12 +345,8 @@ mod tests { #[default(&[][..])] subcommand: &[&str], ) -> Settings { let mut s = Settings::utc(); - s.stats - .common_prefix - .extend(prefix.iter().map(|p| p.to_string())); - s.stats - .common_subcommands - .extend(subcommand.iter().map(|p| p.to_string())); + s.stats.common_prefix.extend(prefix.iter().map(|p| p.to_string())); + s.stats.common_subcommands.extend(subcommand.iter().map(|p| p.to_string())); s } diff --git a/crates/atuin-kv/src/database.rs b/crates/atuin-kv/src/database.rs index d0ed253f2..64cc3a925 100644 --- a/crates/atuin-kv/src/database.rs +++ b/crates/atuin-kv/src/database.rs @@ -1,13 +1,13 @@ -use std::{path::Path, str::FromStr, time::Duration}; +use std::path::Path; +use std::str::FromStr; +use std::time::Duration; use atuin_common::utils; -use sqlx::{ - Result, Row, - sqlite::{ - SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, - SqliteSynchronous, - }, +use sqlx::sqlite::{ + SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, + SqliteSynchronous, }; +use sqlx::{Result, Row}; use tokio::fs; use tracing::debug; @@ -25,7 +25,8 @@ impl Database { if utils::broken_symlink(path) { eprintln!( - "Atuin: KV sqlite db path ({path:?}) is a broken symlink. Unable to read or create replacement." + "Atuin: KV sqlite db path ({path:?}) is a broken symlink. Unable to read or \ + create replacement." ); std::process::exit(1); } @@ -56,9 +57,7 @@ impl Database { } pub async fn sqlite_version(&self) -> Result { - sqlx::query_scalar("SELECT sqlite_version()") - .fetch_one(&self.pool) - .await + sqlx::query_scalar("SELECT sqlite_version()").fetch_one(&self.pool).await } async fn setup_db(pool: &SqlitePool) -> Result<()> { @@ -124,11 +123,7 @@ impl Database { let key = row.get("key"); let value = row.get("value"); - KvEntry::builder() - .namespace(namespace) - .key(key) - .value(value) - .build() + KvEntry::builder().namespace(namespace).key(key).value(value).build() } pub async fn load(&self, namespace: &str, key: &str) -> Result> { @@ -166,9 +161,10 @@ impl Database { #[cfg(test)] mod test { - use super::*; use rstest::*; + use super::*; + #[fixture] async fn db() -> Database { Database::new("sqlite::memory:", 1.0).await.unwrap() @@ -207,11 +203,7 @@ mod test { db.save(&entry).await.unwrap(); - let loaded = db - .load(&entry.namespace, &entry.key) - .await - .unwrap() - .unwrap(); + let loaded = db.load(&entry.namespace, &entry.key).await.unwrap().unwrap(); assert_eq!(loaded, entry); } diff --git a/crates/atuin-kv/src/store.rs b/crates/atuin-kv/src/store.rs index 35de7edd0..0229764f0 100644 --- a/crates/atuin-kv/src/store.rs +++ b/crates/atuin-kv/src/store.rs @@ -1,11 +1,10 @@ use std::collections::HashSet; -use eyre::{Result, eyre}; - use atuin_client::record::sqlite_store::SqliteStore; use atuin_common::encryption::paseto_v4; use atuin_domain::record::{Host, HostId, Record, RecordId, RecordIdx, RecordTag, RecordVersion}; use entry::KvEntry; +use eyre::{Result, eyre}; use record::KvRecord; use crate::database::Database; @@ -84,11 +83,8 @@ impl KvStore { async fn push_record(&self, record: KvRecord) -> Result<(RecordId, RecordIdx)> { let bytes = record.serialize()?; - let idx = self - .record_store - .last(self.host_id, &RecordTag::Kv) - .await? - .map_or(0, |p| p.idx + 1); + let idx = + self.record_store.last(self.host_id, &RecordTag::Kv).await?.map_or(0, |p| p.idx + 1); let record = Record::builder() .host(Host::new(self.host_id)) @@ -100,9 +96,7 @@ impl KvStore { let id = record.id; - self.record_store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.record_store.push(&record.encrypt(&self.encryption_key)).await?; Ok((id, idx)) } @@ -154,9 +148,7 @@ impl KvStore { .await?; } None => { - self.kv_db - .delete(kv.namespace.as_str(), kv.key.as_str()) - .await?; + self.kv_db.delete(kv.namespace.as_str(), kv.key.as_str()).await?; } } } @@ -167,9 +159,7 @@ impl KvStore { // but just in case because ** S O F T W A R E ** for kv in cached { if !visited.contains(&format!("{}.{}", kv.namespace, kv.key)) { - self.kv_db - .delete(kv.namespace.as_str(), kv.key.as_str()) - .await?; + self.kv_db.delete(kv.namespace.as_str(), kv.key.as_str()).await?; } } @@ -184,9 +174,10 @@ impl KvStore { #[cfg(test)] mod tests { - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[fixture] async fn store() -> KvStore { let record_store = SqliteStore::new("sqlite::memory:", 1.0).await.unwrap(); diff --git a/crates/atuin-kv/src/store/record.rs b/crates/atuin-kv/src/store/record.rs index d482b58af..371f48fba 100644 --- a/crates/atuin-kv/src/store/record.rs +++ b/crates/atuin-kv/src/store/record.rs @@ -102,10 +102,11 @@ impl KvRecord { #[cfg(test)] mod tests { - use super::{DecryptedData, KvRecord}; use atuin_domain::record::RecordVersion; use rstest::rstest; + use super::{DecryptedData, KvRecord}; + #[rstest] #[case::some( Some("baz".to_owned()), @@ -134,9 +135,8 @@ mod tests { value: Some("baz".to_owned()), }; - let snapshot = vec![ - 0x93, 0xa3, b'f', b'o', b'o', 0xa3, b'b', b'a', b'r', 0xa3, b'b', b'a', b'z', - ]; + let snapshot = + vec![0x93, 0xa3, b'f', b'o', b'o', 0xa3, b'b', b'a', b'r', 0xa3, b'b', b'a', b'z']; let decoded = KvRecord::deserialize(&DecryptedData(snapshot), &RecordVersion::V0).unwrap(); diff --git a/crates/atuin-lab-share/src/crypto.rs b/crates/atuin-lab-share/src/crypto.rs index b04da9c88..948abce04 100644 --- a/crates/atuin-lab-share/src/crypto.rs +++ b/crates/atuin-lab-share/src/crypto.rs @@ -155,9 +155,7 @@ impl SessionKey { /// [`CryptoError::InvalidKey`] if the fragment is not unpadded base64url /// or does not decode to exactly [`KEY_LEN`] bytes. pub fn from_fragment(fragment: &str) -> Result { - let mut decoded = URL_SAFE_NO_PAD - .decode(fragment) - .map_err(|_| CryptoError::InvalidKey)?; + let mut decoded = URL_SAFE_NO_PAD.decode(fragment).map_err(|_| CryptoError::InvalidKey)?; if decoded.len() != KEY_LEN { decoded.zeroize(); return Err(CryptoError::InvalidKey); @@ -189,13 +187,10 @@ impl SessionKey { } let (nonce, ciphertext) = blob.split_at(NONCE_LEN); self.cipher() - .decrypt( - Nonce::from_slice(nonce), - Payload { - msg: ciphertext, - aad, - }, - ) + .decrypt(Nonce::from_slice(nonce), Payload { + msg: ciphertext, + aad, + }) .map_err(|_| CryptoError::DecryptFailed) } @@ -205,13 +200,10 @@ impl SessionKey { fn seal(&self, nonce: &[u8; NONCE_LEN], plaintext: &[u8], aad: &[u8; AAD_LEN]) -> Vec { let sealed = self .cipher() - .encrypt( - Nonce::from_slice(nonce), - Payload { - msg: plaintext, - aad, - }, - ) + .encrypt(Nonce::from_slice(nonce), Payload { + msg: plaintext, + aad, + }) .expect("AES-GCM encryption is infallible for in-memory frame sizes"); let mut blob = Vec::with_capacity(NONCE_LEN + sealed.len()); blob.extend_from_slice(nonce); @@ -253,10 +245,8 @@ mod tests { const VECTOR_CT_TAG_HEX: &str = "83cddb4a4811f46bacb67216f20a673e0ba9227cc3507c5757312b86e37c9fd6929b4ef72f0194e13a1674ed8686300ed4"; fn vector_key() -> SessionKey { - let bytes: [u8; KEY_LEN] = hex::decode(VECTOR_KEY_HEX) - .expect("valid hex") - .try_into() - .expect("32 bytes"); + let bytes: [u8; KEY_LEN] = + hex::decode(VECTOR_KEY_HEX).expect("valid hex").try_into().expect("32 bytes"); SessionKey::from_bytes(bytes) } @@ -266,10 +256,8 @@ mod tests { #[test] fn frozen_vector_encrypts_to_the_exact_bytes() { - let nonce: [u8; NONCE_LEN] = hex::decode(VECTOR_NONCE_HEX) - .expect("valid hex") - .try_into() - .expect("12 bytes"); + let nonce: [u8; NONCE_LEN] = + hex::decode(VECTOR_NONCE_HEX).expect("valid hex").try_into().expect("12 bytes"); let aad = frame_aad(FrameKind::Output, 42); assert_eq!(hex::encode(aad), "01000000000000002a"); @@ -306,10 +294,7 @@ mod tests { let aad = frame_aad(kind, 7); let blob = key.encrypt(b"round trip", &aad); assert_eq!(blob.len(), b"round trip".len() + NONCE_LEN + TAG_LEN); - assert_eq!( - key.decrypt(&blob, &aad).expect("round-trips"), - b"round trip" - ); + assert_eq!(key.decrypt(&blob, &aad).expect("round-trips"), b"round trip"); } // Empty plaintext is legal: the blob is exactly the 28-byte overhead. let aad = frame_aad(FrameKind::Output, 8); @@ -348,9 +333,7 @@ mod tests { let fragment = key.to_fragment(); assert_eq!(fragment.len(), 43); assert!( - fragment - .bytes() - .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'), + fragment.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'), "fragment must match ^[A-Za-z0-9_-]{{43}}$, got {fragment:?}" ); @@ -365,23 +348,14 @@ mod tests { #[test] fn from_fragment_rejects_wrong_lengths_and_padded_input() { // 42 chars decode to 31 bytes, 44 to 33 — both are not a key. - assert!(matches!( - SessionKey::from_fragment(&"A".repeat(42)), - Err(CryptoError::InvalidKey) - )); - assert!(matches!( - SessionKey::from_fragment(&"A".repeat(44)), - Err(CryptoError::InvalidKey) - )); + assert!(matches!(SessionKey::from_fragment(&"A".repeat(42)), Err(CryptoError::InvalidKey))); + assert!(matches!(SessionKey::from_fragment(&"A".repeat(44)), Err(CryptoError::InvalidKey))); // `=` is not in the no-pad alphabet at all. assert!(matches!( SessionKey::from_fragment(&format!("{}=", "A".repeat(43))), Err(CryptoError::InvalidKey) )); - assert!(matches!( - SessionKey::from_fragment(""), - Err(CryptoError::InvalidKey) - )); + assert!(matches!(SessionKey::from_fragment(""), Err(CryptoError::InvalidKey))); } #[test] @@ -402,10 +376,8 @@ mod tests { let Ok(path) = std::env::var("INTEROP_OUT") else { return; }; - let blob = vector_key().encrypt( - b"rust-produced blob: hello viewer\n", - &frame_aad(FrameKind::Keyframe, 111), - ); + let blob = vector_key() + .encrypt(b"rust-produced blob: hello viewer\n", &frame_aad(FrameKind::Keyframe, 111)); std::fs::write(&path, hex::encode(blob)).expect("write INTEROP_OUT"); } } diff --git a/crates/atuin-lab-share/src/error.rs b/crates/atuin-lab-share/src/error.rs index 04b9ff224..965ced636 100644 --- a/crates/atuin-lab-share/src/error.rs +++ b/crates/atuin-lab-share/src/error.rs @@ -52,7 +52,10 @@ pub enum Error { "couldn't reach the hub at {url} within {timeout_secs}s -- is it running, and is \ ATUIN_LAB_HUB_URL correct?" )] - HubUnreachable { url: Url, timeout_secs: u64 }, + HubUnreachable { + url: Url, + timeout_secs: u64, + }, /// The transport task ended (dropped its oneshot) before delivering the /// first join URL. @@ -102,8 +105,8 @@ pub enum Error { /// old proxy writes its one-shot snapshot blob and closes). The proxy's /// version is fixed at shell startup, so the fix is a new session. #[error( - "the running atuin pty-proxy is too old to attach to -- start a new shell session \ - (with the current atuin) and run --active there" + "the running atuin pty-proxy is too old to attach to -- start a new shell session (with \ + the current atuin) and run --active there" )] ProxyTooOld, @@ -124,9 +127,9 @@ pub enum Error { /// answer), keeps the retry advice, and points `lsof` at *whatever* holds /// the socket rather than at a client that may not exist. #[error( - "the atuin pty-proxy socket accepted the connection but did not answer in time -- \ - wait a moment and retry; if it persists, check what is holding the socket with \ - `lsof $ATUIN_PTY_PROXY_SOCKET`" + "the atuin pty-proxy socket accepted the connection but did not answer in time -- wait a \ + moment and retry; if it persists, check what is holding the socket with `lsof \ + $ATUIN_PTY_PROXY_SOCKET`" )] ProxyUnresponsive, @@ -149,19 +152,22 @@ pub enum Error { /// `cols`/`rows` are what the proxy reported, so the message quotes back /// the size of the terminal the user is looking at. #[error( - "the terminal behind the atuin pty-proxy is too small to share: {cols}x{rows} -- \ - atuin lab share needs at least {}x{} -- resize that terminal and run \ - `atuin lab share --active` again", + "the terminal behind the atuin pty-proxy is too small to share: {cols}x{rows} -- atuin \ + lab share needs at least {}x{} -- resize that terminal and run `atuin lab share \ + --active` again", crate::MIN_COLS, crate::MIN_CHILD_ROWS )] - ProxyTerminalTooSmall { cols: u16, rows: u16 }, + ProxyTerminalTooSmall { + cols: u16, + rows: u16, + }, /// `--active --write` asked the proxy for input access and was refused: /// the token we sent did not match the proxy's token file. #[error( - "the pty-proxy denied write access -- the token next to ATUIN_PTY_PROXY_SOCKET did \ - not match; share read-only (drop --write) or start a new shell session" + "the pty-proxy denied write access -- the token next to ATUIN_PTY_PROXY_SOCKET did not \ + match; share read-only (drop --write) or start a new shell session" )] ProxyInputDenied, @@ -194,7 +200,10 @@ pub enum Error { crate::MIN_CHILD_ROWS + 1, crate::MIN_CHILD_ROWS, )] - TerminalTooSmall { cols: u16, rows: u16 }, + TerminalTooSmall { + cols: u16, + rows: u16, + }, /// `atuin lab share` is unix-only for now (it needs a PTY). #[error("atuin lab share currently supports unix platforms only")] diff --git a/crates/atuin-lab-share/src/lib.rs b/crates/atuin-lab-share/src/lib.rs index e2c7a6d83..266f1e016 100644 --- a/crates/atuin-lab-share/src/lib.rs +++ b/crates/atuin-lab-share/src/lib.rs @@ -557,13 +557,13 @@ fn check_terminal() -> Result<()> { #[cfg(unix)] fn warning_copy(active: bool, write: render::WriteMode) -> String { let mut copy = String::from( - "!! Terminal sharing is experimental.\n Everything shown here -- including \ - secrets you type -- is visible to anyone with the link.", + "!! Terminal sharing is experimental.\n Everything shown here -- including secrets you \ + type -- is visible to anyone with the link.", ); if active { copy.push_str( - "\n Viewers will see the CURRENT contents of this terminal, including \ - anything already on screen.", + "\n Viewers will see the CURRENT contents of this terminal, including anything \ + already on screen.", ); } if write.is_write_enabled() { @@ -671,14 +671,8 @@ async fn connect_to_hub( eprintln!("Connecting to {} ...", opts.hub_url); let transport = tokio::spawn( - transport::Transport::new( - opts.hub_url.clone(), - opts.api_token.clone(), - write, - key, - url_tx, - ) - .run(out_rx, in_tx), + transport::Transport::new(opts.hub_url.clone(), opts.api_token.clone(), write, key, url_tx) + .run(out_rx, in_tx), ); match tokio::time::timeout(CONNECT_TIMEOUT, url_rx).await { @@ -699,13 +693,10 @@ fn spawn_subshell( join_url: &str, write: render::WriteMode, ) -> Result { - subshell::Subshell::spawn( - host_size, - &[ - ("ATUIN_SHARE_URL", join_url), - ("ATUIN_SHARE_WRITE", write.as_env_value()), - ], - ) + subshell::Subshell::spawn(host_size, &[ + ("ATUIN_SHARE_URL", join_url), + ("ATUIN_SHARE_WRITE", write.as_env_value()), + ]) } /// `atuin lab share` is unix-only for now (it needs a PTY). @@ -725,8 +716,8 @@ mod tests { fn warning_copy_read_only() { assert_eq!( warning_copy(false, WriteMode::ReadOnly), - "!! Terminal sharing is experimental.\n Everything shown here -- including \ - secrets you type -- is visible to anyone with the link." + "!! Terminal sharing is experimental.\n Everything shown here -- including secrets \ + you type -- is visible to anyone with the link." ); } @@ -734,9 +725,9 @@ mod tests { fn warning_copy_write() { assert_eq!( warning_copy(false, WriteMode::ReadWrite), - "!! Terminal sharing is experimental.\n Everything shown here -- including \ - secrets you type -- is visible to anyone with the link.\n WRITE MODE: they \ - can run commands on your machine." + "!! Terminal sharing is experimental.\n Everything shown here -- including secrets \ + you type -- is visible to anyone with the link.\n WRITE MODE: they can run commands \ + on your machine." ); } @@ -744,9 +735,9 @@ mod tests { fn warning_copy_active_read_only() { assert_eq!( warning_copy(true, WriteMode::ReadOnly), - "!! Terminal sharing is experimental.\n Everything shown here -- including \ - secrets you type -- is visible to anyone with the link.\n Viewers will see \ - the CURRENT contents of this terminal, including anything already on screen." + "!! Terminal sharing is experimental.\n Everything shown here -- including secrets \ + you type -- is visible to anyone with the link.\n Viewers will see the CURRENT \ + contents of this terminal, including anything already on screen." ); } @@ -755,10 +746,10 @@ mod tests { fn warning_copy_active_write() { assert_eq!( warning_copy(true, WriteMode::ReadWrite), - "!! Terminal sharing is experimental.\n Everything shown here -- including \ - secrets you type -- is visible to anyone with the link.\n Viewers will see \ - the CURRENT contents of this terminal, including anything already on screen.\ - \n WRITE MODE: they can run commands on your machine." + "!! Terminal sharing is experimental.\n Everything shown here -- including secrets \ + you type -- is visible to anyone with the link.\n Viewers will see the CURRENT \ + contents of this terminal, including anything already on screen.\n WRITE MODE: they \ + can run commands on your machine." ); } @@ -774,9 +765,7 @@ mod tests { #[test] fn affirmative_answers() { - for answer in [ - "y", "Y", "yes", "YES", "Yes", " y ", "y\n", "yes\r\n", "\ty\t", - ] { + for answer in ["y", "Y", "yes", "YES", "Yes", " y ", "y\n", "yes\r\n", "\ty\t"] { assert!(is_affirmative(answer), "{answer:?} should confirm"); } } @@ -785,9 +774,7 @@ mod tests { /// almost-yeses all decline. #[test] fn declining_answers() { - for answer in [ - "", "\n", "n", "N", "no", "yess", "y es", "yeah", "ja", "si", "0", "true", - ] { + for answer in ["", "\n", "n", "N", "no", "yess", "y es", "yeah", "ja", "si", "0", "true"] { assert!(!is_affirmative(answer), "{answer:?} should decline"); } } @@ -805,10 +792,7 @@ mod tests { /// child and the spawning parent: byte-pin it. #[test] fn share_already_running_display() { - assert_eq!( - Error::ShareAlreadyRunning.to_string(), - "an active share is already running" - ); + assert_eq!(Error::ShareAlreadyRunning.to_string(), "an active share is already running"); } /// A silent proxy is transient: the copy must say retry and point at @@ -822,9 +806,9 @@ mod tests { let copy = Error::ProxyUnresponsive.to_string(); assert_eq!( copy, - "the atuin pty-proxy socket accepted the connection but did not answer in time \ - -- wait a moment and retry; if it persists, check what is holding the socket \ - with `lsof $ATUIN_PTY_PROXY_SOCKET`" + "the atuin pty-proxy socket accepted the connection but did not answer in time -- \ + wait a moment and retry; if it persists, check what is holding the socket with `lsof \ + $ATUIN_PTY_PROXY_SOCKET`" ); assert!(copy.is_ascii()); assert!(!copy.contains("new shell")); @@ -853,14 +837,10 @@ mod tests { (80, 4, Size { cols: 80, rows: 3 }), (1, 24, Size { cols: 1, rows: 23 }), (2, 24, Size { cols: 2, rows: 23 }), - ( - 200, - 60, - Size { - cols: 200, - rows: 59, - }, - ), + (200, 60, Size { + cols: 200, + rows: 59, + }), ] { assert_eq!( host_size_from(cols, rows).expect("{cols}x{rows} is a survivable size"), @@ -899,10 +879,10 @@ mod tests { let copy = Error::TerminalTooSmall { cols: 0, rows: 0 }.to_string(); assert_eq!( copy, - "terminal is too small to share: 0x0 -- atuin lab share needs at least 1x3 \ - (2 rows for the shell plus 1 reserved for the warning bar) -- resize the \ - window, or if this is a non-interactive invocation such as \ - `script -q /dev/null ...`, give it a real terminal size" + "terminal is too small to share: 0x0 -- atuin lab share needs at least 1x3 (2 rows \ + for the shell plus 1 reserved for the warning bar) -- resize the window, or if this \ + is a non-interactive invocation such as `script -q /dev/null ...`, give it a real \ + terminal size" ); assert!(copy.is_ascii()); } diff --git a/crates/atuin-lab-share/src/lifecycle.rs b/crates/atuin-lab-share/src/lifecycle.rs index da1b7ff2a..991cc23d0 100644 --- a/crates/atuin-lab-share/src/lifecycle.rs +++ b/crates/atuin-lab-share/src/lifecycle.rs @@ -71,9 +71,7 @@ pub fn url_file_path() -> PathBuf { fn url_file_path_from(runtime_dir: Option) -> PathBuf { match runtime_dir { Some(dir) => dir.join("lab-share.url"), - None => atuin_common::utils::data_dir() - .join("private") - .join("lab-share.url"), + None => atuin_common::utils::data_dir().join("private").join("lab-share.url"), } } @@ -154,13 +152,7 @@ fn open_pidfile(path: &Path) -> std::io::Result { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .mode(0o600) - .open(path) + OpenOptions::new().read(true).write(true).create(true).truncate(false).mode(0o600).open(path) } /// Probe the pidfile lock without disturbing a holder. @@ -233,24 +225,15 @@ pub async fn wait_for_lock_release(path: &Path, timeout: Duration) -> bool { /// best-effort: a failed write must not kill a healthy session. pub fn write_url_file(path: &Path, url: &str, owner: Option<&str>) -> std::io::Result<()> { let Some(parent) = path.parent() else { - return Err(std::io::Error::other( - "url file path has no parent directory", - )); + return Err(std::io::Error::other("url file path has no parent directory")); }; - fs::DirBuilder::new() - .recursive(true) - .mode(0o700) - .create(parent)?; + fs::DirBuilder::new().recursive(true).mode(0o700).create(parent)?; let mut tmp_name = path.file_name().unwrap_or_default().to_os_string(); tmp_name.push(".tmp"); let tmp = path.with_file_name(tmp_name); { - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&tmp)?; + let mut file = + OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(&tmp)?; file.write_all(url.as_bytes())?; file.write_all(b"\n")?; if let Some(owner) = owner { @@ -396,10 +379,7 @@ mod tests { assert_eq!(read_url_file(&path), None, "missing file reads None"); write_url_file(&path, "https://hub.example/s/one#key1", None).unwrap(); - assert_eq!( - read_url_file(&path).as_deref(), - Some("https://hub.example/s/one#key1") - ); + assert_eq!(read_url_file(&path).as_deref(), Some("https://hub.example/s/one#key1")); assert_eq!(read_url_file_owner(&path), None, "no owner recorded"); let mode = fs::metadata(&path).unwrap().permissions().mode(); assert_eq!(mode & 0o777, 0o600, "URL file must be private"); @@ -442,10 +422,7 @@ mod tests { let path = tmp.path().join("private").join("lab-share.url"); write_url_file(&path, "https://hub.example/s/one#key1", None).unwrap(); - let mode = fs::metadata(tmp.path().join("private")) - .unwrap() - .permissions() - .mode(); + let mode = fs::metadata(tmp.path().join("private")).unwrap().permissions().mode(); assert_eq!(mode & 0o777, 0o700, "URL directory must be private"); } diff --git a/crates/atuin-lab-share/src/protocol.rs b/crates/atuin-lab-share/src/protocol.rs index 713559b81..c8565ddbc 100644 --- a/crates/atuin-lab-share/src/protocol.rs +++ b/crates/atuin-lab-share/src/protocol.rs @@ -36,14 +36,8 @@ impl PhoenixPush<'_> { /// Encode as a Phoenix v2 JSON array. #[must_use] pub(crate) fn encode(&self) -> String { - serde_json::json!([ - self.join_ref, - self.ref_, - self.topic, - self.event, - self.payload - ]) - .to_string() + serde_json::json!([self.join_ref, self.ref_, self.topic, self.event, self.payload]) + .to_string() } } @@ -64,7 +58,10 @@ pub(crate) enum Incoming { response: Value, }, /// A server-initiated channel event (`output` requests, `participants`, …). - Event { event: String, payload: Value }, + Event { + event: String, + payload: Value, + }, /// `phx_error`: the channel process crashed; the client must rejoin. Error { // `parse` fills this in from the `phx_error` payload so the variant @@ -110,9 +107,10 @@ impl Incoming { #[cfg(test)] mod tests { - use super::*; use serde_json::json; + use super::*; + // Encoders: compare *parsed* values, not raw strings — the wire format is // "a JSON array with these five elements", not a particular byte sequence. @@ -129,10 +127,7 @@ mod tests { } .encode(); let v: Value = serde_json::from_str(&frame).expect("valid JSON"); - assert_eq!( - v, - json!(["1", "1", "share:host", "phx_join", { "write": true }]) - ); + assert_eq!(v, json!(["1", "1", "share:host", "phx_join", { "write": true }])); } #[test] @@ -147,10 +142,7 @@ mod tests { } .encode(); let v: Value = serde_json::from_str(&frame).expect("valid JSON"); - assert_eq!( - v, - json!(["1", "7", "share:host", "output", { "seq": 3, "data": "aGk=" }]) - ); + assert_eq!(v, json!(["1", "7", "share:host", "output", { "seq": 3, "data": "aGk=" }])); } #[test] @@ -198,10 +190,7 @@ mod tests { #[test] fn decode_maps_phx_close() { let raw = r#"["1","1","share:host","phx_close",{}]"#; - assert!(matches!( - Incoming::parse(raw).expect("decodes"), - Incoming::Close - )); + assert!(matches!(Incoming::parse(raw).expect("decodes"), Incoming::Close)); } #[test] @@ -221,18 +210,9 @@ mod tests { #[test] fn decode_tolerates_empty_and_short_arrays_as_other() { - assert!(matches!( - Incoming::parse("[]").expect("decodes"), - Incoming::Other - )); - assert!(matches!( - Incoming::parse(r#"["1","2"]"#).expect("decodes"), - Incoming::Other - )); - assert!(matches!( - Incoming::parse("null").expect("decodes"), - Incoming::Other - )); + assert!(matches!(Incoming::parse("[]").expect("decodes"), Incoming::Other)); + assert!(matches!(Incoming::parse(r#"["1","2"]"#).expect("decodes"), Incoming::Other)); + assert!(matches!(Incoming::parse("null").expect("decodes"), Incoming::Other)); } #[test] diff --git a/crates/atuin-lab-share/src/proxy_tap.rs b/crates/atuin-lab-share/src/proxy_tap.rs index 777bb74ed..ed68afbcf 100644 --- a/crates/atuin-lab-share/src/proxy_tap.rs +++ b/crates/atuin-lab-share/src/proxy_tap.rs @@ -286,9 +286,7 @@ fn read_token(socket_path: &Path, want_input: bool) -> crate::Result> { if !want_input { return Ok(Vec::new()); } - let path = socket_path - .parent() - .map_or_else(|| PathBuf::from("token"), |dir| dir.join("token")); + let path = socket_path.parent().map_or_else(|| PathBuf::from("token"), |dir| dir.join("token")); std::fs::read(&path).map_err(|source| Error::ProxyToken { path, source }) } @@ -318,9 +316,7 @@ fn read_token(socket_path: &Path, want_input: bool) -> crate::Result> { fn handshake(stream: &mut UnixStream, want_input: bool, token: &[u8]) -> crate::Result { if stream.set_read_timeout(Some(HANDSHAKE_TIMEOUT)).is_err() || stream.write_all(&MAGIC).is_err() - || stream - .write_all(&protocol::subscribe_frame(want_input, token)) - .is_err() + || stream.write_all(&protocol::subscribe_frame(want_input, token)).is_err() { return Err(Error::ProxyTooOld); } @@ -373,10 +369,7 @@ fn handshake(stream: &mut UnixStream, want_input: bool, token: &[u8]) -> crate:: /// reliable here because `protocol::read_frame` -> `read_exact_or_eof` /// propagates the raw `io::Error` untouched. fn is_read_timeout(error: &io::Error) -> bool { - matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut - ) + matches!(error.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut) } /// The blocking reader half of the tap, run by the session's detached @@ -559,9 +552,8 @@ mod tests { let mut magic = [0u8; 4]; server.read_exact(&mut magic).expect("greeting"); assert_eq!(magic, MAGIC); - let (frame_type, payload) = protocol::read_frame(server) - .expect("subscribe frame") - .expect("not eof"); + let (frame_type, payload) = + protocol::read_frame(server).expect("subscribe frame").expect("not eof"); assert_eq!(frame_type, protocol::FRAME_SUBSCRIBE); let (got_want, got_token) = protocol::decode_subscribe(&payload).expect("valid subscribe"); assert_eq!(got_want, want_input); @@ -595,9 +587,7 @@ mod tests { let served = snapshot.clone(); let fake_proxy = std::thread::spawn(move || { expect_subscribe(&mut server, true, b"tok"); - server - .write_all(&protocol::hello_frame(true)) - .expect("hello"); + server.write_all(&protocol::hello_frame(true)).expect("hello"); server .write_all(&protocol::encode_frame(FRAME_KEYFRAME, &served.encode())) .expect("keyframe"); @@ -618,18 +608,12 @@ mod tests { let _ = server.write_all(&test_snapshot().encode()); // close without ever reading }); - assert!(matches!( - handshake(&mut client, false, b""), - Err(Error::ProxyTooOld) - )); + assert!(matches!(handshake(&mut client, false, b""), Err(Error::ProxyTooOld))); fake_proxy.join().expect("fake proxy"); let (mut client, server) = UnixStream::pair().expect("socketpair"); drop(server); // immediate EOF - assert!(matches!( - handshake(&mut client, false, b""), - Err(Error::ProxyTooOld) - )); + assert!(matches!(handshake(&mut client, false, b""), Err(Error::ProxyTooOld))); } /// A proxy that accepts and then goes silent is *wedged*, not old: the @@ -671,9 +655,7 @@ mod tests { let (mut client, mut server) = UnixStream::pair().expect("socketpair"); let fake_proxy = std::thread::spawn(move || { expect_subscribe(&mut server, false, b""); - server - .write_all(&protocol::hello_frame(false)) - .expect("hello"); + server.write_all(&protocol::hello_frame(false)).expect("hello"); // ...then nothing, with the connection held open. std::thread::sleep(HANDSHAKE_TIMEOUT + Duration::from_secs(1)); }); @@ -694,10 +676,7 @@ mod tests { expect_subscribe(&mut server, true, b"wrong"); let _ = server.write_all(&protocol::hello_frame(false)); }); - assert!(matches!( - handshake(&mut client, true, b"wrong"), - Err(Error::ProxyInputDenied) - )); + assert!(matches!(handshake(&mut client, true, b"wrong"), Err(Error::ProxyInputDenied))); fake_proxy.join().expect("fake proxy"); } @@ -712,18 +691,10 @@ mod tests { let (client, mut server) = UnixStream::pair().expect("socketpair"); let (mut reader, eof_rx) = stopped_reader(client); - server - .write_all(&protocol::encode_frame(FRAME_OUTPUT, b"hello")) - .expect("output"); - server - .write_all(&protocol::resize_frame(30, 100)) - .expect("resize"); - server - .write_all(&protocol::encode_frame(0x7f, b"future")) - .expect("unknown"); - server - .write_all(&protocol::encode_frame(FRAME_OUTPUT, b"world")) - .expect("output"); + server.write_all(&protocol::encode_frame(FRAME_OUTPUT, b"hello")).expect("output"); + server.write_all(&protocol::resize_frame(30, 100)).expect("resize"); + server.write_all(&protocol::encode_frame(0x7f, b"future")).expect("unknown"); + server.write_all(&protocol::encode_frame(FRAME_OUTPUT, b"world")).expect("output"); server.write_all(&protocol::end_frame()).expect("end"); drop(server); @@ -734,13 +705,10 @@ mod tests { // The resize arrives exactly between the chunks it separated on the // wire — never before "hello" or after "world". match reader.read_event().expect("second event") { - Some(ReadEvent::Resize(size)) => assert_eq!( - size, - Size { - cols: 100, - rows: 30 - } - ), + Some(ReadEvent::Resize(size)) => assert_eq!(size, Size { + cols: 100, + rows: 30 + }), _ => panic!("expected the resize between the two output chunks"), } match reader.read_event().expect("third event") { @@ -775,10 +743,10 @@ mod tests { // A survivable geometry passes through untouched — no bar row is // reserved, because the tap feeds a headless session. - assert_eq!( - attach_size(&test_snapshot()).expect("80x24 is survivable"), - Size { cols: 80, rows: 24 } - ); + assert_eq!(attach_size(&test_snapshot()).expect("80x24 is survivable"), Size { + cols: 80, + rows: 24 + }); // Exactly on the floor is fine; one row under is not. assert!( attach_size(&Snapshot { @@ -853,12 +821,8 @@ mod tests { // Path 2: a mid-session Resize frame. let (client, mut server) = UnixStream::pair().expect("socketpair"); let (mut reader, _eof_rx) = stopped_reader(client); - server - .write_all(&protocol::resize_frame(1, 1)) - .expect("resize"); - server - .write_all(&protocol::resize_frame(0, 0)) - .expect("resize"); + server.write_all(&protocol::resize_frame(1, 1)).expect("resize"); + server.write_all(&protocol::resize_frame(0, 0)).expect("resize"); drop(server); match reader.read_event().expect("first resize") { Some(ReadEvent::Resize(size)) => assert_eq!(size, floor, "1x1 must be floored"), @@ -877,9 +841,7 @@ mod tests { let (client, mut server) = UnixStream::pair().expect("socketpair"); let (mut reader, _eof_rx) = stopped_reader(client); let payload: Vec = (0..=255u8).cycle().take(1000).collect(); - server - .write_all(&protocol::encode_frame(FRAME_OUTPUT, &payload)) - .expect("output"); + server.write_all(&protocol::encode_frame(FRAME_OUTPUT, &payload)).expect("output"); match reader.read_event().expect("event") { Some(ReadEvent::Output(bytes)) => assert_eq!(bytes, payload), @@ -899,13 +861,9 @@ mod tests { let fake_proxy = std::thread::spawn(move || { let (mut conn, _) = listener.accept().expect("accept"); expect_subscribe(&mut conn, false, b""); - conn.write_all(&protocol::hello_frame(false)) - .expect("hello"); - conn.write_all(&protocol::encode_frame( - FRAME_KEYFRAME, - &test_snapshot().encode(), - )) - .expect("keyframe"); + conn.write_all(&protocol::hello_frame(false)).expect("hello"); + conn.write_all(&protocol::encode_frame(FRAME_KEYFRAME, &test_snapshot().encode())) + .expect("keyframe"); conn // keep the reconnected feed open until the client is done }); @@ -958,9 +916,8 @@ mod tests { }), }; writer.write_all(b"keys").expect("write"); - let (frame_type, payload) = protocol::read_frame(&mut server) - .expect("frame") - .expect("not eof"); + let (frame_type, payload) = + protocol::read_frame(&mut server).expect("frame").expect("not eof"); assert_eq!(frame_type, protocol::FRAME_INPUT); assert_eq!(payload, b"keys"); } diff --git a/crates/atuin-lab-share/src/query.rs b/crates/atuin-lab-share/src/query.rs index ffb94d97a..963d920d9 100644 --- a/crates/atuin-lab-share/src/query.rs +++ b/crates/atuin-lab-share/src/query.rs @@ -99,10 +99,7 @@ mod tests { #[test] fn multiple_probes_in_one_chunk_are_all_answered_in_order() { - assert_eq!( - replies(b"\x1b[6n\x1b[0c", (2, 3)), - b"\x1b[3;4R\x1b[?6c".as_slice() - ); + assert_eq!(replies(b"\x1b[6n\x1b[0c", (2, 3)), b"\x1b[3;4R\x1b[?6c".as_slice()); } #[test] diff --git a/crates/atuin-lab-share/src/render.rs b/crates/atuin-lab-share/src/render.rs index 2aa1a9e99..e3428af46 100644 --- a/crates/atuin-lab-share/src/render.rs +++ b/crates/atuin-lab-share/src/render.rs @@ -4,10 +4,9 @@ use std::borrow::Cow; use std::io::Write as _; -use atuin_common::string::EllipsizeExt as _; -use atuin_common::string::Measure; use atuin_common::string::align::Alignment; use atuin_common::string::ellipsis::{Indicator, Pos}; +use atuin_common::string::{EllipsizeExt as _, Measure}; use unicode_width::UnicodeWidthStr; use crate::Size; @@ -135,10 +134,7 @@ impl StatusBar { let segments: [(u8, Cow<'static, str>); 5] = [ (MANDATORY, Cow::Borrowed("! SHARED SESSION")), (TIER_PROSE, Cow::Borrowed("anything you type is visible")), - ( - TIER_VIEWERS, - Cow::Owned(format!("{} viewers", self.viewers)), - ), + (TIER_VIEWERS, Cow::Owned(format!("{} viewers", self.viewers))), (MANDATORY, Cow::Borrowed(write_state)), (TIER_HINT, Cow::Borrowed("Ctrl-\\ to end")), ]; @@ -443,13 +439,7 @@ mod tests { joined.process(&keyframe); assert_eq!(joined.screen().contents(), live.screen().contents()); - assert_eq!( - joined.screen().cursor_position(), - live.screen().cursor_position() - ); - assert_eq!( - joined.screen().contents_formatted(), - live.screen().contents_formatted() - ); + assert_eq!(joined.screen().cursor_position(), live.screen().cursor_position()); + assert_eq!(joined.screen().contents_formatted(), live.screen().contents_formatted()); } } diff --git a/crates/atuin-lab-share/src/session/mod.rs b/crates/atuin-lab-share/src/session/mod.rs index 4d7ad3ee1..49c2971a3 100644 --- a/crates/atuin-lab-share/src/session/mod.rs +++ b/crates/atuin-lab-share/src/session/mod.rs @@ -49,7 +49,10 @@ pub(crate) enum Outbound { Keyframe(Frame), /// The host's terminal geometry (already minus the bar row), for the hub /// to negotiate viewer sizes against. - HostSize { cols: u16, rows: u16 }, + HostSize { + cols: u16, + rows: u16, + }, /// Kill switch / clean exit: end the session and invalidate the link now. End, } @@ -107,16 +110,16 @@ impl Inbound { /// E2EE blob, which the transport opens before dispatching to the session. pub(crate) fn from_event(event: &str, payload: &Value) -> Option { match event { - "input" => b64_decode(payload["data"].as_str().unwrap_or_default()) - .ok() - .map(Self::Input), + "input" => { + b64_decode(payload["data"].as_str().unwrap_or_default()).ok().map(Self::Input) + } "set_size" => Some(Self::SetSize { cols: u16::try_from(payload["cols"].as_u64()?).ok()?, rows: u16::try_from(payload["rows"].as_u64()?).ok()?, }), - "participants" => Some(Self::Participants( - u32::try_from(payload["count"].as_u64()?).ok()?, - )), + "participants" => { + Some(Self::Participants(u32::try_from(payload["count"].as_u64()?).ok()?)) + } "request_keyframe" => Some(Self::RequestKeyframe), _ => None, } @@ -297,8 +300,8 @@ impl SessionTask { self.input_disabled = true; self.dirty = true; eprintln!( - "\r\n[atuin lab share] viewer input disabled: the replay-protection budget is \ - spent. Output and viewing continue; restart the share to re-enable typing.\r" + "\r\n[atuin lab share] viewer input disabled: the replay-protection budget is spent. \ + Output and viewing continue; restart the share to re-enable typing.\r" ); } @@ -321,7 +324,8 @@ impl SessionTask { match &self.url_sink { Some(sink) => sink(join_url), None if fresh_session => println!( - "\r\n Reconnected as a NEW session -- the previous link is dead.\r\n New link: {join_url}\r" + "\r\n Reconnected as a NEW session -- the previous link is dead.\r\n New link: \ + {join_url}\r" ), None => println!("\r\n Share this link: {join_url}\r"), } @@ -970,10 +974,7 @@ mod tests { #[test] fn resize_notice_does_not_blame_a_viewer_when_nobody_is_watching() { - assert_eq!( - resize_notice(OLD, NEW, 0), - Some("resized to 100x30".to_string()) - ); + assert_eq!(resize_notice(OLD, NEW, 0), Some("resized to 100x30".to_string())); } #[test] @@ -1044,9 +1045,7 @@ mod tests { "the source is resized to the clamped geometry, once" ); // ...and the model built at that geometry survives wrapping text. - let _ = task - .screen - .process_chunk(b"wrap this well past the end\r\nmore\r\n"); + let _ = task.screen.process_chunk(b"wrap this well past the end\r\nmore\r\n"); } let (mut task, _) = resize_task(Size { cols: 80, rows: 23 }); task.apply_host_window(1, 1); @@ -1060,20 +1059,14 @@ mod tests { #[test] fn a_hub_negotiated_size_below_the_floor_is_clamped_not_honoured() { let (task, _) = resize_task(Size { cols: 80, rows: 23 }); - assert_eq!( - task.clamp_child(Size { cols: 0, rows: 0 }), - Size { - cols: crate::MIN_COLS, - rows: crate::MIN_CHILD_ROWS - } - ); - assert_eq!( - task.clamp_child(Size { cols: 80, rows: 1 }), - Size { - cols: 80, - rows: crate::MIN_CHILD_ROWS - } - ); + assert_eq!(task.clamp_child(Size { cols: 0, rows: 0 }), Size { + cols: crate::MIN_COLS, + rows: crate::MIN_CHILD_ROWS + }); + assert_eq!(task.clamp_child(Size { cols: 80, rows: 1 }), Size { + cols: 80, + rows: crate::MIN_CHILD_ROWS + }); // The physical maximum still wins over an oversized request. assert_eq!( task.clamp_child(Size { @@ -1222,10 +1215,10 @@ mod tests { // Startup order is fixed: geometry first, then the bootstrap // snapshot as chunk 0 — the very first `Output` frame — with the // initial keyframe riding it. - assert!(matches!( - next_outbound(&mut out_rx).await, - Outbound::HostSize { cols: 80, rows: 24 } - )); + assert!(matches!(next_outbound(&mut out_rx).await, Outbound::HostSize { + cols: 80, + rows: 24 + })); match next_outbound(&mut out_rx).await { Outbound::Output(frame) => { assert_eq!(frame.seq, 1, "the bootstrap is chunk 0: seq 1"); @@ -1233,15 +1226,10 @@ mod tests { } _ => panic!("the bootstrap must be the first Output frame"), } - assert!(matches!( - next_outbound(&mut out_rx).await, - Outbound::Keyframe(_) - )); + assert!(matches!(next_outbound(&mut out_rx).await, Outbound::Keyframe(_))); // Live source bytes flow out as `Output` frames. - source_tx - .send(ReadEvent::Output(b"hello".to_vec())) - .expect("reader alive"); + source_tx.send(ReadEvent::Output(b"hello".to_vec())).expect("reader alive"); match next_non_keyframe(&mut out_rx).await { Outbound::Output(frame) => assert_eq!(frame.data, b"hello"), _ => panic!("source bytes must surface as Output"), @@ -1249,9 +1237,7 @@ mod tests { // Viewer input (write mode) reaches the source's writer via the // detached pty-writer thread; poll until it lands. - in_tx - .send(Inbound::Input(b"typed".to_vec())) - .expect("session alive"); + in_tx.send(Inbound::Input(b"typed".to_vec())).expect("session alive"); let deadline = tokio::time::Instant::now() + Duration::from_secs(10); while sink.lock().expect("writer lock").as_slice() != b"typed" { assert!( @@ -1270,17 +1256,11 @@ mod tests { rows: 30, })) .expect("reader alive"); - assert!(matches!( - next_outbound(&mut out_rx).await, - Outbound::Keyframe(_) - )); - assert!(matches!( - next_non_keyframe(&mut out_rx).await, - Outbound::HostSize { - cols: 100, - rows: 30 - } - )); + assert!(matches!(next_outbound(&mut out_rx).await, Outbound::Keyframe(_))); + assert!(matches!(next_non_keyframe(&mut out_rx).await, Outbound::HostSize { + cols: 100, + rows: 30 + })); // Source EOF plus the wait closure's code ends the session with // exactly that code. @@ -1343,10 +1323,10 @@ mod tests { }; let run = tokio::spawn(session.run()); - assert!(matches!( - next_outbound(&mut out_rx).await, - Outbound::HostSize { cols: 80, rows: 24 } - )); + assert!(matches!(next_outbound(&mut out_rx).await, Outbound::HostSize { + cols: 80, + rows: 24 + })); // First join, then a reconnect the hub turned into a NEW session: // both URLs must reach the sink, in order — the fresh_session @@ -1371,13 +1351,10 @@ mod tests { ); tokio::time::sleep(Duration::from_millis(10)).await; } - assert_eq!( - urls.lock().expect("url lock").as_slice(), - [ - "https://hub.example/s/one#key".to_string(), - "https://hub.example/s/two#key".to_string(), - ] - ); + assert_eq!(urls.lock().expect("url lock").as_slice(), [ + "https://hub.example/s/one#key".to_string(), + "https://hub.example/s/two#key".to_string(), + ]); // EOF + wait still end a headless session with the wait's code. drop(source_tx); diff --git a/crates/atuin-lab-share/src/session/screen.rs b/crates/atuin-lab-share/src/session/screen.rs index 2c2231ba6..070d6c350 100644 --- a/crates/atuin-lab-share/src/session/screen.rs +++ b/crates/atuin-lab-share/src/session/screen.rs @@ -3,10 +3,9 @@ use std::time::{Duration, Instant}; -use crate::Size; use crate::backpressure::Frame; -use crate::query; use crate::render::keyframe_bytes; +use crate::{Size, query}; /// Bytes of child output after which a fresh keyframe is emitted. const KEYFRAME_BYTES: u64 = 256 * 1024; @@ -230,9 +229,7 @@ mod tests { let outcome = state.process_chunk(b"hi"); assert_eq!(outcome.output.seq, 1); assert_eq!(outcome.output.data, b"hi"); - let keyframe = outcome - .keyframe - .expect("the initial keyframe rides the first chunk"); + let keyframe = outcome.keyframe.expect("the initial keyframe rides the first chunk"); assert_eq!(keyframe.seq, 2, "minted after the output it reflects"); } @@ -297,10 +294,7 @@ mod tests { let expected = vt100::Parser::new(10, 40, 0); assert_eq!(keyframe.data, expected.screen().contents_formatted()); - assert!( - !state.keyframe_due(), - "set_size serviced the pending request" - ); + assert!(!state.keyframe_due(), "set_size serviced the pending request"); } /// The degenerate-geometry floor, end to end through the model. @@ -333,10 +327,7 @@ mod tests { // Newlines, carriage returns and SGR on the same tiny grid. let _ = state.process_chunk(b"\r\n\x1b[31mred\x1b[0m\r\nmore\r\n"); let keyframe = state.emit_keyframe(); - assert!( - !keyframe.data.is_empty(), - "a keyframe at the floor is still a real repaint" - ); + assert!(!keyframe.data.is_empty(), "a keyframe at the floor is still a real repaint"); } /// `set_size` floors the same way, including the mid-session shrink to a @@ -346,10 +337,7 @@ mod tests { let mut state = ScreenState::new(SIZE); let _ = state.process_chunk(b"hello world"); let keyframe = state.set_size(Size { cols: 1, rows: 1 }); - assert_eq!( - state.screen().size(), - (crate::MIN_CHILD_ROWS, crate::MIN_COLS) - ); + assert_eq!(state.screen().size(), (crate::MIN_CHILD_ROWS, crate::MIN_COLS)); assert!(!keyframe.data.is_empty()); // ...and the floored grid still takes wrapping text afterwards. let _ = state.process_chunk(b"and more text that wraps repeatedly"); diff --git a/crates/atuin-lab-share/src/subshell.rs b/crates/atuin-lab-share/src/subshell.rs index 8f78acc81..08bf65b59 100644 --- a/crates/atuin-lab-share/src/subshell.rs +++ b/crates/atuin-lab-share/src/subshell.rs @@ -85,10 +85,8 @@ impl Subshell { for (key, value) in env { cmd.env(key, value); } - let child = pair - .slave - .spawn_command(cmd) - .map_err(|e| Error::SpawnShell(format!("{e:#}")))?; + let child = + pair.slave.spawn_command(cmd).map_err(|e| Error::SpawnShell(format!("{e:#}")))?; drop(pair.slave); Ok(Self { diff --git a/crates/atuin-lab-share/src/transport.rs b/crates/atuin-lab-share/src/transport.rs index cde613f08..8d899d94b 100644 --- a/crates/atuin-lab-share/src/transport.rs +++ b/crates/atuin-lab-share/src/transport.rs @@ -151,7 +151,10 @@ enum TransportError { "the hub returned a join url on a different origin than the configured hub: got {got:?}, \ expected a url on {want} -- refusing to hand out the session key" )] - JoinUrlOrigin { got: String, want: String }, + JoinUrlOrigin { + got: String, + want: String, + }, #[error("connection closed by the hub")] Closed, } @@ -308,10 +311,7 @@ struct InputDrops { /// renumbers or splices frames causes an authentication failure on the viewer /// instead of corrupted content. fn output_payload(key: &SessionKey, frame: &Frame) -> Value { - let blob = key.encrypt( - &frame.data, - &crypto::frame_aad(FrameKind::Output, frame.seq), - ); + let blob = key.encrypt(&frame.data, &crypto::frame_aad(FrameKind::Output, frame.seq)); json!({ "seq": frame.seq, "data": b64_encode(&blob) }) } @@ -319,10 +319,7 @@ fn output_payload(key: &SessionKey, frame: &Frame) -> Value { /// Keyframe AAD, so a keyframe blob can never be reflected as output (or vice /// versa). fn keyframe_payload(key: &SessionKey, frame: &Frame) -> Value { - let blob = key.encrypt( - &frame.data, - &crypto::frame_aad(FrameKind::Keyframe, frame.seq), - ); + let blob = key.encrypt(&frame.data, &crypto::frame_aad(FrameKind::Keyframe, frame.seq)); json!({ "seq": frame.seq, "data": b64_encode(&blob) }) } @@ -451,9 +448,7 @@ impl Connection { return Ok(()); } for frame in self.queue.drain_output() { - self.wire - .push(EVENT_OUTPUT, output_payload(key, &frame)) - .await?; + self.wire.push(EVENT_OUTPUT, output_payload(key, &frame)).await?; } Ok(()) } @@ -493,9 +488,7 @@ impl Connection { // session — it is re-encrypted here, never cached as // ciphertext, so a fresh `seq` and a fresh random nonce always // travel together. - self.wire - .push(EVENT_KEYFRAME, keyframe_payload(key, &frame)) - .await?; + self.wire.push(EVENT_KEYFRAME, keyframe_payload(key, &frame)).await?; // Ends any resync window opened by an overflow: output queued // after this keyframe carries a greater `seq`, so the hub's // buffer is contiguous again. @@ -503,9 +496,7 @@ impl Connection { } Outbound::HostSize { cols, rows } => { self.flush(key).await?; - self.wire - .push(EVENT_HOST_SIZE, json!({ "cols": cols, "rows": rows })) - .await?; + self.wire.push(EVENT_HOST_SIZE, json!({ "cols": cols, "rows": rows })).await?; } Outbound::End => { self.flush(key).await?; @@ -638,8 +629,8 @@ impl Transport { } let d = &self.input_drops; eprintln!( - "\r\n[atuin lab share] input: accepted={} replay={} exhausted={} rejected={} \ - empty={} read_only={}\r", + "\r\n[atuin lab share] input: accepted={} replay={} exhausted={} rejected={} empty={} \ + read_only={}\r", d.accepted, d.replay, d.exhausted, d.rejected, d.empty, d.read_only ); } @@ -888,9 +879,7 @@ impl Transport { self.input_drops.rejected += 1; return None; } - let Some(nonce) = blob - .get(..NONCE_LEN) - .and_then(|n| <[u8; NONCE_LEN]>::try_from(n).ok()) + let Some(nonce) = blob.get(..NONCE_LEN).and_then(|n| <[u8; NONCE_LEN]>::try_from(n).ok()) else { self.input_drops.rejected += 1; return None; @@ -920,10 +909,7 @@ impl Transport { // and the fail-closed point un-inflatable by a hostile hub. It also // preserves the older invariant that garbage cannot displace real // nonces, strengthened into "garbage cannot consume budget". - let Ok(plaintext) = self - .key - .decrypt(blob, &crypto::frame_aad(FrameKind::Input, 0)) - else { + let Ok(plaintext) = self.key.decrypt(blob, &crypto::frame_aad(FrameKind::Input, 0)) else { self.input_drops.rejected += 1; return None; }; @@ -1130,7 +1116,6 @@ fn is_fresh_session(last: Option<&str>, new: &str) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::protocol::b64_decode; /// A fixed key so encrypt/decrypt assertions are reproducible across the @@ -1179,17 +1164,12 @@ mod tests { assert_eq!(url.host_str(), Some("hub.example.com")); // A path-prefixed base (reverse-proxied hub) keeps its prefix. assert_eq!(url.path(), "/some/base/sockets/share/websocket"); - let pairs: Vec<(String, String)> = url - .query_pairs() - .map(|(k, v)| (k.into_owned(), v.into_owned())) - .collect(); - assert_eq!( - pairs, - vec![ - ("vsn".to_string(), "2.0.0".to_string()), - ("token".to_string(), "api-token-123".to_string()), - ] - ); + let pairs: Vec<(String, String)> = + url.query_pairs().map(|(k, v)| (k.into_owned(), v.into_owned())).collect(); + assert_eq!(pairs, vec![ + ("vsn".to_string(), "2.0.0".to_string()), + ("token".to_string(), "api-token-123".to_string()), + ]); } /// A bare origin normalizes to path `/`; appending must not produce a @@ -1216,10 +1196,7 @@ mod tests { #[test] fn join_payload_has_no_resume_token_on_the_first_join() { - assert_eq!( - test_transport(false).join_payload(), - json!({ "write": false }) - ); + assert_eq!(test_transport(false).join_payload(), json!({ "write": false })); } #[test] @@ -1227,10 +1204,7 @@ mod tests { let mut t = test_transport(true); t.host_resume_token = Some("secret-resume".to_string()); t.last_public_token = Some("public-token".to_string()); - assert_eq!( - t.join_payload(), - json!({ "write": true, "resume_token": "secret-resume" }) - ); + assert_eq!(t.join_payload(), json!({ "write": true, "resume_token": "secret-resume" })); } /// `from_event` is keyless: it b64-decodes the sealed blob and passes it @@ -1288,18 +1262,10 @@ mod tests { let last = blob.len() - 1; blob[last] ^= 0x01; assert_eq!(t.decrypt_input(&blob), None); - assert_eq!( - t.input_nonces.len(), - 0, - "unauthenticated bytes never consume budget" - ); + assert_eq!(t.input_nonces.len(), 0, "unauthenticated bytes never consume budget"); blob[last] ^= 0x01; assert_eq!(t.decrypt_input(&blob).as_deref(), Some(&b"x"[..])); - assert_eq!( - t.input_nonces.len(), - 1, - "only the genuine blob spent a slot" - ); + assert_eq!(t.input_nonces.len(), 1, "only the genuine blob spent a slot"); } /// Cross-channel reflection: a blob the host sealed as *output* must never @@ -1336,10 +1302,7 @@ mod tests { ledger.record(nonce_of(i)); } assert!(ledger.is_full()); - assert!( - ledger.contains(&nonce_of(0)), - "the oldest nonce is NOT evicted to make room" - ); + assert!(ledger.contains(&nonce_of(0)), "the oldest nonce is NOT evicted to make room"); assert_eq!(ledger.len(), 4); } @@ -1370,11 +1333,7 @@ mod tests { // Asserted before anything else about the ledger's state, so that a // regression to an evicting policy fails HERE, on the security // property, rather than on a bookkeeping precondition. - assert_eq!( - t.decrypt_input(&x), - None, - "the captured blob must never execute a second time" - ); + assert_eq!(t.decrypt_input(&x), None, "the captured blob must never execute a second time"); assert_eq!(t.input_drops.replay, 1); // And the flood's tail was refused rather than allowed to displace it. @@ -1391,10 +1350,7 @@ mod tests { const CAP: usize = 8; let mut t = capped_transport(true, CAP); for i in 0..CAP { - assert!( - t.decrypt_input(&sealed_input(format!("k{i}").as_bytes())) - .is_some() - ); + assert!(t.decrypt_input(&sealed_input(format!("k{i}").as_bytes())).is_some()); } assert_eq!( t.decrypt_input(&sealed_input(b"never delivered")), @@ -1452,10 +1408,7 @@ mod tests { // Exactly at the limit: 3044 + 12 + 16 = 3072. let at_limit = sealed_input(&vec![b'a'; MAX_INPUT_BLOB_BYTES - 28]); assert_eq!(at_limit.len(), MAX_INPUT_BLOB_BYTES); - assert_eq!( - t.decrypt_input(&at_limit).map(|p| p.len()), - Some(MAX_INPUT_BLOB_BYTES - 28) - ); + assert_eq!(t.decrypt_input(&at_limit).map(|p| p.len()), Some(MAX_INPUT_BLOB_BYTES - 28)); } /// The ledger is process-lifetime and cleared by NOTHING — in particular @@ -1506,23 +1459,14 @@ mod tests { const CAP: usize = 4; let mut t = capped_transport(true, CAP); for i in 0..CAP { - assert!( - t.decrypt_input(&sealed_input(format!("k{i}").as_bytes())) - .is_some() - ); + assert!(t.decrypt_input(&sealed_input(format!("k{i}").as_bytes())).is_some()); } assert!(!t.input_exhausted_notified); for i in 0..10 { - assert_eq!( - t.decrypt_input(&sealed_input(format!("x{i}").as_bytes())), - None - ); + assert_eq!(t.decrypt_input(&sealed_input(format!("x{i}").as_bytes())), None); } assert!(t.input_exhausted_notified, "the notice latches"); - assert_eq!( - t.input_drops.exhausted, 10, - "every refusal is still counted" - ); + assert_eq!(t.input_drops.exhausted, 10, "every refusal is still counted"); // Armed exactly once, and drained by the first taker. assert!(t.take_input_disabled_notice()); assert!(!t.take_input_disabled_notice()); @@ -1585,14 +1529,8 @@ mod tests { b"hello viewer" ); // Wrong kind (reflection) and wrong seq (renumbering) must both fail. - assert!( - key.decrypt(&blob, &crypto::frame_aad(FrameKind::Keyframe, 42)) - .is_err() - ); - assert!( - key.decrypt(&blob, &crypto::frame_aad(FrameKind::Output, 43)) - .is_err() - ); + assert!(key.decrypt(&blob, &crypto::frame_aad(FrameKind::Keyframe, 42)).is_err()); + assert!(key.decrypt(&blob, &crypto::frame_aad(FrameKind::Output, 43)).is_err()); } #[test] @@ -1611,10 +1549,7 @@ mod tests { .expect("genuine keyframe blob decrypts"), b"\x1b[2Jrepaint" ); - assert!( - key.decrypt(&blob, &crypto::frame_aad(FrameKind::Output, 7)) - .is_err() - ); + assert!(key.decrypt(&blob, &crypto::frame_aad(FrameKind::Output, 7)).is_err()); } /// Each encryption draws a fresh random nonce, so equal plaintext under @@ -1626,10 +1561,7 @@ mod tests { seq: 1, data: b"same bytes".to_vec(), }; - assert_ne!( - output_payload(&key, &frame)["data"], - output_payload(&key, &frame)["data"] - ); + assert_ne!(output_payload(&key, &frame)["data"], output_payload(&key, &frame)["data"]); } #[test] @@ -1660,10 +1592,7 @@ mod tests { // Both consumers see the SAME fragmented URL: the first-join oneshot // (feeds `ATUIN_SHARE_URL`) and the session's `Connected`. - assert_eq!( - url_rx.try_recv().expect("first join fires the oneshot"), - want - ); + assert_eq!(url_rx.try_recv().expect("first join fires the oneshot"), want); match in_rx.try_recv().expect("Connected was sent") { Inbound::Connected { join_url, @@ -1789,10 +1718,7 @@ mod tests { .expect("wss hub and https join url are the same origin"); assert_eq!( url, - format!( - "https://hub.example.com/lab/share/t#{}", - test_key().to_fragment() - ) + format!("https://hub.example.com/lab/share/t#{}", test_key().to_fragment()) ); } @@ -1840,40 +1766,25 @@ mod tests { fn same_origin_compares_scheme_host_and_port_and_nothing_else() { let accept = [ // ws/wss hub bases against the http/https links they mint. - ( - "wss://hub.example.com", - "https://hub.example.com/lab/share/t", - ), + ("wss://hub.example.com", "https://hub.example.com/lab/share/t"), ("wss://hub.example.com", "https://hub.example.com:443/x"), ("ws://127.0.0.1:4131", "http://127.0.0.1:4131/lab/share/t"), ("ws://localhost", "http://localhost:80/x"), // An http/https base (an `ATUIN_LAB_HUB_URL` given as given). - ( - "https://hub.example.com/hub/", - "https://hub.example.com/a/b?c=1", - ), + ("https://hub.example.com/hub/", "https://hub.example.com/a/b?c=1"), ("http://127.0.0.1:4131", "http://127.0.0.1:4131/x"), ]; for (hub, join) in accept { assert!( - same_origin( - &Url::parse(hub).expect("hub"), - &Url::parse(join).expect("join") - ), + same_origin(&Url::parse(hub).expect("hub"), &Url::parse(join).expect("join")), "expected same origin: {hub} vs {join}" ); } let refuse = [ // Different host, including a lookalike subdomain and suffix. - ( - "wss://hub.example.com", - "https://attacker.example.com/collect?s=1", - ), - ( - "wss://hub.example.com", - "https://hub.example.com.evil.test/x", - ), + ("wss://hub.example.com", "https://attacker.example.com/collect?s=1"), + ("wss://hub.example.com", "https://hub.example.com.evil.test/x"), ("wss://hub.example.com", "https://evil.hub.example.com/x"), // Different port, including the implicit-vs-wrong-explicit pair. ("ws://127.0.0.1:4131", "http://127.0.0.1:4132/x"), @@ -1890,10 +1801,7 @@ mod tests { ]; for (hub, join) in refuse { assert!( - !same_origin( - &Url::parse(hub).expect("hub"), - &Url::parse(join).expect("join") - ), + !same_origin(&Url::parse(hub).expect("hub"), &Url::parse(join).expect("join")), "expected different origins: {hub} vs {join}" ); } @@ -2036,10 +1944,7 @@ mod tests { let blob = hex::decode(field("blob_hex")).expect("blob_hex is hex"); let expected = hex::decode(field("plaintext_hex")).expect("plaintext_hex is hex"); - assert!( - !expected.is_empty(), - "the sealed plaintext must be non-empty" - ); + assert!(!expected.is_empty(), "the sealed plaintext must be non-empty"); let mut transport = test_transport(true); diff --git a/crates/atuin-nucleo/matcher/benches/fuzzy_match.rs b/crates/atuin-nucleo/matcher/benches/fuzzy_match.rs index 504d68d15..29628df8d 100644 --- a/crates/atuin-nucleo/matcher/benches/fuzzy_match.rs +++ b/crates/atuin-nucleo/matcher/benches/fuzzy_match.rs @@ -36,14 +36,7 @@ const PROGRAMS: [&str; 8] = [ "atuin search", ]; -const ARGS: [&str; 6] = [ - "commit --amend", - "build --release", - "up -d", - "-la", - "--follow", - "status", -]; +const ARGS: [&str; 6] = ["commit --amend", "build --release", "up -d", "-la", "--follow", "status"]; const PATHS: [&str; 6] = [ "crates/atuin-client/src/history.rs", @@ -59,17 +52,15 @@ fn fuzzy_match(bencher: divan::Bencher, n: usize) { let haystacks: Vec = corpus(n).iter().map(|cmd| cmd.as_str().into()).collect(); let needle: Utf32String = "gitcm".into(); - bencher - .with_inputs(|| Matcher::new(Config::DEFAULT)) - .bench_local_refs(|matcher| { - let mut total: u32 = 0; - for haystack in &haystacks { - if let Some(score) = matcher.fuzzy_match(haystack.slice(..), needle.slice(..)) { - total += u32::from(score); - } + bencher.with_inputs(|| Matcher::new(Config::DEFAULT)).bench_local_refs(|matcher| { + let mut total: u32 = 0; + for haystack in &haystacks { + if let Some(score) = matcher.fuzzy_match(haystack.slice(..), needle.slice(..)) { + total += u32::from(score); } - total - }); + } + total + }); } /// Scores and ranks the whole candidate list, which is what a single keystroke does in the UI. diff --git a/crates/atuin-nucleo/matcher/src/chars.rs b/crates/atuin-nucleo/matcher/src/chars.rs index d13a24661..8e55ff7bb 100644 --- a/crates/atuin-nucleo/matcher/src/chars.rs +++ b/crates/atuin-nucleo/matcher/src/chars.rs @@ -2,9 +2,9 @@ use std::fmt::{self, Debug, Display}; +use crate::Config; #[cfg(feature = "unicode-casefold")] use crate::chars::case_fold::CASE_FOLDING_SIMPLE; -use crate::Config; //autogenerated by generate-ucd #[allow(warnings)] @@ -163,9 +163,7 @@ pub fn to_lower_case(c: char) -> char { #[inline(always)] pub fn is_upper_case(c: char) -> bool { #[cfg(feature = "unicode-casefold")] - let val = CASE_FOLDING_SIMPLE - .binary_search_by_key(&c, |(upper, _)| *upper) - .is_ok(); + let val = CASE_FOLDING_SIMPLE.binary_search_by_key(&c, |(upper, _)| *upper).is_ok(); #[cfg(not(feature = "unicode-casefold"))] let val = c.is_uppercase(); val @@ -195,10 +193,7 @@ pub fn graphemes(text: &str) -> impl Iterator + '_ { if grapheme == "\r\n" { '\n' } else { - grapheme - .chars() - .next() - .expect("graphemes must be non-empty") + grapheme.chars().next().expect("graphemes must be non-empty") } }); #[cfg(not(feature = "unicode-segmentation"))] diff --git a/crates/atuin-nucleo/matcher/src/debug.rs b/crates/atuin-nucleo/matcher/src/debug.rs index b8369f32d..45c442868 100644 --- a/crates/atuin-nucleo/matcher/src/debug.rs +++ b/crates/atuin-nucleo/matcher/src/debug.rs @@ -1,6 +1,7 @@ -use crate::matrix::{MatrixCell, ScoreCell}; use std::fmt::{Debug, Formatter, Result}; +use crate::matrix::{MatrixCell, ScoreCell}; + impl Debug for ScoreCell { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "({}, {})", self.score, self.matched) diff --git a/crates/atuin-nucleo/matcher/src/exact.rs b/crates/atuin-nucleo/matcher/src/exact.rs index 3cb3ceb20..5d22c0ad1 100644 --- a/crates/atuin-nucleo/matcher/src/exact.rs +++ b/crates/atuin-nucleo/matcher/src/exact.rs @@ -1,9 +1,8 @@ -use memchr::memmem; -use memchr::{Memchr, Memchr2}; +use memchr::{Memchr, Memchr2, memmem}; +use crate::Matcher; use crate::chars::{AsciiChar, Char}; use crate::score::{BONUS_FIRST_CHAR_MULTIPLIER, SCORE_MATCH}; -use crate::Matcher; impl Matcher { pub(crate) fn substring_match_1_ascii( diff --git a/crates/atuin-nucleo/matcher/src/fuzzy_greedy.rs b/crates/atuin-nucleo/matcher/src/fuzzy_greedy.rs index 386d289c9..d001d40f0 100644 --- a/crates/atuin-nucleo/matcher/src/fuzzy_greedy.rs +++ b/crates/atuin-nucleo/matcher/src/fuzzy_greedy.rs @@ -1,5 +1,5 @@ -use crate::chars::Char; use crate::Matcher; +use crate::chars::Char; impl Matcher { /// greedy fallback algorithm, much faster (linear time) but reported scores/indices @@ -12,7 +12,11 @@ impl Matcher { mut end: usize, indices: &mut Vec, ) -> Option { - let first_char_end = if H::ASCII && N::ASCII { start + 1 } else { end }; + let first_char_end = if H::ASCII && N::ASCII { + start + 1 + } else { + end + }; 'nonascii: { if !H::ASCII || !N::ASCII { let mut needle_iter = needle[1..].iter().copied(); diff --git a/crates/atuin-nucleo/matcher/src/fuzzy_optimal.rs b/crates/atuin-nucleo/matcher/src/fuzzy_optimal.rs index 1bcebe2ce..d19716821 100644 --- a/crates/atuin-nucleo/matcher/src/fuzzy_optimal.rs +++ b/crates/atuin-nucleo/matcher/src/fuzzy_optimal.rs @@ -121,11 +121,7 @@ impl MatcherDataView<'_, H> { let mut row_iter = needle.iter().copied().zip(self.row_offs.iter_mut()); let (mut needle_char, mut row_start) = row_iter.next().unwrap(); - let col_iter = self - .haystack - .iter_mut() - .zip(self.bonus.iter_mut()) - .enumerate(); + let col_iter = self.haystack.iter_mut().zip(self.bonus.iter_mut()).enumerate(); let mut matched = false; for (i, (c_, bonus_)) in col_iter { @@ -271,11 +267,8 @@ impl MatcherDataView<'_, H> { H: PartialEq, { let mut matrix_cells = &mut self.matrix_cells[self.current_row.len()..]; - let mut row_iter = needle[1..] - .iter() - .copied() - .zip(self.row_offs[1..].iter().copied()) - .enumerate(); + let mut row_iter = + needle[1..].iter().copied().zip(self.row_offs[1..].iter().copied()).enumerate(); let (mut needle_idx, (mut needle_char, mut row_off)) = row_iter.next().unwrap(); for (next_needle_idx, (next_needle_char, next_row_off)) in row_iter { Self::score_row::( @@ -312,18 +305,16 @@ impl MatcherDataView<'_, H> { let mut matrix_cells = &self.matrix_cells[..matrix_len]; let width = self.current_row.len(); - let mut row_iter = self.row_offs[..self.row_offs.len() - 1] - .iter() - .copied() - .enumerate() - .rev() - .map(|(i, off)| { - let relative_off = off as usize - i; - let row; - (matrix_cells, row) = - matrix_cells.split_at(matrix_cells.len() - (width - relative_off)); - (i, off, row) - }); + let mut row_iter = + self.row_offs[..self.row_offs.len() - 1].iter().copied().enumerate().rev().map( + |(i, off)| { + let relative_off = off as usize - i; + let row; + (matrix_cells, row) = + matrix_cells.split_at(matrix_cells.len() - (width - relative_off)); + (i, off, row) + }, + ); let (mut row_idx, mut row_off, mut row) = row_iter.next().unwrap(); let mut col = max_score_end; let relative_last_row_off = last_row_off as usize + 1 - self.row_offs.len(); diff --git a/crates/atuin-nucleo/matcher/src/lib.rs b/crates/atuin-nucleo/matcher/src/lib.rs index 9ae4b6657..a1d4ed764 100644 --- a/crates/atuin-nucleo/matcher/src/lib.rs +++ b/crates/atuin-nucleo/matcher/src/lib.rs @@ -100,11 +100,10 @@ mod utf32_str; #[cfg(test)] mod tests; -pub use crate::config::Config; -pub use crate::utf32_str::{Utf32Str, Utf32String}; - use crate::chars::{AsciiChar, Char}; +pub use crate::config::Config; use crate::matrix::MatrixSlab; +pub use crate::utf32_str::{Utf32Str, Utf32String}; /// A matcher engine that can execute (fuzzy) matches. /// @@ -169,9 +168,7 @@ impl Clone for Matcher { impl std::fmt::Debug for Matcher { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Matcher") - .field("config", &self.config) - .finish_non_exhaustive() + f.debug_struct("Matcher").field("config", &self.config).finish_non_exhaustive() } } @@ -725,9 +722,7 @@ impl Matcher { AsciiChar::cast(haystack)[start..end] .iter() .map(|c| c.normalize(&self.config)) - .eq(AsciiChar::cast(needle) - .iter() - .map(|c| c.normalize(&self.config))) + .eq(AsciiChar::cast(needle).iter().map(|c| c.normalize(&self.config))) } else { &haystack[start..end] == needle }; @@ -751,9 +746,7 @@ impl Matcher { let matched = haystack[start..end] .iter() .map(|c| c.normalize(&self.config)) - .eq(AsciiChar::cast(needle) - .iter() - .map(|c| c.normalize(&self.config))); + .eq(AsciiChar::cast(needle).iter().map(|c| c.normalize(&self.config))); if !matched { return None; } diff --git a/crates/atuin-nucleo/matcher/src/matrix.rs b/crates/atuin-nucleo/matcher/src/matrix.rs index a91ed95f5..05a76452f 100644 --- a/crates/atuin-nucleo/matcher/src/matrix.rs +++ b/crates/atuin-nucleo/matcher/src/matrix.rs @@ -1,8 +1,8 @@ -use std::alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout}; +use std::alloc::{Layout, alloc_zeroed, dealloc, handle_alloc_error}; use std::marker::PhantomData; use std::mem::size_of; use std::panic::{RefUnwindSafe, UnwindSafe}; -use std::ptr::{slice_from_raw_parts_mut, NonNull}; +use std::ptr::{NonNull, slice_from_raw_parts_mut}; use crate::chars::Char; @@ -64,13 +64,7 @@ impl MatrixLayout { unsafe fn fieds_from_ptr( &self, ptr: NonNull, - ) -> ( - *mut [C], - *mut [u8], - *mut [u16], - *mut [ScoreCell], - *mut [MatrixCell], - ) { + ) -> (*mut [C], *mut [u8], *mut [u16], *mut [ScoreCell], *mut [MatrixCell]) { let base = ptr.as_ptr(); let haystack = base.add(self.haystack_off) as *mut C; let haystack = slice_from_raw_parts_mut(haystack, self.haystack_len); @@ -177,9 +171,7 @@ impl MatrixSlab { matrix_layout.fieds_from_ptr(self.0); // copy haystack before creating references to ensure we don't create // references to invalid chars (which may or may not be UB) - haystack_ - .as_ptr() - .copy_to_nonoverlapping(haystack as *mut _, haystack_.len()); + haystack_.as_ptr().copy_to_nonoverlapping(haystack as *mut _, haystack_.len()); Some(MatcherDataView { haystack: &mut *haystack, row_offs: &mut *rows, diff --git a/crates/atuin-nucleo/matcher/src/pattern.rs b/crates/atuin-nucleo/matcher/src/pattern.rs index 495feede3..46cc21a70 100644 --- a/crates/atuin-nucleo/matcher/src/pattern.rs +++ b/crates/atuin-nucleo/matcher/src/pattern.rs @@ -2,7 +2,7 @@ use std::cmp::Reverse; -use crate::{chars, Matcher, Utf32Str}; +use crate::{Matcher, Utf32Str, chars}; #[cfg(test)] mod tests; diff --git a/crates/atuin-nucleo/matcher/src/pattern/tests.rs b/crates/atuin-nucleo/matcher/src/pattern/tests.rs index 88880ba91..718cd3191 100644 --- a/crates/atuin-nucleo/matcher/src/pattern/tests.rs +++ b/crates/atuin-nucleo/matcher/src/pattern/tests.rs @@ -115,21 +115,15 @@ fn escape() { #[test] fn pattern_atoms() { - assert_eq!( - Pattern::parse("a b", CaseMatching::Ignore, Normalization::Smart).atoms, - vec![ - Atom::parse("a", CaseMatching::Ignore, Normalization::Smart), - Atom::parse("b", CaseMatching::Ignore, Normalization::Smart), - ] - ); + assert_eq!(Pattern::parse("a b", CaseMatching::Ignore, Normalization::Smart).atoms, vec![ + Atom::parse("a", CaseMatching::Ignore, Normalization::Smart), + Atom::parse("b", CaseMatching::Ignore, Normalization::Smart), + ]); - assert_eq!( - Pattern::parse("a\n b", CaseMatching::Ignore, Normalization::Smart).atoms, - vec![ - Atom::parse("a", CaseMatching::Ignore, Normalization::Smart), - Atom::parse("b", CaseMatching::Ignore, Normalization::Smart), - ] - ); + assert_eq!(Pattern::parse("a\n b", CaseMatching::Ignore, Normalization::Smart).atoms, vec![ + Atom::parse("a", CaseMatching::Ignore, Normalization::Smart), + Atom::parse("b", CaseMatching::Ignore, Normalization::Smart), + ]); assert_eq!( Pattern::parse(" a b\r\n", CaseMatching::Ignore, Normalization::Smart).atoms, @@ -139,11 +133,8 @@ fn pattern_atoms() { ] ); - assert_eq!( - Pattern::parse("ほ げ", CaseMatching::Smart, Normalization::Smart).atoms, - vec![ - Atom::parse("ほ", CaseMatching::Smart, Normalization::Smart), - Atom::parse("げ", CaseMatching::Smart, Normalization::Smart), - ], - ) + assert_eq!(Pattern::parse("ほ げ", CaseMatching::Smart, Normalization::Smart).atoms, vec![ + Atom::parse("ほ", CaseMatching::Smart, Normalization::Smart), + Atom::parse("げ", CaseMatching::Smart, Normalization::Smart), + ],) } diff --git a/crates/atuin-nucleo/matcher/src/prefilter.rs b/crates/atuin-nucleo/matcher/src/prefilter.rs index 1e79cc2c7..904a90bbf 100644 --- a/crates/atuin-nucleo/matcher/src/prefilter.rs +++ b/crates/atuin-nucleo/matcher/src/prefilter.rs @@ -1,8 +1,8 @@ use ::memchr::{memchr, memchr2, memrchr, memrchr2}; +use crate::Matcher; use crate::chars::Char; use crate::utf32_str::Utf32Str; -use crate::Matcher; #[inline(always)] fn find_ascii_ignore_case(c: u8, haystack: &[u8]) -> Option { diff --git a/crates/atuin-nucleo/matcher/src/tests.rs b/crates/atuin-nucleo/matcher/src/tests.rs index a883c6ba5..a44bb991e 100644 --- a/crates/atuin-nucleo/matcher/src/tests.rs +++ b/crates/atuin-nucleo/matcher/src/tests.rs @@ -1,3 +1,5 @@ +use Algorithm::*; + use crate::chars::Char; use crate::pattern::{CaseMatching, Normalization, Pattern}; use crate::score::{ @@ -7,8 +9,6 @@ use crate::score::{ use crate::utf32_str::Utf32Str; use crate::{Config, Matcher}; -use Algorithm::*; - #[derive(Debug)] enum Algorithm { FuzzyOptimal, @@ -70,12 +70,10 @@ fn assert_matches( assert_eq!( res, Some(score), - "{needle:?} did not match {haystack:?}: matched {match_chars:?} {matched_indices:?} {algo:?}" - ); - assert_eq!( - matched_indices, indices, - "{needle:?} match {haystack:?} {algo:?}" + "{needle:?} did not match {haystack:?}: matched {match_chars:?} \ + {matched_indices:?} {algo:?}" ); + assert_eq!(matched_indices, indices, "{needle:?} match {haystack:?} {algo:?}"); assert_eq!( match_chars, needle_chars, "{needle:?} match {haystack:?} indices are incorrect {matched_indices:?} {algo:?}" @@ -116,10 +114,7 @@ fn assert_not_matches_with( Postfix => matcher.postfix_match(haystack, needle), Exact => matcher.exact_match(haystack, needle), }; - assert_eq!( - res, None, - "{needle:?} should not match {haystack:?} {algo:?}" - ); + assert_eq!(res, None, "{needle:?} should not match {haystack:?} {algo:?}"); } } } @@ -138,111 +133,99 @@ const BONUS_BOUNDARY_DELIMITER: u16 = Config::DEFAULT.bonus_boundary_delimiter; #[test] fn test_fuzzy() { - assert_matches( - &[FuzzyGreedy, FuzzyOptimal], - false, - false, - false, - false, - &[ - ( - "fooBarbaz1", - "obr", - &[2, 3, 5], - BONUS_CAMEL123 - PENALTY_GAP_START, - ), - ( - "/usr/share/doc/at/ChangeLog", - "changelog", - &[18, 19, 20, 21, 22, 23, 24, 25, 26], - (BONUS_FIRST_CHAR_MULTIPLIER + 8) * BONUS_BOUNDARY_DELIMITER, - ), - ( - "fooBarbaz1", - "br", - &[3, 5], - BONUS_CAMEL123 * BONUS_FIRST_CHAR_MULTIPLIER - PENALTY_GAP_START, - ), - ( - "foo bar baz", - "fbb", - &[0, 4, 8], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 2 - - 2 * PENALTY_GAP_START - - 4 * PENALTY_GAP_EXTENSION, - ), - ( - "/AutomatorDocument.icns", - "rdoc", - &[9, 10, 11, 12], - BONUS_CAMEL123 + 2 * BONUS_CONSECUTIVE, - ), - ( - "/man1/zshcompctl.1", - "zshc", - &[6, 7, 8, 9], - BONUS_BOUNDARY_DELIMITER * (BONUS_FIRST_CHAR_MULTIPLIER + 3), - ), - ( - "/.oh-my-zsh/cache", - "zshc", - &[8, 9, 10, 12], - BONUS_BOUNDARY * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - PENALTY_GAP_START - + BONUS_BOUNDARY_DELIMITER, - ), - ( - "ab0123 456", - "12356", - &[3, 4, 5, 8, 9], - BONUS_CONSECUTIVE * 3 - PENALTY_GAP_START - PENALTY_GAP_EXTENSION, - ), - ( - "abc123 456", - "12356", - &[3, 4, 5, 8, 9], - BONUS_CAMEL123 * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - - PENALTY_GAP_START - - PENALTY_GAP_EXTENSION - + BONUS_CONSECUTIVE, - ), - ( - "foo/bar/baz", - "fbb", - &[0, 4, 8], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 - - 2 * PENALTY_GAP_START - - 4 * PENALTY_GAP_EXTENSION, - ), - ( - "fooBarBaz", - "fbb", - &[0, 3, 6], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 - - 2 * PENALTY_GAP_START - - 2 * PENALTY_GAP_EXTENSION, - ), - ( - "foo barbaz", - "fbb", - &[0, 4, 7], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE - - PENALTY_GAP_START * 2 - - PENALTY_GAP_EXTENSION * 3, - ), - ( - "fooBar Baz", - "foob", - &[0, 1, 2, 3], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), - ), - ( - "xFoo-Bar Baz", - "foo-b", - &[1, 2, 3, 4, 5], - BONUS_CAMEL123 * (BONUS_FIRST_CHAR_MULTIPLIER + 2) + 2 * BONUS_NON_WORD, - ), - ], - ); + assert_matches(&[FuzzyGreedy, FuzzyOptimal], false, false, false, false, &[ + ("fooBarbaz1", "obr", &[2, 3, 5], BONUS_CAMEL123 - PENALTY_GAP_START), + ( + "/usr/share/doc/at/ChangeLog", + "changelog", + &[18, 19, 20, 21, 22, 23, 24, 25, 26], + (BONUS_FIRST_CHAR_MULTIPLIER + 8) * BONUS_BOUNDARY_DELIMITER, + ), + ( + "fooBarbaz1", + "br", + &[3, 5], + BONUS_CAMEL123 * BONUS_FIRST_CHAR_MULTIPLIER - PENALTY_GAP_START, + ), + ( + "foo bar baz", + "fbb", + &[0, 4, 8], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 2 + - 2 * PENALTY_GAP_START + - 4 * PENALTY_GAP_EXTENSION, + ), + ( + "/AutomatorDocument.icns", + "rdoc", + &[9, 10, 11, 12], + BONUS_CAMEL123 + 2 * BONUS_CONSECUTIVE, + ), + ( + "/man1/zshcompctl.1", + "zshc", + &[6, 7, 8, 9], + BONUS_BOUNDARY_DELIMITER * (BONUS_FIRST_CHAR_MULTIPLIER + 3), + ), + ( + "/.oh-my-zsh/cache", + "zshc", + &[8, 9, 10, 12], + BONUS_BOUNDARY * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - PENALTY_GAP_START + + BONUS_BOUNDARY_DELIMITER, + ), + ( + "ab0123 456", + "12356", + &[3, 4, 5, 8, 9], + BONUS_CONSECUTIVE * 3 - PENALTY_GAP_START - PENALTY_GAP_EXTENSION, + ), + ( + "abc123 456", + "12356", + &[3, 4, 5, 8, 9], + BONUS_CAMEL123 * (BONUS_FIRST_CHAR_MULTIPLIER + 2) + - PENALTY_GAP_START + - PENALTY_GAP_EXTENSION + + BONUS_CONSECUTIVE, + ), + ( + "foo/bar/baz", + "fbb", + &[0, 4, 8], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 + - 2 * PENALTY_GAP_START + - 4 * PENALTY_GAP_EXTENSION, + ), + ( + "fooBarBaz", + "fbb", + &[0, 3, 6], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + - 2 * PENALTY_GAP_START + - 2 * PENALTY_GAP_EXTENSION, + ), + ( + "foo barbaz", + "fbb", + &[0, 4, 7], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE + - PENALTY_GAP_START * 2 + - PENALTY_GAP_EXTENSION * 3, + ), + ( + "fooBar Baz", + "foob", + &[0, 1, 2, 3], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), + ), + ( + "xFoo-Bar Baz", + "foo-b", + &[1, 2, 3, 4, 5], + BONUS_CAMEL123 * (BONUS_FIRST_CHAR_MULTIPLIER + 2) + 2 * BONUS_NON_WORD, + ), + ]); } #[test] @@ -259,60 +242,46 @@ fn empty_needle() { #[test] fn test_substring() { - assert_matches( - &[Substring, Prefix], - false, - false, - false, - false, - &[ - ( - "foo bar baz", - "foo", - &[0, 1, 2], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - " foo bar baz", - "FOO", - &[1, 2, 3], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - " foo bar baz", - " FOO", - &[0, 1, 2, 3], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), - ), - ], - ); - assert_matches( - &[Substring, Postfix], - false, - false, - false, - false, - &[ - ( - "foo bar baz", - "baz", - &[8, 9, 10], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - "foo bar baz ", - "baz", - &[8, 9, 10], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - "foo bar baz ", - "baz ", - &[8, 9, 10, 11], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), - ), - ], - ); + assert_matches(&[Substring, Prefix], false, false, false, false, &[ + ( + "foo bar baz", + "foo", + &[0, 1, 2], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ( + " foo bar baz", + "FOO", + &[1, 2, 3], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ( + " foo bar baz", + " FOO", + &[0, 1, 2, 3], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), + ), + ]); + assert_matches(&[Substring, Postfix], false, false, false, false, &[ + ( + "foo bar baz", + "baz", + &[8, 9, 10], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ( + "foo bar baz ", + "baz", + &[8, 9, 10], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ( + "foo bar baz ", + "baz ", + &[8, 9, 10, 11], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), + ), + ]); assert_matches( &[Substring, Prefix, Postfix, Exact, FuzzyGreedy, FuzzyOptimal], false, @@ -320,18 +289,8 @@ fn test_substring() { false, false, &[ - ( - "foo", - "foo", - &[0, 1, 2], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - " foo", - "foo", - &[1, 2, 3], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), + ("foo", "foo", &[0, 1, 2], BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2)), + (" foo", "foo", &[1, 2, 3], BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2)), ( " foo", " foo", @@ -340,419 +299,279 @@ fn test_substring() { ), ], ); - assert_matches( - &[Substring], - false, - false, - false, - false, - &[ - ( - "fooBarbaz1", - "oba", - &[2, 3, 4], - BONUS_CAMEL123 + BONUS_CONSECUTIVE, - ), - ( - "/AutomatorDocument.icns", - "rdoc", - &[9, 10, 11, 12], - BONUS_CAMEL123 + 2 * BONUS_CONSECUTIVE, - ), - ( - "/man1/zshcompctl.1", - "zshc", - &[6, 7, 8, 9], - BONUS_BOUNDARY_DELIMITER * (BONUS_FIRST_CHAR_MULTIPLIER + 3), - ), - ( - "/.oh-my-zsh/cache", - "zsh/c", - &[8, 9, 10, 11, 12], - BONUS_BOUNDARY * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - + BONUS_NON_WORD - + BONUS_BOUNDARY_DELIMITER, - ), - ], - ); - assert_not_matches_with( - true, - false, - &[Prefix, Substring, Postfix, Exact], - &[( - "At the Road’s End - Seeming - SOL: A Self-Banishment Ritual", - "adi", - )], - ) + assert_matches(&[Substring], false, false, false, false, &[ + ("fooBarbaz1", "oba", &[2, 3, 4], BONUS_CAMEL123 + BONUS_CONSECUTIVE), + ( + "/AutomatorDocument.icns", + "rdoc", + &[9, 10, 11, 12], + BONUS_CAMEL123 + 2 * BONUS_CONSECUTIVE, + ), + ( + "/man1/zshcompctl.1", + "zshc", + &[6, 7, 8, 9], + BONUS_BOUNDARY_DELIMITER * (BONUS_FIRST_CHAR_MULTIPLIER + 3), + ), + ( + "/.oh-my-zsh/cache", + "zsh/c", + &[8, 9, 10, 11, 12], + BONUS_BOUNDARY * (BONUS_FIRST_CHAR_MULTIPLIER + 2) + + BONUS_NON_WORD + + BONUS_BOUNDARY_DELIMITER, + ), + ]); + assert_not_matches_with(true, false, &[Prefix, Substring, Postfix, Exact], &[( + "At the Road’s End - Seeming - SOL: A Self-Banishment Ritual", + "adi", + )]) } #[test] fn test_substring_case_sensitive() { - assert_matches( - &[Substring, Prefix], - false, - true, - false, - false, - &[ - ( - "Foo bar baz", - "Foo", - &[0, 1, 2], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - "Fȫô bar baz", - "Fȫô", - &[0, 1, 2], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ( - "Foo ฿ar baz", - "Foo", - &[0, 1, 2], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), - ), - ], - ); + assert_matches(&[Substring, Prefix], false, true, false, false, &[ + ( + "Foo bar baz", + "Foo", + &[0, 1, 2], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ( + "Fȫô bar baz", + "Fȫô", + &[0, 1, 2], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ( + "Foo ฿ar baz", + "Foo", + &[0, 1, 2], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2), + ), + ]); assert_not_matches_with(false, true, &[Substring, Prefix], &[("foo bar baz", "Foo")]); } #[test] fn test_fuzzy_case_sensitive() { - assert_matches( - &[FuzzyGreedy, FuzzyOptimal], - false, - true, - false, - false, - &[ - ( - "fooBarbaz1", - "oBr", - &[2, 3, 5], - BONUS_CAMEL123 - PENALTY_GAP_START, - ), - ( - "Foo/Bar/Baz", - "FBB", - &[0, 4, 8], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 - - 2 * PENALTY_GAP_START - - 4 * PENALTY_GAP_EXTENSION, - ), - ( - "FooBarBaz", - "FBB", - &[0, 3, 6], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 - - 2 * PENALTY_GAP_START - - 2 * PENALTY_GAP_EXTENSION, - ), - ( - "FooBar Baz", - "FooB", - &[0, 1, 2, 3], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), - ), - ("foo-bar", "o-ba", &[2, 3, 4, 5], BONUS_NON_WORD * 3), - ], - ); + assert_matches(&[FuzzyGreedy, FuzzyOptimal], false, true, false, false, &[ + ("fooBarbaz1", "oBr", &[2, 3, 5], BONUS_CAMEL123 - PENALTY_GAP_START), + ( + "Foo/Bar/Baz", + "FBB", + &[0, 4, 8], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 + - 2 * PENALTY_GAP_START + - 4 * PENALTY_GAP_EXTENSION, + ), + ( + "FooBarBaz", + "FBB", + &[0, 3, 6], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + - 2 * PENALTY_GAP_START + - 2 * PENALTY_GAP_EXTENSION, + ), + ( + "FooBar Baz", + "FooB", + &[0, 1, 2, 3], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 3), + ), + ("foo-bar", "o-ba", &[2, 3, 4, 5], BONUS_NON_WORD * 3), + ]); } #[test] fn test_normalize() { - assert_matches( - &[FuzzyGreedy, FuzzyOptimal], - true, - false, - false, - false, - &[ - ( - "Só Danço Samba", - "So", - &[0, 1], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1), - ), - ( - "Só Danço Samba", - "sodc", - &[0, 1, 3, 6], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1) - PENALTY_GAP_START - + BONUS_BOUNDARY_WHITE - - PENALTY_GAP_START - - PENALTY_GAP_EXTENSION, - ), - ( - "Danço", - "danco", - &[0, 1, 2, 3, 4], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 4), - ), - ( - "DanÇo", - "danco", - &[0, 1, 2, 3, 4], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 4), - ), - ( - "xÇando", - "cando", - &[1, 2, 3, 4, 5], - BONUS_CAMEL123 * (BONUS_FIRST_CHAR_MULTIPLIER + 4), - ), - ("ۂ(GCGɴCG", "n", &[5], 0), - ], - ) + assert_matches(&[FuzzyGreedy, FuzzyOptimal], true, false, false, false, &[ + ("Só Danço Samba", "So", &[0, 1], BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1)), + ( + "Só Danço Samba", + "sodc", + &[0, 1, 3, 6], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1) - PENALTY_GAP_START + + BONUS_BOUNDARY_WHITE + - PENALTY_GAP_START + - PENALTY_GAP_EXTENSION, + ), + ( + "Danço", + "danco", + &[0, 1, 2, 3, 4], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 4), + ), + ( + "DanÇo", + "danco", + &[0, 1, 2, 3, 4], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 4), + ), + ("xÇando", "cando", &[1, 2, 3, 4, 5], BONUS_CAMEL123 * (BONUS_FIRST_CHAR_MULTIPLIER + 4)), + ("ۂ(GCGɴCG", "n", &[5], 0), + ]) } #[test] fn test_unicode() { - assert_matches( - &[FuzzyGreedy, FuzzyOptimal, Substring], - true, - false, - false, - false, - &[ - ( - "你好世界", - "你好", - &[0, 1], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1), - ), - ( - " 你好世界", - "你好", - &[1, 2], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1), - ), - ], - ); - assert_matches( - &[FuzzyGreedy, FuzzyOptimal], - true, - false, - false, - false, - &[( - "你好世界", - "你世", - &[0, 2], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER - PENALTY_GAP_START, - )], - ); - assert_not_matches( - false, - false, - &[("Flibbertigibbet / イタズラっ子たち", "lying")], - ); + assert_matches(&[FuzzyGreedy, FuzzyOptimal, Substring], true, false, false, false, &[ + ("你好世界", "你好", &[0, 1], BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1)), + (" 你好世界", "你好", &[1, 2], BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1)), + ]); + assert_matches(&[FuzzyGreedy, FuzzyOptimal], true, false, false, false, &[( + "你好世界", + "你世", + &[0, 2], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER - PENALTY_GAP_START, + )]); + assert_not_matches(false, false, &[("Flibbertigibbet / イタズラっ子たち", "lying")]); } #[test] fn test_long_str() { - assert_matches( - &[FuzzyGreedy, FuzzyOptimal], - false, - false, - false, - false, - &[( - &"x".repeat(u16::MAX as usize + 1), - "xx", - &[0, 1], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1), - )], - ); + assert_matches(&[FuzzyGreedy, FuzzyOptimal], false, false, false, false, &[( + &"x".repeat(u16::MAX as usize + 1), + "xx", + &[0, 1], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1), + )]); } #[test] fn test_casing() { - assert_matches( - &[FuzzyGreedy, FuzzyOptimal], - false, - false, - false, - false, - &[ - // these two have the same score - ( - "fooBar", - "foobar", - &[0, 1, 2, 3, 4, 5], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 5), - ), - ( - "foobar", - "foobar", - &[0, 1, 2, 3, 4, 5], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 5), - ), - // these two have the same score (slightly lower than the other two: 60 instead of 70) - ( - "foo-bar", - "foobar", - &[0, 1, 2, 4, 5, 6], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - PENALTY_GAP_START - + BONUS_BOUNDARY * 3, - ), - ( - "foo_bar", - "foobar", - &[0, 1, 2, 4, 5, 6], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - PENALTY_GAP_START - + BONUS_BOUNDARY * 3, - ), - ], - ) + assert_matches(&[FuzzyGreedy, FuzzyOptimal], false, false, false, false, &[ + // these two have the same score + ( + "fooBar", + "foobar", + &[0, 1, 2, 3, 4, 5], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 5), + ), + ( + "foobar", + "foobar", + &[0, 1, 2, 3, 4, 5], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 5), + ), + // these two have the same score (slightly lower than the other two: 60 instead of 70) + ( + "foo-bar", + "foobar", + &[0, 1, 2, 4, 5, 6], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - PENALTY_GAP_START + + BONUS_BOUNDARY * 3, + ), + ( + "foo_bar", + "foobar", + &[0, 1, 2, 4, 5, 6], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - PENALTY_GAP_START + + BONUS_BOUNDARY * 3, + ), + ]) } #[test] fn test_optimal() { - assert_matches( - &[FuzzyOptimal], - false, - false, - false, - false, - &[ - ( - "axxx xx ", - "xx", - &[5, 6], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1), - ), - ( - "SS!H", - "S!", - &[0, 2], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER - PENALTY_GAP_START - + BONUS_NON_WORD, - ), - // this case is a cool example of why our algorithm is more than fzf - // we handle this correctly detect that it's better to match - // the second f instead of the third yielding a higher score - // (despite using the same scoring function!) - ( - "xf.foo", - "xfoo", - &[0, 3, 4, 5], - BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER - - PENALTY_GAP_START - - PENALTY_GAP_EXTENSION - + BONUS_BOUNDARY * 3, - ), - ( - "xf fo", - "xfo", - &[0, 3, 4], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2) - - PENALTY_GAP_START - - PENALTY_GAP_EXTENSION, - ), - ], - ); + assert_matches(&[FuzzyOptimal], false, false, false, false, &[ + ("axxx xx ", "xx", &[5, 6], BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1)), + ( + "SS!H", + "S!", + &[0, 2], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER - PENALTY_GAP_START + BONUS_NON_WORD, + ), + // this case is a cool example of why our algorithm is more than fzf + // we handle this correctly detect that it's better to match + // the second f instead of the third yielding a higher score + // (despite using the same scoring function!) + ( + "xf.foo", + "xfoo", + &[0, 3, 4, 5], + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + - PENALTY_GAP_START + - PENALTY_GAP_EXTENSION + + BONUS_BOUNDARY * 3, + ), + ( + "xf fo", + "xfo", + &[0, 3, 4], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 2) + - PENALTY_GAP_START + - PENALTY_GAP_EXTENSION, + ), + ]); } #[test] fn test_reject() { - assert_not_matches( - true, - false, - &[ - ("你好界", "abc"), - ("你好界", "a"), - ("你好世界", "富"), - ("Só Danço Samba", "sox"), - ("fooBarbaz", "fooBarbazz"), - ("fooBarbaz", "c"), - ], - ); - assert_not_matches( - true, - true, - &[ - ("你好界", "abc"), - ("abc", "你"), - ("abc", "A"), - ("abc", "d"), - ("你好世界", "富"), - ("Só Danço Samba", "sox"), - ("fooBarbaz", "oBZ"), - ("Foo Bar Baz", "fbb"), - ("fooBarbaz", "fooBarbazz"), - ], - ); - assert_not_matches( - false, - true, - &[ - ("Só Danço Samba", "sod"), - ("Só Danço Samba", "soc"), - ("Só Danç", "So"), - ], - ); + assert_not_matches(true, false, &[ + ("你好界", "abc"), + ("你好界", "a"), + ("你好世界", "富"), + ("Só Danço Samba", "sox"), + ("fooBarbaz", "fooBarbazz"), + ("fooBarbaz", "c"), + ]); + assert_not_matches(true, true, &[ + ("你好界", "abc"), + ("abc", "你"), + ("abc", "A"), + ("abc", "d"), + ("你好世界", "富"), + ("Só Danço Samba", "sox"), + ("fooBarbaz", "oBZ"), + ("Foo Bar Baz", "fbb"), + ("fooBarbaz", "fooBarbazz"), + ]); + assert_not_matches(false, true, &[ + ("Só Danço Samba", "sod"), + ("Só Danço Samba", "soc"), + ("Só Danç", "So"), + ]); assert_not_matches(false, false, &[("ۂۂfoۂۂ", "foo")]); } #[test] fn test_prefer_prefix() { - assert_matches( - &[FuzzyOptimal, FuzzyGreedy], - false, - false, - false, - true, - &[ - ( - "Moby Dick", - "md", - &[0, 5], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1) + MAX_PREFIX_BONUS - - PENALTY_GAP_START - - 3 * PENALTY_GAP_EXTENSION, - ), - ( - "Though I cannot tell why it was exactly that those stage managers, the Fates, put me down for this shabby part of a whaling voyage", - "md", - &[82, 85], - BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1) - - PENALTY_GAP_START - - PENALTY_GAP_EXTENSION, - ), - ], - ); + assert_matches(&[FuzzyOptimal, FuzzyGreedy], false, false, false, true, &[ + ( + "Moby Dick", + "md", + &[0, 5], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1) + MAX_PREFIX_BONUS + - PENALTY_GAP_START + - 3 * PENALTY_GAP_EXTENSION, + ), + ( + "Though I cannot tell why it was exactly that those stage managers, the Fates, put me \ + down for this shabby part of a whaling voyage", + "md", + &[82, 85], + BONUS_BOUNDARY_WHITE * (BONUS_FIRST_CHAR_MULTIPLIER + 1) + - PENALTY_GAP_START + - PENALTY_GAP_EXTENSION, + ), + ]); } #[test] fn test_single_char_needle() { - assert_matches( - &[FuzzyOptimal], - false, - false, - false, - false, - &[( - "foO", - "o", - &[2], - BONUS_FIRST_CHAR_MULTIPLIER * BONUS_CAMEL123, - )], - ); - assert_matches( - &[FuzzyOptimal], - false, - false, - false, - false, - &[( - "föÖ", - "ö", - &[2], - BONUS_FIRST_CHAR_MULTIPLIER * BONUS_CAMEL123, - )], - ); + assert_matches(&[FuzzyOptimal], false, false, false, false, &[( + "foO", + "o", + &[2], + BONUS_FIRST_CHAR_MULTIPLIER * BONUS_CAMEL123, + )]); + assert_matches(&[FuzzyOptimal], false, false, false, false, &[( + "föÖ", + "ö", + &[2], + BONUS_FIRST_CHAR_MULTIPLIER * BONUS_CAMEL123, + )]); } #[test] diff --git a/crates/atuin-nucleo/matcher/src/utf32_str.rs b/crates/atuin-nucleo/matcher/src/utf32_str.rs index 77bd9d51f..cf0fb2282 100644 --- a/crates/atuin-nucleo/matcher/src/utf32_str.rs +++ b/crates/atuin-nucleo/matcher/src/utf32_str.rs @@ -155,14 +155,12 @@ impl<'a> Utf32Str<'a> { #[inline] pub(crate) fn leading_white_space(self) -> usize { match self { - Utf32Str::Ascii(bytes) => bytes - .iter() - .position(|b| !b.is_ascii_whitespace()) - .unwrap_or(0), - Utf32Str::Unicode(codepoints) => codepoints - .iter() - .position(|c| !c.is_whitespace()) - .unwrap_or(0), + Utf32Str::Ascii(bytes) => { + bytes.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(0) + } + Utf32Str::Unicode(codepoints) => { + codepoints.iter().position(|c| !c.is_whitespace()).unwrap_or(0) + } } } @@ -170,16 +168,12 @@ impl<'a> Utf32Str<'a> { #[inline] pub(crate) fn trailing_white_space(self) -> usize { match self { - Utf32Str::Ascii(bytes) => bytes - .iter() - .rev() - .position(|b| !b.is_ascii_whitespace()) - .unwrap_or(0), - Utf32Str::Unicode(codepoints) => codepoints - .iter() - .rev() - .position(|c| !c.is_whitespace()) - .unwrap_or(0), + Utf32Str::Ascii(bytes) => { + bytes.iter().rev().position(|b| !b.is_ascii_whitespace()).unwrap_or(0) + } + Utf32Str::Unicode(codepoints) => { + codepoints.iter().rev().position(|c| !c.is_whitespace()).unwrap_or(0) + } } } diff --git a/crates/atuin-nucleo/src/boxcar.rs b/crates/atuin-nucleo/src/boxcar.rs index 271986db1..94a17d95c 100644 --- a/crates/atuin-nucleo/src/boxcar.rs +++ b/crates/atuin-nucleo/src/boxcar.rs @@ -78,9 +78,7 @@ impl Vec { /// Returns the number of elements in the vector. #[inline] pub fn count(&self) -> u32 { - self.inflight - .load(Ordering::Acquire) - .min(MAX_ENTRIES as u64) as u32 + self.inflight.load(Ordering::Acquire).min(MAX_ENTRIES as u64) as u32 } // Returns a reference to the element at the given index. @@ -130,10 +128,7 @@ impl Vec { let entry = Bucket::::get(entries, location.entry, self.columns); // safety: the entry is active - (*entry) - .active - .load(Ordering::Acquire) - .then(|| Entry::read(entry, self.columns)) + (*entry).active.load(Ordering::Acquire).then(|| Entry::read(entry, self.columns)) } } @@ -188,10 +183,7 @@ impl Vec { where I: IntoIterator + ExactSizeIterator, { - let count: u32 = values - .len() - .try_into() - .expect("overflowed maximum capacity"); + let count: u32 = values.len().try_into().expect("overflowed maximum capacity"); if count == 0 { assert!( values.into_iter().next().is_none(), @@ -292,10 +284,7 @@ impl Vec { /// the iterator is deterministically sized and will not grow /// as more elements are pushed pub unsafe fn snapshot(&self, start: u32) -> Iter<'_, T> { - let end = self - .inflight - .load(Ordering::Acquire) - .min(MAX_ENTRIES as u64) as u32; + let end = self.inflight.load(Ordering::Acquire).min(MAX_ENTRIES as u64) as u32; assert!(start <= end, "index {start} is out of bounds!"); Iter { location: Location::of(start), @@ -309,10 +298,7 @@ impl Vec { /// the iterator is deterministically sized and will not grow /// as more elements are pushed pub unsafe fn par_snapshot(&self, start: u32) -> ParIter<'_, T> { - let end = self - .inflight - .load(Ordering::Acquire) - .min(MAX_ENTRIES as u64) as u32; + let end = self.inflight.load(Ordering::Acquire).min(MAX_ENTRIES as u64) as u32; assert!(start <= end, "index {start} is out of bounds!"); ParIter { @@ -355,10 +341,7 @@ impl Iter<'_, T> { impl<'v, T> Iterator for Iter<'v, T> { type Item = SnapshotItem<'v, T>; fn size_hint(&self) -> (usize, Option) { - ( - (self.end - self.idx) as usize, - Some((self.end - self.idx) as usize), - ) + ((self.end - self.idx) as usize, Some((self.end - self.idx) as usize)) } fn next(&mut self) -> Option> { @@ -566,10 +549,7 @@ impl Entry { fn layout(cols: u32) -> Layout { let head = Layout::new::(); let tail = Layout::array::(cols as usize).expect("invalid memory layout"); - head.extend(tail) - .expect("invalid memory layout") - .0 - .pad_to_align() + head.extend(tail).expect("invalid memory layout").0.pad_to_align() } unsafe fn matcher_cols_raw<'a>( diff --git a/crates/atuin-nucleo/src/lib.rs b/crates/atuin-nucleo/src/lib.rs index efc7628c6..d59dd7c5f 100644 --- a/crates/atuin-nucleo/src/lib.rs +++ b/crates/atuin-nucleo/src/lib.rs @@ -27,8 +27,8 @@ changes in the future. */ use std::ops::{Bound, RangeBounds}; -use std::sync::atomic::{self, AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool, Ordering}; use std::time::Duration; /// A filter predicate that determines whether an item should be included in matching. @@ -40,12 +40,12 @@ pub type Filter = Arc bool + Send + Sync>; /// Returns the combined/external score used for sorting results. pub type Scorer = Arc u32 + Send + Sync>; +pub use atuin_nucleo_matcher::{Config, Matcher, Utf32Str, Utf32String, chars}; use parking_lot::Mutex; use rayon::ThreadPool; use crate::pattern::MultiPattern; use crate::worker::Worker; -pub use atuin_nucleo_matcher::{chars, Config, Matcher, Utf32Str, Utf32String}; mod boxcar; mod par_sort; @@ -213,9 +213,7 @@ impl Snapshot { Bound::Excluded(&end) => end as usize, Bound::Unbounded => self.matches.len(), }; - self.matches[start..end] - .iter() - .map(|&m| unsafe { self.items.get_unchecked(m.idx) }) + self.matches[start..end].iter().map(|&m| unsafe { self.items.get_unchecked(m.idx) }) } /// Returns a reference to the item at the given index. @@ -502,8 +500,7 @@ impl Nucleo { if cleared { inner.items = self.items.clone(); } - self.pool - .spawn(move || unsafe { inner.run(status, cleared) }) + self.pool.spawn(move || unsafe { inner.run(status, cleared) }) } Status { changed, running } } diff --git a/crates/atuin-nucleo/src/par_sort.rs b/crates/atuin-nucleo/src/par_sort.rs index 92f716cc3..ec97b8216 100644 --- a/crates/atuin-nucleo/src/par_sort.rs +++ b/crates/atuin-nucleo/src/par_sort.rs @@ -31,10 +31,9 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use std::cmp; use std::mem::{self, MaybeUninit}; -use std::ptr; use std::sync::atomic::{self, AtomicBool}; +use std::{cmp, ptr}; /// When dropped, copies from `src` into `dest`. struct CopyOnDrop { @@ -564,10 +563,7 @@ where } } - ( - l + partition_in_blocks(&mut v[l..r], pivot, is_less), - l >= r, - ) + (l + partition_in_blocks(&mut v[l..r], pivot, is_less), l >= r) // `_pivot_guard` goes out of scope and writes the pivot (which is a stack-allocated // variable) back into the slice where it originally was. This step is critical in ensuring diff --git a/crates/atuin-nucleo/src/pattern.rs b/crates/atuin-nucleo/src/pattern.rs index a96632747..0534149fb 100644 --- a/crates/atuin-nucleo/src/pattern.rs +++ b/crates/atuin-nucleo/src/pattern.rs @@ -52,19 +52,13 @@ impl MultiPattern { let old_status = self.cols[column].1; if append && old_status != Status::Rescore - && self.cols[column] - .0 - .atoms - .last() - .is_none_or(|last| !last.negative) + && self.cols[column].0.atoms.last().is_none_or(|last| !last.negative) { self.cols[column].1 = Status::Update; } else { self.cols[column].1 = Status::Rescore; } - self.cols[column] - .0 - .reparse(new_text, case_matching, normalization); + self.cols[column].0.reparse(new_text, case_matching, normalization); } pub fn column_pattern(&self, column: usize) -> &Pattern { @@ -72,11 +66,7 @@ impl MultiPattern { } pub(crate) fn status(&self) -> Status { - self.cols - .iter() - .map(|&(_, status)| status) - .max() - .unwrap_or(Status::Unchanged) + self.cols.iter().map(|&(_, status)| status).max().unwrap_or(Status::Unchanged) } pub(crate) fn reset_status(&mut self) { diff --git a/crates/atuin-nucleo/src/tests.rs b/crates/atuin-nucleo/src/tests.rs index 1052264a4..97cd0bbef 100644 --- a/crates/atuin-nucleo/src/tests.rs +++ b/crates/atuin-nucleo/src/tests.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use atuin_nucleo_matcher::Config; -use crate::{pattern, Nucleo}; +use crate::{Nucleo, pattern}; #[test] fn active_injector_count() { @@ -83,11 +83,7 @@ fn filter_excludes_items() { assert_eq!(nucleo.snapshot().matched_item_count(), 2); // Verify the items are correct - let items: Vec<_> = nucleo - .snapshot() - .matched_items(..) - .map(|i| i.data.text.clone()) - .collect(); + let items: Vec<_> = nucleo.snapshot().matched_items(..).map(|i| i.data.text.clone()).collect(); assert!(items.contains(&"apple".to_string())); assert!(items.contains(&"avocado".to_string())); assert!(!items.contains(&"apricot".to_string())); @@ -141,17 +137,11 @@ fn scorer_affects_sort_order() { assert_eq!(nucleo.snapshot().matched_item_count(), 3); // Set scorer that uses priority as the score (ignoring fuzzy score) - nucleo.set_scorer(Some(Arc::new(|item: &TestItem, _fuzzy_score| { - item.priority - }))); + nucleo.set_scorer(Some(Arc::new(|item: &TestItem, _fuzzy_score| item.priority))); // Search again - should be sorted by priority (high to low) while nucleo.tick(10).running {} - let items: Vec<_> = nucleo - .snapshot() - .matched_items(..) - .map(|i| i.data.clone()) - .collect(); + let items: Vec<_> = nucleo.snapshot().matched_items(..).map(|i| i.data.clone()).collect(); assert_eq!(items.len(), 3); assert_eq!(items[0].text, "blueberry"); // priority 100 assert_eq!(items[1].text, "blackberry"); // priority 50 @@ -208,11 +198,7 @@ fn filter_and_scorer_combined() { while nucleo.tick(10).running {} // Should have 2 items (cherry, coconut) sorted by priority - let items: Vec<_> = nucleo - .snapshot() - .matched_items(..) - .map(|i| i.data.clone()) - .collect(); + let items: Vec<_> = nucleo.snapshot().matched_items(..).map(|i| i.data.clone()).collect(); assert_eq!(items.len(), 2); assert_eq!(items[0].text, "coconut"); // priority 50 assert_eq!(items[1].text, "cherry"); // priority 10 @@ -241,9 +227,7 @@ fn scorer_combines_with_fuzzy_score() { ); // Set scorer that combines fuzzy score with priority - nucleo.set_scorer(Some(Arc::new(|item: &TestItem, fuzzy_score| { - fuzzy_score + item.priority - }))); + nucleo.set_scorer(Some(Arc::new(|item: &TestItem, fuzzy_score| fuzzy_score + item.priority))); nucleo.pattern.reparse( 0, diff --git a/crates/atuin-nucleo/src/worker.rs b/crates/atuin-nucleo/src/worker.rs index 45e27ceeb..855558492 100644 --- a/crates/atuin-nucleo/src/worker.rs +++ b/crates/atuin-nucleo/src/worker.rs @@ -1,15 +1,16 @@ use std::cell::UnsafeCell; use std::mem::take; -use std::sync::atomic::{self, AtomicBool, AtomicU32}; use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool, AtomicU32}; use atuin_nucleo_matcher::Config; use parking_lot::Mutex; -use rayon::{prelude::*, ThreadPool}; +use rayon::ThreadPool; +use rayon::prelude::*; use crate::par_sort::par_quicksort; use crate::pattern::{self, MultiPattern}; -use crate::{boxcar, Filter, Match, Scorer}; +use crate::{Filter, Match, Scorer, boxcar}; struct Matchers(Box<[UnsafeCell]>); @@ -299,8 +300,7 @@ impl Worker { if canceled { self.was_canceled = true; } else { - self.matches - .truncate(self.matches.len() - take(unmatched.get_mut()) as usize); + self.matches.truncate(self.matches.len() - take(unmatched.get_mut()) as usize); if self.should_notify.load(atomic::Ordering::Relaxed) { (self.notify)(); } @@ -327,16 +327,10 @@ impl Worker { // array here which involves some pointer chasing let item1 = self.items.get_unchecked(match1.idx); let item2 = &self.items.get_unchecked(match2.idx); - let len1: u32 = item1 - .matcher_columns - .iter() - .map(|haystack| haystack.len() as u32) - .sum(); - let len2 = item2 - .matcher_columns - .iter() - .map(|haystack| haystack.len() as u32) - .sum(); + let len1: u32 = + item1.matcher_columns.iter().map(|haystack| haystack.len() as u32).sum(); + let len2 = + item2.matcher_columns.iter().map(|haystack| haystack.len() as u32).sum(); if len1 == len2 { if self.reverse_items { match2.idx < match1.idx @@ -392,12 +386,11 @@ impl Worker { } } else { // No filter - add all items - self.matches - .extend((0..self.last_snapshot).map(|idx| Match { - score: 0, - external_score: 0, - idx, - })); + self.matches.extend((0..self.last_snapshot).map(|idx| Match { + score: 0, + external_score: 0, + idx, + })); } // there are usually only very few in flight items (one for each writer) self.remove_in_flight_matches(); diff --git a/crates/atuin-pty-proxy/src/capture.rs b/crates/atuin-pty-proxy/src/capture.rs index ae2680ea5..dcbb37efd 100644 --- a/crates/atuin-pty-proxy/src/capture.rs +++ b/crates/atuin-pty-proxy/src/capture.rs @@ -55,8 +55,7 @@ impl CommandCaptureTracker { pub(crate) fn push(&mut self, data: &[u8], mut on_capture: impl FnMut(CommandCapture)) { let mut events = Vec::new(); - self.parser - .push_located(data, |located| events.push(located)); + self.parser.push_located(data, |located| events.push(located)); let mut start = 0; for located in events { @@ -71,9 +70,7 @@ impl CommandCaptureTracker { let append_end = self .parser .incomplete_osc_sequence_start() - .map_or(data.len(), |sequence_start| { - sequence_start.min(data.len()).max(start) - }); + .map_or(data.len(), |sequence_start| sequence_start.min(data.len()).max(start)); if start < append_end { self.append(&data[start..append_end]); } @@ -89,10 +86,8 @@ impl CommandCaptureTracker { } fn append_output(&mut self, data: &[u8]) { - self.buffers.output_observed_bytes = self - .buffers - .output_observed_bytes - .saturating_add(data.len() as u64); + self.buffers.output_observed_bytes = + self.buffers.output_observed_bytes.saturating_add(data.len() as u64); if self.buffers.output_truncated { return; @@ -172,10 +167,10 @@ impl CommandCaptureTracker { #[cfg(test)] mod tests { - use super::*; - use rstest::{fixture, rstest}; + use super::*; + #[fixture] fn tracker(#[default(80)] cols: u16) -> CommandCaptureTracker { CommandCaptureTracker::new(Arc::new(AtomicU16::new(cols))) @@ -183,9 +178,7 @@ mod tests { fn assert_no_terminal_controls(text: &str) { assert!( - !text - .chars() - .any(|ch| ch.is_control() && ch != '\n' && ch != '\t'), + !text.chars().any(|ch| ch.is_control() && ch != '\n' && ch != '\t'), "text still contains terminal controls: {text:?}" ); } @@ -255,19 +248,16 @@ mod tests { }, ); - assert_eq!( - captures, - vec![CommandCapture { - prompt: "%".to_string(), - command: "ls".to_string(), - output: "file".to_string(), - exit_code: Some(1), - history_id: Some("hist".to_string()), - session_id: Some("sess".to_string()), - output_truncated: false, - output_observed_bytes: 15, - }] - ); + assert_eq!(captures, vec![CommandCapture { + prompt: "%".to_string(), + command: "ls".to_string(), + output: "file".to_string(), + exit_code: Some(1), + history_id: Some("hist".to_string()), + session_id: Some("sess".to_string()), + output_truncated: false, + output_observed_bytes: 15, + }]); } #[rstest] @@ -321,9 +311,7 @@ mod tests { tracker.push(b"\x1b]133;C\x07line one\r\n\x1b]133;D;0\x07", |capture| { captures.push(capture); }); - tracker.push(b"\x1b]133;D;0;history_id=018f", |capture| { - captures.push(capture) - }); + tracker.push(b"\x1b]133;D;0;history_id=018f", |capture| captures.push(capture)); assert!(captures.is_empty()); @@ -340,12 +328,9 @@ mod tests { fn split_finish_marker_is_not_counted_as_output(mut tracker: CommandCaptureTracker) { let mut captures = Vec::new(); - tracker.push( - b"\x1b]133;C\x07line one\r\n\x1b]133;D;0;history_id=018f", - |capture| { - captures.push(capture); - }, - ); + tracker.push(b"\x1b]133;C\x07line one\r\n\x1b]133;D;0;history_id=018f", |capture| { + captures.push(capture); + }); assert!(captures.is_empty()); tracker.push(b";session_id=abcd\x07", |capture| captures.push(capture)); @@ -366,10 +351,7 @@ mod tests { assert_eq!(captures.len(), 1); assert!(captures[0].output_truncated); - assert_eq!( - captures[0].output_observed_bytes, - (MAX_OUTPUT_CAPTURE_BYTES + 10) as u64 - ); + assert_eq!(captures[0].output_observed_bytes, (MAX_OUTPUT_CAPTURE_BYTES + 10) as u64); } #[rstest] diff --git a/crates/atuin-pty-proxy/src/debug.rs b/crates/atuin-pty-proxy/src/debug.rs index 806bde904..3171e117f 100644 --- a/crates/atuin-pty-proxy/src/debug.rs +++ b/crates/atuin-pty-proxy/src/debug.rs @@ -15,8 +15,7 @@ impl Osc133DebugHighlighter { pub(crate) fn render(&mut self, data: &[u8]) -> Vec { let mut events = Vec::new(); - self.parser - .push_located(data, |located| events.push(located)); + self.parser.push_located(data, |located| events.push(located)); if events.is_empty() { return data.to_vec(); diff --git a/crates/atuin-pty-proxy/src/osc133.rs b/crates/atuin-pty-proxy/src/osc133.rs index 801830b2a..80cbdb5f2 100644 --- a/crates/atuin-pty-proxy/src/osc133.rs +++ b/crates/atuin-pty-proxy/src/osc133.rs @@ -72,7 +72,10 @@ pub enum Param { /// A positional parameter without an equals sign. Value(String), /// A `key=value` parameter. - KeyValue { key: String, value: String }, + KeyValue { + key: String, + value: String, + }, } /// An OSC 133 event with its position in the most recent input chunk. @@ -329,9 +332,7 @@ fn parse_exit_code(code: &[u8]) -> Option { return None; } - std::str::from_utf8(code) - .ok() - .and_then(|code| code.parse::().ok()) + std::str::from_utf8(code).ok().and_then(|code| code.parse::().ok()) } fn parse_params(metadata: &[u8]) -> Params { @@ -363,9 +364,10 @@ fn parse_param(param: &[u8]) -> Param { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + /// Collect all events from a single `push` call. fn parse_events(data: &[u8]) -> Vec { let mut parser = Parser::new(); @@ -421,15 +423,12 @@ mod tests { parser.push(b"\x1b]133;D;0\x07", |e| events.push(e)); assert_eq!(parser.zone(), Zone::Unknown); - assert_eq!( - events, - vec![ - Event::PromptStart, - Event::CommandStart, - Event::CommandExecuted, - Event::CommandFinished { exit_code: Some(0) }, - ] - ); + assert_eq!(events, vec![ + Event::PromptStart, + Event::CommandStart, + Event::CommandExecuted, + Event::CommandFinished { exit_code: Some(0) }, + ]); } // -- Multiple events / interleaved text in one push ----------------------- @@ -506,16 +505,13 @@ mod tests { let data = b"\x1b]133;A\x07$ \x1b]133;B\x07\x1b]133;D\x07\x1b]133;A\x07$ \x1b]133;B\x07"; parser.push(data, |e| events.push(e)); - assert_eq!( - events, - vec![ - Event::PromptStart, - Event::CommandStart, - Event::CommandFinished { exit_code: None }, - Event::PromptStart, - Event::CommandStart, - ] - ); + assert_eq!(events, vec![ + Event::PromptStart, + Event::CommandStart, + Event::CommandFinished { exit_code: None }, + Event::PromptStart, + Event::CommandStart, + ]); assert_eq!(parser.zone(), Zone::Input); } @@ -531,12 +527,9 @@ mod tests { parser.push(&[byte], |e| events.push(e)); } - assert_eq!( - events, - vec![Event::CommandFinished { - exit_code: Some(99) - }] - ); + assert_eq!(events, vec![Event::CommandFinished { + exit_code: Some(99) + }]); } // -- Located event offsets ------------------------------------------------ @@ -549,16 +542,13 @@ mod tests { parser.push_located(data, |e| events.push(e)); - assert_eq!( - events, - vec![LocatedEvent { - event: Event::PromptStart, - start_offset: b"before".len(), - offset: b"before\x1b]133;A\x07".len(), - zone: Zone::Prompt, - params: Params::default(), - }] - ); + assert_eq!(events, vec![LocatedEvent { + event: Event::PromptStart, + start_offset: b"before".len(), + offset: b"before\x1b]133;A\x07".len(), + zone: Zone::Prompt, + params: Params::default(), + }]); } #[test] @@ -569,18 +559,15 @@ mod tests { parser.push_located(b"\x1b]133;", |e| events.push(e)); parser.push_located(b"D;42\x07after", |e| events.push(e)); - assert_eq!( - events, - vec![LocatedEvent { - event: Event::CommandFinished { - exit_code: Some(42) - }, - start_offset: 0, - offset: b"D;42\x07".len(), - zone: Zone::Unknown, - params: Params::default(), - }] - ); + assert_eq!(events, vec![LocatedEvent { + event: Event::CommandFinished { + exit_code: Some(42) + }, + start_offset: 0, + offset: b"D;42\x07".len(), + zone: Zone::Unknown, + params: Params::default(), + }]); } #[test] @@ -588,27 +575,18 @@ mod tests { let mut parser = Parser::new(); let mut events = Vec::new(); - parser.push_located( - b"\x1b]133;D;127;history_id=018f;session_id=abcd;flag\x07", - |event| events.push(event), - ); + parser.push_located(b"\x1b]133;D;127;history_id=018f;session_id=abcd;flag\x07", |event| { + events.push(event) + }); assert_eq!(events.len(), 1); let event = &events[0]; - assert_eq!( - event.event, - Event::CommandFinished { - exit_code: Some(127) - } - ); + assert_eq!(event.event, Event::CommandFinished { + exit_code: Some(127) + }); assert_eq!(event.params.get("history_id"), Some("018f")); assert_eq!(event.params.get("session_id"), Some("abcd")); - assert!( - event - .params - .iter() - .any(|param| param == &Param::Value("flag".to_string())) - ); + assert!(event.params.iter().any(|param| param == &Param::Value("flag".to_string()))); } #[test] diff --git a/crates/atuin-pty-proxy/src/protocol.rs b/crates/atuin-pty-proxy/src/protocol.rs index 19efdb4c7..cfbcb5780 100644 --- a/crates/atuin-pty-proxy/src/protocol.rs +++ b/crates/atuin-pty-proxy/src/protocol.rs @@ -119,10 +119,7 @@ pub fn classify_greeting(first: &[u8]) -> Greeting { /// sizes (PTY read chunks and screen snapshots) and must cap them first. #[must_use] pub fn encode_frame(frame_type: u8, payload: &[u8]) -> Vec { - assert!( - payload.len() <= MAX_FRAME_LEN, - "frame payload exceeds MAX_FRAME_LEN" - ); + assert!(payload.len() <= MAX_FRAME_LEN, "frame payload exceeds MAX_FRAME_LEN"); let len = u32::try_from(payload.len()).expect("payload length fits in u32"); let mut buf = Vec::with_capacity(5 + payload.len()); buf.push(frame_type); @@ -149,10 +146,7 @@ pub fn read_frame(reader: &mut impl Read) -> io::Result)>> { let frame_type = header[0]; let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize; if len > MAX_FRAME_LEN { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "frame exceeds maximum length", - )); + return Err(io::Error::new(io::ErrorKind::InvalidData, "frame exceeds maximum length")); } let mut payload = vec![0u8; len]; reader.read_exact(&mut payload)?; diff --git a/crates/atuin-pty-proxy/src/pty_proxy.rs b/crates/atuin-pty-proxy/src/pty_proxy.rs index 396b26882..dc95c77c7 100644 --- a/crates/atuin-pty-proxy/src/pty_proxy.rs +++ b/crates/atuin-pty-proxy/src/pty_proxy.rs @@ -121,20 +121,13 @@ fn detect_shell(cli_shell: Option) -> Result { return Ok(shell); } - Err( - "could not detect a supported shell. Please specify one explicitly: bash, zsh, fish, or nu" - .to_string(), - ) + Err("could not detect a supported shell. Please specify one explicitly: bash, zsh, fish, or nu" + .to_string()) } fn shell_from_name(name: &str) -> Option { - let shell = name - .trim() - .rsplit('/') - .next() - .unwrap_or(name) - .trim_start_matches('-') - .to_ascii_lowercase(); + let shell = + name.trim().rsplit('/').next().unwrap_or(name).trim_start_matches('-').to_ascii_lowercase(); match shell.as_str() { "bash" => Some(Shell::Bash), @@ -147,10 +140,7 @@ fn shell_from_name(name: &str) -> Option { fn env_flag(name: &str) -> bool { std::env::var(name).is_ok_and(|value| { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "on" - ) + matches!(value.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on") }) } diff --git a/crates/atuin-pty-proxy/src/runtime.rs b/crates/atuin-pty-proxy/src/runtime.rs index 1a520c69f..a2f7accac 100644 --- a/crates/atuin-pty-proxy/src/runtime.rs +++ b/crates/atuin-pty-proxy/src/runtime.rs @@ -67,21 +67,12 @@ fn run(options: RuntimeOptions) -> eyre::Result<()> { cmd.umask(Some(mask as _)); } - let mut child = pair - .slave - .spawn_command(cmd) - .map_err(|e| eyre::eyre!("{e:#}"))?; + let mut child = pair.slave.spawn_command(cmd).map_err(|e| eyre::eyre!("{e:#}"))?; drop(pair.slave); - let pty_reader = pair - .master - .try_clone_reader() - .map_err(|e| eyre::eyre!("{e:#}"))?; - let pty_writer = pair - .master - .take_writer() - .map_err(|e| eyre::eyre!("{e:#}"))?; + let pty_reader = pair.master.try_clone_reader().map_err(|e| eyre::eyre!("{e:#}"))?; + let pty_writer = pair.master.take_writer().map_err(|e| eyre::eyre!("{e:#}"))?; let mut core = ProxyCore::spawn(ProxyCoreConfig { reader: pty_reader, @@ -273,9 +264,8 @@ fn spawn_output_pump(pump: OutputPump) -> JoinHandle<()> { } = pump; let mut highlighter = debug_osc133.then(Osc133DebugHighlighter::new); - let mut capture_tracker = command_capture_sink - .as_ref() - .map(|_| CommandCaptureTracker::new(current_cols)); + let mut capture_tracker = + command_capture_sink.as_ref().map(|_| CommandCaptureTracker::new(current_cols)); let mut buf = [0u8; 8192]; loop { @@ -387,9 +377,10 @@ fn process_exit_code(code: u32) -> i32 { #[cfg(test)] mod tests { - use super::process_exit_code; use rstest::rstest; + use super::process_exit_code; + #[rstest] #[case::zero(0, 0)] #[case::mid_range(127, 127)] diff --git a/crates/atuin-pty-proxy/src/screen.rs b/crates/atuin-pty-proxy/src/screen.rs index ea64c5e09..1db53f020 100644 --- a/crates/atuin-pty-proxy/src/screen.rs +++ b/crates/atuin-pty-proxy/src/screen.rs @@ -140,11 +140,7 @@ pub(crate) fn fallback_proxy_dir() -> PathBuf { fn proxy_dir_name() -> String { let mut suffix = [0u8; 4]; rand::thread_rng().fill_bytes(&mut suffix); - format!( - "atuin-pty-proxy-{}-{}", - std::process::id(), - hex_encode(&suffix) - ) + format!("atuin-pty-proxy-{}-{}", std::process::id(), hex_encode(&suffix)) } pub(crate) fn socket_path_in(dir: &Path) -> PathBuf { @@ -379,8 +375,7 @@ impl ParserState { /// whose connection is gone. Never blocks (see the invariant on /// [`spawn_parser_thread`]). fn fan_out(&mut self, frame: &[u8]) { - self.subscribers - .retain(|(_, subscriber)| subscriber.try_send(frame.to_vec()).is_ok()); + self.subscribers.retain(|(_, subscriber)| subscriber.try_send(frame.to_vec()).is_ok()); } } @@ -679,25 +674,18 @@ fn write_frames(mut stream: UnixStream, frames_rx: &Receiver>) { let _ = stream.shutdown(std::net::Shutdown::Both); } -#[cfg(any( - target_os = "macos", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly" -))] +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "dragonfly"))] fn peer_uid(stream: &UnixStream) -> Option { - use nix::sys::socket::{getsockopt, sockopt::LocalPeerCred}; - getsockopt(stream, LocalPeerCred) - .ok() - .map(|cred| cred.uid()) + use nix::sys::socket::getsockopt; + use nix::sys::socket::sockopt::LocalPeerCred; + getsockopt(stream, LocalPeerCred).ok().map(|cred| cred.uid()) } #[cfg(any(target_os = "linux", target_os = "android"))] fn peer_uid(stream: &UnixStream) -> Option { - use nix::sys::socket::{getsockopt, sockopt::PeerCredentials}; - getsockopt(stream, PeerCredentials) - .ok() - .map(|cred| cred.uid()) + use nix::sys::socket::getsockopt; + use nix::sys::socket::sockopt::PeerCredentials; + getsockopt(stream, PeerCredentials).ok().map(|cred| cred.uid()) } #[cfg(not(any( @@ -720,9 +708,10 @@ fn peer_uid_matches(stream: &UnixStream) -> bool { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[test] fn zero_dimension_screens_do_not_panic() { // Debug builds have overflow checks; vt100 0.16 computes @@ -821,10 +810,7 @@ mod tests { // Drain everything that was queued before the drop... while frames_rx.try_recv().is_ok() {} // ...after which the channel reports disconnection. - assert!(matches!( - frames_rx.try_recv(), - Err(mpsc::TryRecvError::Disconnected) - )); + assert!(matches!(frames_rx.try_recv(), Err(mpsc::TryRecvError::Disconnected))); } /// The registry is capped: a same-uid client cannot accumulate @@ -915,17 +901,11 @@ mod tests { } // A signal or a peer that gave up between connect and accept. for code in [libc::EINTR, libc::ECONNABORTED] { - assert!( - matches!(classify(code), AcceptFailure::Retry), - "errno {code}" - ); + assert!(matches!(classify(code), AcceptFailure::Retry), "errno {code}"); } // The listener itself is gone: nothing left to accept. for code in [libc::EBADF, libc::EINVAL, libc::ENOTSOCK] { - assert!( - matches!(classify(code), AcceptFailure::Fatal), - "errno {code}" - ); + assert!(matches!(classify(code), AcceptFailure::Fatal), "errno {code}"); } } @@ -939,10 +919,7 @@ mod tests { .map(|_| ConnectionSlot::acquire(&live).expect("under the cap")) .collect(); assert_eq!(live.load(Ordering::Acquire), MAX_LIVE_CONNECTIONS); - assert!( - ConnectionSlot::acquire(&live).is_none(), - "the cap must hold" - ); + assert!(ConnectionSlot::acquire(&live).is_none(), "the cap must hold"); held.pop(); assert_eq!(live.load(Ordering::Acquire), MAX_LIVE_CONNECTIONS - 1); diff --git a/crates/atuin-pty-proxy/tests/fd_pressure.rs b/crates/atuin-pty-proxy/tests/fd_pressure.rs index d31dd7f6b..95ffa73ac 100644 --- a/crates/atuin-pty-proxy/tests/fd_pressure.rs +++ b/crates/atuin-pty-proxy/tests/fd_pressure.rs @@ -55,10 +55,7 @@ fn spawn_proxy(rows: u16, cols: u16) -> TestProxy { pixel_height: 0, }) .expect("openpty"); - let child = pair - .slave - .spawn_command(CommandBuilder::new("/bin/cat")) - .expect("spawn /bin/cat"); + let child = pair.slave.spawn_command(CommandBuilder::new("/bin/cat")).expect("spawn /bin/cat"); drop(pair.slave); let reader = pair.master.try_clone_reader().expect("clone reader"); @@ -124,9 +121,7 @@ fn legacy_snapshot(sock: &Path) -> Result, String> { .set_read_timeout(Some(CLIENT_READ_TIMEOUT)) .map_err(|e| format!("set read timeout: {e}"))?; let mut blob = Vec::new(); - stream - .read_to_end(&mut blob) - .map_err(|e| format!("read: {e}"))?; + stream.read_to_end(&mut blob).map_err(|e| format!("read: {e}"))?; Ok(blob) } @@ -162,10 +157,7 @@ fn socket_server_survives_fd_exhaustion() { let proxy = spawn_proxy(ROWS, COLS); paint_and_wait(&proxy, ROWS, COLS, MIN_SNAPSHOT_BYTES); - assert!( - legacy_snapshot(&proxy.sock).is_ok(), - "the server must serve before the flood" - ); + assert!(legacy_snapshot(&proxy.sock).is_ok(), "the server must serve before the flood"); // Everything that needs an fd to start is up; squeeze the process. let original_soft = set_nofile(NOFILE_LIMIT); @@ -213,21 +205,15 @@ fn socket_server_survives_fd_exhaustion() { // ...and so does a v2 subscribe, the other client of the same loop. let mut client = UnixStream::connect(&proxy.sock).expect("v2 connect after fd exhaustion"); - client - .set_read_timeout(Some(CLIENT_READ_TIMEOUT)) - .expect("set read timeout"); + client.set_read_timeout(Some(CLIENT_READ_TIMEOUT)).expect("set read timeout"); client.write_all(&protocol::MAGIC).expect("write magic"); - client - .write_all(&protocol::subscribe_frame(false, b"")) - .expect("write subscribe"); - let (frame_type, payload) = protocol::read_frame(&mut client) - .expect("read hello") - .expect("hello, not EOF"); + client.write_all(&protocol::subscribe_frame(false, b"")).expect("write subscribe"); + let (frame_type, payload) = + protocol::read_frame(&mut client).expect("read hello").expect("hello, not EOF"); assert_eq!(frame_type, protocol::FRAME_HELLO); assert!(protocol::decode_hello(&payload).is_some()); - let (frame_type, payload) = protocol::read_frame(&mut client) - .expect("read keyframe") - .expect("keyframe, not EOF"); + let (frame_type, payload) = + protocol::read_frame(&mut client).expect("read keyframe").expect("keyframe, not EOF"); assert_eq!(frame_type, protocol::FRAME_KEYFRAME); let snapshot = Snapshot::decode(&payload).expect("decode keyframe"); assert_eq!((snapshot.rows, snapshot.cols), (ROWS, COLS)); diff --git a/crates/atuin-pty-proxy/tests/subscriber.rs b/crates/atuin-pty-proxy/tests/subscriber.rs index 97d646848..7ce05da95 100644 --- a/crates/atuin-pty-proxy/tests/subscriber.rs +++ b/crates/atuin-pty-proxy/tests/subscriber.rs @@ -32,9 +32,10 @@ impl Drop for TestProxy { } fn disable_echo(master: &dyn MasterPty) { - use nix::sys::termios; use std::os::fd::BorrowedFd; + use nix::sys::termios; + let raw_fd = master.as_raw_fd().expect("MasterPty::as_raw_fd"); // SAFETY: The file descriptor is owned by `master`, which cannot get dropped during the life // of the `BorrowedFd` because we hold a shared reference to it. @@ -59,10 +60,7 @@ fn spawn_proxy(rows: u16, cols: u16) -> TestProxy { // that don't stretch the full width of the screen, causing certain tests to fail. Disable echo // so we're only testing the output of `cat`, which is reliable. disable_echo(&*pair.master); - let child = pair - .slave - .spawn_command(CommandBuilder::new("/bin/cat")) - .expect("spawn /bin/cat"); + let child = pair.slave.spawn_command(CommandBuilder::new("/bin/cat")).expect("spawn /bin/cat"); drop(pair.slave); let reader = pair.master.try_clone_reader().expect("clone reader"); @@ -103,20 +101,14 @@ struct Client { impl Client { fn connect(sock: &Path, want_input: bool, token: &[u8]) -> Self { let mut stream = UnixStream::connect(sock).expect("connect"); - stream - .set_read_timeout(Some(CLIENT_READ_TIMEOUT)) - .expect("set read timeout"); + stream.set_read_timeout(Some(CLIENT_READ_TIMEOUT)).expect("set read timeout"); stream.write_all(&protocol::MAGIC).expect("write magic"); - stream - .write_all(&protocol::subscribe_frame(want_input, token)) - .expect("write subscribe"); + stream.write_all(&protocol::subscribe_frame(want_input, token)).expect("write subscribe"); Self { stream } } fn read_frame(&mut self) -> (u8, Vec) { - protocol::read_frame(&mut self.stream) - .expect("read frame") - .expect("unexpected EOF") + protocol::read_frame(&mut self.stream).expect("read frame").expect("unexpected EOF") } fn expect_hello(&mut self, want_granted: bool) { @@ -156,10 +148,7 @@ fn authenticated_input_roundtrips_through_the_shell() { let marker = b"itest-marker-4242"; let mut line = marker.to_vec(); line.push(b'\n'); - client - .stream - .write_all(&protocol::input_frame(&line)) - .expect("write input"); + client.stream.write_all(&protocol::input_frame(&line)).expect("write input"); // Wait for `cat`'s output. let mut seen = Vec::new(); @@ -203,10 +192,7 @@ fn input_without_the_token_is_refused_and_closed() { client.expect_hello(false); client.expect_keyframe(); - client - .stream - .write_all(&protocol::input_frame(b"nope\n")) - .expect("write input"); + client.stream.write_all(&protocol::input_frame(b"nope\n")).expect("write input"); // The server must close the connection. Buffered frames may still // arrive first; a reset instead of a clean EOF also counts as closed. @@ -273,9 +259,7 @@ fn legacy_one_shot_snapshot_still_works() { let proxy = spawn_proxy(24, 80); let mut stream = UnixStream::connect(&proxy.sock).expect("connect"); - stream - .set_read_timeout(Some(CLIENT_READ_TIMEOUT)) - .expect("set read timeout"); + stream.set_read_timeout(Some(CLIENT_READ_TIMEOUT)).expect("set read timeout"); let mut blob = Vec::new(); stream.read_to_end(&mut blob).expect("read snapshot"); @@ -305,9 +289,7 @@ fn wait_for_snapshot_of_at_least(sock: &Path, min_bytes: usize) { let deadline = Instant::now() + DEADLINE; loop { let mut stream = UnixStream::connect(sock).expect("connect probe"); - stream - .set_read_timeout(Some(CLIENT_READ_TIMEOUT)) - .expect("set read timeout"); + stream.set_read_timeout(Some(CLIENT_READ_TIMEOUT)).expect("set read timeout"); let mut blob = Vec::new(); stream.read_to_end(&mut blob).expect("read snapshot"); let len = blob.len(); @@ -355,9 +337,7 @@ fn a_stalled_client_does_not_wedge_the_socket_server() { // The search popup's contract, while the staller stalls. let start = Instant::now(); let mut legacy = UnixStream::connect(&proxy.sock).expect("connect legacy"); - legacy - .set_read_timeout(Some(WEDGE_TIMEOUT)) - .expect("set read timeout"); + legacy.set_read_timeout(Some(WEDGE_TIMEOUT)).expect("set read timeout"); let mut blob = Vec::new(); legacy .read_to_end(&mut blob) @@ -373,10 +353,7 @@ fn a_stalled_client_does_not_wedge_the_socket_server() { // ...and a v2 subscribe, the other client of the same loop. let start = Instant::now(); let mut client = Client::connect(&proxy.sock, false, b""); - client - .stream - .set_read_timeout(Some(WEDGE_TIMEOUT)) - .expect("set read timeout"); + client.stream.set_read_timeout(Some(WEDGE_TIMEOUT)).expect("set read timeout"); client.expect_hello(false); let snapshot = client.expect_keyframe(); assert_eq!((snapshot.rows, snapshot.cols), (80, 200)); @@ -388,7 +365,5 @@ fn a_stalled_client_does_not_wedge_the_socket_server() { } fn contains(haystack: &[u8], needle: &[u8]) -> bool { - haystack - .windows(needle.len()) - .any(|window| window == needle) + haystack.windows(needle.len()).any(|window| window == needle) } diff --git a/crates/atuin-scripts/src/database.rs b/crates/atuin-scripts/src/database.rs index d2e1fcc9e..d078eff46 100644 --- a/crates/atuin-scripts/src/database.rs +++ b/crates/atuin-scripts/src/database.rs @@ -1,13 +1,13 @@ -use std::{path::Path, str::FromStr, time::Duration}; +use std::path::Path; +use std::str::FromStr; +use std::time::Duration; use atuin_common::utils; -use sqlx::{ - Result, Row, - sqlite::{ - SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, - SqliteSynchronous, - }, +use sqlx::sqlite::{ + SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow, + SqliteSynchronous, }; +use sqlx::{Result, Row}; use tokio::fs; use tracing::debug; use uuid::Uuid; @@ -26,7 +26,8 @@ impl Database { if utils::broken_symlink(path) { eprintln!( - "Atuin: Script sqlite db path ({path:?}) is a broken symlink. Unable to read or create replacement." + "Atuin: Script sqlite db path ({path:?}) is a broken symlink. Unable to read or \ + create replacement." ); std::process::exit(1); } @@ -57,9 +58,7 @@ impl Database { } pub async fn sqlite_version(&self) -> Result { - sqlx::query_scalar("SELECT sqlite_version()") - .fetch_one(&self.pool) - .await + sqlx::query_scalar("SELECT sqlite_version()").fetch_one(&self.pool).await } async fn setup_db(pool: &SqlitePool) -> Result<()> { @@ -193,12 +192,8 @@ impl Database { pub async fn clear(&self) -> Result<()> { debug!("clearing all scripts from sqlite"); - sqlx::query("delete from script_tags") - .execute(&self.pool) - .await?; - sqlx::query("delete from scripts") - .execute(&self.pool) - .await?; + sqlx::query("delete from script_tags").execute(&self.pool).await?; + sqlx::query("delete from scripts").execute(&self.pool).await?; Ok(()) } @@ -206,10 +201,7 @@ impl Database { pub async fn delete(&self, id: &str) -> Result<()> { debug!("deleting script {}", id); - sqlx::query("delete from scripts where id = ?1") - .bind(id) - .execute(&self.pool) - .await?; + sqlx::query("delete from scripts where id = ?1").bind(id).execute(&self.pool).await?; // delete all the tags for the script sqlx::query("delete from script_tags where script_id = ?1") @@ -226,14 +218,17 @@ impl Database { let mut tx = self.pool.begin().await?; // Update the script's base fields - sqlx::query("update scripts set name = ?1, description = ?2, shebang = ?3, script = ?4 where id = ?5") - .bind(s.name.as_str()) - .bind(s.description.as_str()) - .bind(s.shebang.as_str()) - .bind(s.script.as_str()) - .bind(s.id.to_string()) - .execute(&mut *tx) - .await?; + sqlx::query( + "update scripts set name = ?1, description = ?2, shebang = ?3, script = ?4 where id = \ + ?5", + ) + .bind(s.name.as_str()) + .bind(s.description.as_str()) + .bind(s.shebang.as_str()) + .bind(s.script.as_str()) + .bind(s.id.to_string()) + .execute(&mut *tx) + .await?; // Delete all existing tags for this script sqlx::query("delete from script_tags where script_id = ?1") @@ -284,9 +279,10 @@ impl Database { #[cfg(test)] mod test { - use super::*; use rstest::*; + use super::*; + #[fixture] async fn db() -> Database { Database::new("sqlite::memory:", 1.0).await.unwrap() diff --git a/crates/atuin-scripts/src/execution.rs b/crates/atuin-scripts/src/execution.rs index 5bf94aaa5..cec348dc3 100644 --- a/crates/atuin-scripts/src/execution.rs +++ b/crates/atuin-scripts/src/execution.rs @@ -1,13 +1,15 @@ -use crate::store::script::Script; -use eyre::Result; use std::collections::{HashMap, HashSet}; use std::process::Stdio; + +use eyre::Result; use tempfile::NamedTempFile; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::sync::mpsc; use tokio::task; use tracing::debug; +use crate::store::script::Script; + // Helper function to build a complete script with shebang pub fn build_executable_script(script: String, shebang: String) -> String { if shebang.is_empty() { @@ -136,11 +138,8 @@ pub async fn execute_script_interactive( cmd.arg(temp_path.to_str().unwrap()); // Try with the interpreter - child_result = cmd - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn(); + child_result = + cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn(); } } @@ -153,18 +152,12 @@ pub async fn execute_script_interactive( }; // Get handles to stdin, stdout, stderr - let mut stdin = child - .stdin - .take() - .ok_or_else(|| "Failed to open child process stdin".to_string())?; - let stdout = child - .stdout - .take() - .ok_or_else(|| "Failed to open child process stdout".to_string())?; - let stderr = child - .stderr - .take() - .ok_or_else(|| "Failed to open child process stderr".to_string())?; + let mut stdin = + child.stdin.take().ok_or_else(|| "Failed to open child process stdin".to_string())?; + let stdout = + child.stdout.take().ok_or_else(|| "Failed to open child process stdout".to_string())?; + let stderr = + child.stderr.take().ok_or_else(|| "Failed to open child process stderr".to_string())?; // Create channels for the interactive session let (stdin_tx, mut stdin_rx) = mpsc::channel::(32); diff --git a/crates/atuin-scripts/src/store.rs b/crates/atuin-scripts/src/store.rs index a862bfb54..f868eae56 100644 --- a/crates/atuin-scripts/src/store.rs +++ b/crates/atuin-scripts/src/store.rs @@ -1,8 +1,7 @@ -use eyre::{Result, eyre}; - use atuin_client::record::sqlite_store::SqliteStore; use atuin_common::encryption::paseto_v4; use atuin_domain::record::{Host, HostId, Record, RecordId, RecordIdx, RecordTag, RecordVersion}; +use eyre::{Result, eyre}; use record::ScriptRecord; use script::Script; @@ -29,11 +28,7 @@ impl ScriptStore { async fn push_record(&self, record: ScriptRecord) -> Result<(RecordId, RecordIdx)> { let bytes = record.serialize()?; - let idx = self - .store - .last(self.host_id, &RecordTag::Script) - .await? - .map_or(0, |p| p.idx + 1); + let idx = self.store.last(self.host_id, &RecordTag::Script).await?.map_or(0, |p| p.idx + 1); let record = Record::builder() .host(Host::new(self.host_id)) @@ -45,9 +40,7 @@ impl ScriptStore { let id = record.id; - self.store - .push(&record.encrypt(&self.encryption_key)) - .await?; + self.store.push(&record.encrypt(&self.encryption_key)).await?; Ok((id, idx)) } diff --git a/crates/atuin-scripts/src/store/record.rs b/crates/atuin-scripts/src/store/record.rs index c085e140e..9b97b3e7b 100644 --- a/crates/atuin-scripts/src/store/record.rs +++ b/crates/atuin-scripts/src/store/record.rs @@ -91,9 +91,10 @@ impl ScriptRecord { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[test] fn test_serialize_create() { let script = Script::builder() @@ -109,15 +110,12 @@ mod tests { let serialized = record.serialize().unwrap(); - assert_eq!( - serialized.0, - vec![ - 204, 0, 196, 65, 150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, - 45, 55, 57, 56, 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, - 98, 99, 54, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, - 116, 145, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116 - ] - ); + assert_eq!(serialized.0, vec![ + 204, 0, 196, 65, 150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, + 55, 57, 56, 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, + 54, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 145, + 164, 116, 101, 115, 116, 164, 116, 101, 115, 116 + ]); } #[test] @@ -128,13 +126,10 @@ mod tests { let serialized = record.serialize().unwrap(); - assert_eq!( - serialized.0, - vec![ - 204, 1, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, 55, 57, - 56, 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, 54 - ] - ); + assert_eq!(serialized.0, vec![ + 204, 1, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, 55, 57, 56, + 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, 54 + ]); } #[test] @@ -152,16 +147,12 @@ mod tests { let serialized = record.serialize().unwrap(); - assert_eq!( - serialized.0, - vec![ - 204, 2, 196, 71, 150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, - 45, 55, 57, 56, 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, - 98, 99, 54, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, - 116, 146, 164, 116, 101, 115, 116, 165, 116, 101, 115, 116, 50, 164, 116, 101, 115, - 116 - ], - ); + assert_eq!(serialized.0, vec![ + 204, 2, 196, 71, 150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, + 55, 57, 56, 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, + 54, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 146, + 164, 116, 101, 115, 116, 165, 116, 101, 115, 116, 50, 164, 116, 101, 115, 116 + ],); } #[rstest] diff --git a/crates/atuin-scripts/src/store/script.rs b/crates/atuin-scripts/src/store/script.rs index a68ba4c58..e31369a14 100644 --- a/crates/atuin-scripts/src/store/script.rs +++ b/crates/atuin-scripts/src/store/script.rs @@ -1,12 +1,9 @@ use atuin_domain::record::DecryptedData; use eyre::{Result, bail, ensure}; -use uuid::Uuid; - -use rmp::{ - decode::{self, Bytes}, - encode, -}; +use rmp::decode::{self, Bytes}; +use rmp::encode; use typed_builder::TypedBuilder; +use uuid::Uuid; pub const SCRIPT_LEN: usize = 20000; // 20kb max total len @@ -118,15 +115,12 @@ mod tests { }; let serialized = script.serialize().unwrap(); - assert_eq!( - serialized.0, - vec![ - 150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, 55, 57, 56, - 50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, 54, 164, - 116, 101, 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 145, 164, - 116, 101, 115, 116, 164, 116, 101, 115, 116 - ] - ); + assert_eq!(serialized.0, vec![ + 150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, 55, 57, 56, 50, + 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, 54, 164, 116, 101, + 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 145, 164, 116, 101, 115, + 116, 164, 116, 101, 115, 116 + ]); } #[test] diff --git a/crates/atuin-search-bench/benches/search.rs b/crates/atuin-search-bench/benches/search.rs index ec71dad90..2ed637466 100644 --- a/crates/atuin-search-bench/benches/search.rs +++ b/crates/atuin-search-bench/benches/search.rs @@ -42,15 +42,8 @@ fn main() { /// Queries the way users type them: empty (frecency-only listing), short /// low-selectivity prefixes, multi-word, and a no-match worst case. -const QUERIES: &[&str] = &[ - "", - "g", - "git", - "git p", - "cargo build", - "docker compose up", - "zzznomatchzzz", -]; +const QUERIES: &[&str] = + &["", "g", "git", "git p", "cargo build", "docker compose up", "zzznomatchzzz"]; /// The interactive UI requests up to 200 results per query. const LIMIT: u32 = 200; @@ -90,10 +83,7 @@ impl fmt::Display for Case { } fn env_usize(name: &str, default: usize) -> usize { - std::env::var(name) - .ok() - .and_then(|v| v.trim().parse().ok()) - .unwrap_or(default) + std::env::var(name).ok().and_then(|v| v.trim().parse().ok()).unwrap_or(default) } fn scales() -> &'static Vec { @@ -148,10 +138,7 @@ fn commands() -> &'static Vec { eprintln!("error: BENCH_DATA file {path:?} contains no commands"); std::process::exit(1); } - eprintln!( - "corpus: real history ({path}, {} lines, cycled)", - lines.len() - ); + eprintln!("corpus: real history ({path}, {} lines, cycled)", lines.len()); // repeated commands are the norm in raw history, so cycling stays // representative let mut i = 0; @@ -206,9 +193,7 @@ fn index(scale: usize) -> Arc { eprintln!(" {query:?}: {n} results (limit {LIMIT})"); } let dir = filter_dir(); - let n = index - .search("git", IndexFilterMode::Directory(dir.clone()), LIMIT) - .count(); + let n = index.search("git", IndexFilterMode::Directory(dir.clone()), LIMIT).count(); eprintln!(" \"git\" in {dir:?}: {n} results (limit {LIMIT})"); assert!(n > 0, "directory filter matched nothing; filter is broken"); diff --git a/crates/atuin-search-bench/src/corpus.rs b/crates/atuin-search-bench/src/corpus.rs index 9562e0d74..7c4dd3f02 100644 --- a/crates/atuin-search-bench/src/corpus.rs +++ b/crates/atuin-search-bench/src/corpus.rs @@ -128,44 +128,22 @@ const FILES: &[&str] = &[ "schema.sql", ]; -const BRANCHES: &[&str] = &[ - "main", - "develop", - "feature/search", - "fix/sync-timeout", - "chore/deps", - "release-18", -]; +const BRANCHES: &[&str] = + &["main", "develop", "feature/search", "fix/sync-timeout", "chore/deps", "release-18"]; -const HOSTS: &[&str] = &[ - "prod-web-01", - "staging.internal", - "db.example.com", - "api.example.com", - "10.0.0.42", -]; +const HOSTS: &[&str] = + &["prod-web-01", "staging.internal", "db.example.com", "api.example.com", "10.0.0.42"]; const USERS: &[&str] = &["root", "deploy", "ellie", "admin"]; -const IMAGES: &[&str] = &[ - "ubuntu:24.04", - "postgres:16", - "redis:7", - "alpine", - "rust:1.97", -]; +const IMAGES: &[&str] = &["ubuntu:24.04", "postgres:16", "redis:7", "alpine", "rust:1.97"]; fn gen_one(rng: &mut Rng) -> String { match rng.below(100) { // exact repeats of common commands — the deduplicatable head 0..=37 => rng.pick_common(COMMON).to_string(), 38..=57 => match rng.below(5) { - 0 => format!( - "git commit -m '{} {} {}'", - rng.pick(WORDS), - rng.pick(WORDS), - rng.arg() - ), + 0 => format!("git commit -m '{} {} {}'", rng.pick(WORDS), rng.pick(WORDS), rng.arg()), 1 => format!("git checkout {}", rng.pick(BRANCHES)), 2 => format!("git push origin {}", rng.pick(BRANCHES)), 3 => format!("git rebase -i HEAD~{}", 1 + rng.below(9)), @@ -182,12 +160,7 @@ fn gen_one(rng: &mut Rng) -> String { 0 => format!("cargo run --release -p {}", rng.pick(WORDS)), 1 => format!("npm run {}", rng.pick(WORDS)), 2 => format!("make {}", rng.pick(WORDS)), - _ => format!( - "python3 {}.py --{} {}", - rng.pick(WORDS), - rng.pick(WORDS), - rng.arg() - ), + _ => format!("python3 {}.py --{} {}", rng.pick(WORDS), rng.pick(WORDS), rng.arg()), }, 82..=89 => match rng.below(4) { 0 => format!("ssh {}@{}", rng.pick(USERS), rng.pick(HOSTS)), @@ -210,13 +183,11 @@ fn gen_one(rng: &mut Rng) -> String { 5 + rng.below(20) ), 1 => format!("history | grep {}", rng.pick(WORDS)), - _ => format!( - "ps aux | grep {} | awk '{{print $2}}' | xargs kill -9", - rng.pick(WORDS) - ), + _ => format!("ps aux | grep {} | awk '{{print $2}}' | xargs kill -9", rng.pick(WORDS)), }, 95..=96 => format!( - "for f in {}/*.{}; do {} \"$f\" >> {}.log; done && tail -n {} {}.log | sort | uniq -c | sort -rn", + "for f in {}/*.{}; do {} \"$f\" >> {}.log; done && tail -n {} {}.log | sort | uniq -c \ + | sort -rn", rng.pick(DIRS), rng.pick(&["rs", "log", "json", "txt"]), rng.pick(&["cat", "wc -l", "sha256sum"]), diff --git a/crates/atuin-server-database/src/lib.rs b/crates/atuin-server-database/src/lib.rs index fc6b02f90..cb0a80c92 100644 --- a/crates/atuin-server-database/src/lib.rs +++ b/crates/atuin-server-database/src/lib.rs @@ -4,11 +4,12 @@ pub mod models; use std::fmt::Debug; -use self::models::{NewSession, NewUser, Session, User}; use async_trait::async_trait; use atuin_domain::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus, RecordTag}; use serde::{Deserialize, Serialize}; +use self::models::{NewSession, NewUser, Session, User}; + #[derive(Debug, derive_more::Display, derive_more::Error, derive_more::From)] #[display("{self:?}")] pub enum DbError { diff --git a/crates/atuin-server-postgres/src/lib.rs b/crates/atuin-server-postgres/src/lib.rs index 10734a006..681eb7015 100644 --- a/crates/atuin-server-postgres/src/lib.rs +++ b/crates/atuin-server-postgres/src/lib.rs @@ -1,13 +1,11 @@ use std::collections::HashMap; -use rand::Rng; - use async_trait::async_trait; use atuin_domain::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus, RecordTag}; use atuin_server_database::models::{NewSession, NewUser, Session, User}; use atuin_server_database::{Database, DbError, DbResult, DbSettings}; +use rand::Rng; use sqlx::postgres::PgPoolOptions; - use tracing::instrument; use uuid::Uuid; use wrappers::{DbRecord, DbSession, DbUser}; @@ -34,25 +32,22 @@ impl Postgres { #[async_trait] impl Database for Postgres { async fn new(settings: &DbSettings) -> DbResult { - let pool = PgPoolOptions::new() - .max_connections(100) - .connect(settings.db_uri.as_str()) - .await?; + let pool = + PgPoolOptions::new().max_connections(100).connect(settings.db_uri.as_str()).await?; // Call server_version_num to get the DB server's major version number // The call returns None for servers older than 8.x. - let pg_major_version: u32 = - pool.acquire() - .await? - .server_version_num() - .ok_or(DbError::Other(eyre::Report::msg( - "could not get PostgreSQL version", - )))? - / 10000; + let pg_major_version: u32 = pool + .acquire() + .await? + .server_version_num() + .ok_or(DbError::Other(eyre::Report::msg("could not get PostgreSQL version")))? + / 10000; if pg_major_version < MIN_PG_VERSION { return Err(DbError::Other(eyre::Report::msg(format!( - "unsupported PostgreSQL version {pg_major_version}, minimum required is {MIN_PG_VERSION}" + "unsupported PostgreSQL version {pg_major_version}, minimum required is \ + {MIN_PG_VERSION}" )))); } @@ -64,24 +59,19 @@ impl Database for Postgres { // Create read replica pool if configured let read_pool = if let Some(read_db_uri) = &settings.read_db_uri { tracing::info!("Connecting to read replica database"); - let read_pool = PgPoolOptions::new() - .max_connections(100) - .connect(read_db_uri.as_str()) - .await?; + let read_pool = + PgPoolOptions::new().max_connections(100).connect(read_db_uri.as_str()).await?; // Verify the read replica is also a supported PostgreSQL version - let read_pg_major_version: u32 = read_pool - .acquire() - .await? - .server_version_num() - .ok_or(DbError::Other(eyre::Report::msg( - "could not get PostgreSQL version from read replica", - )))? - / 10000; + let read_pg_major_version: u32 = + read_pool.acquire().await?.server_version_num().ok_or(DbError::Other( + eyre::Report::msg("could not get PostgreSQL version from read replica"), + ))? / 10000; if read_pg_major_version < MIN_PG_VERSION { return Err(DbError::Other(eyre::Report::msg(format!( - "unsupported PostgreSQL version {read_pg_major_version} on read replica, minimum required is {MIN_PG_VERSION}" + "unsupported PostgreSQL version {read_pg_major_version} on read replica, \ + minimum required is {MIN_PG_VERSION}" )))); } @@ -164,20 +154,14 @@ impl Database for Postgres { .execute(&self.pool) .await?; - sqlx::query("delete from store where user_id = $1") - .bind(u.id) - .execute(&self.pool) - .await?; + sqlx::query("delete from store where user_id = $1").bind(u.id).execute(&self.pool).await?; sqlx::query("delete from total_history_count_user where user_id = $1") .bind(u.id) .execute(&self.pool) .await?; - sqlx::query("delete from users where id = $1") - .bind(u.id) - .execute(&self.pool) - .await?; + sqlx::query("delete from users where id = $1").bind(u.id).execute(&self.pool).await?; Ok(()) } @@ -300,7 +284,8 @@ impl Database for Postgres { "insert into store_idx_cache (user_id, host, tag, idx) values ($1, $2, $3, $4) - on conflict(user_id, host, tag) do update set idx = greatest(store_idx_cache.idx, $4) + on conflict(user_id, host, tag) do update set idx = greatest(store_idx_cache.idx, \ + $4) ", ) .bind(user.id) @@ -308,8 +293,7 @@ impl Database for Postgres { .bind(tag) .bind(idx as i64) .execute(&mut *tx) - .await - ?; + .await?; } tx.commit().await?; @@ -391,10 +375,7 @@ impl Database for Postgres { .await? } else { tracing::debug!("using aggregate query for user {}", user.id); - sqlx::query_as(STATUS_SQL) - .bind(user.id) - .fetch_all(self.read_pool()) - .await? + sqlx::query_as(STATUS_SQL).bind(user.id).fetch_all(self.read_pool()).await? }; res.sort(); diff --git a/crates/atuin-server-postgres/src/wrappers.rs b/crates/atuin-server-postgres/src/wrappers.rs index 0077b5dea..79b6f9341 100644 --- a/crates/atuin-server-postgres/src/wrappers.rs +++ b/crates/atuin-server-postgres/src/wrappers.rs @@ -1,7 +1,8 @@ use ::sqlx::{FromRow, Result}; use atuin_domain::record::{EncryptedData, Host, Record, RecordTag, RecordVersion}; use atuin_server_database::models::{Session, User}; -use sqlx::{Row, postgres::PgRow}; +use sqlx::Row; +use sqlx::postgres::PgRow; #[derive(derive_more::Into)] pub struct DbUser(pub User); diff --git a/crates/atuin-server-sqlite/src/lib.rs b/crates/atuin-server-sqlite/src/lib.rs index 6f24c3467..b28bf5373 100644 --- a/crates/atuin-server-sqlite/src/lib.rs +++ b/crates/atuin-server-sqlite/src/lib.rs @@ -2,14 +2,10 @@ use std::str::FromStr; use async_trait::async_trait; use atuin_domain::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus, RecordTag}; -use atuin_server_database::{ - Database, DbError, DbResult, DbSettings, - models::{NewSession, NewUser, Session, User}, -}; -use sqlx::{ - sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}, - types::Uuid, -}; +use atuin_server_database::models::{NewSession, NewUser, Session, User}; +use atuin_server_database::{Database, DbError, DbResult, DbSettings}; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; +use sqlx::types::Uuid; use tracing::instrument; use wrappers::{DbRecord, DbSession, DbUser}; @@ -142,10 +138,7 @@ impl Database for Sqlite { .execute(&self.pool) .await?; - sqlx::query("delete from users where id = $1") - .bind(u.id) - .execute(&self.pool) - .await?; + sqlx::query("delete from users where id = $1").bind(u.id).execute(&self.pool).await?; sqlx::query("delete from history where user_id = $1") .bind(u.id) @@ -256,10 +249,8 @@ impl Database for Sqlite { const STATUS_SQL: &str = "select host, tag, max(idx) from store where user_id = $1 group by host, tag"; - let res: Vec<(Uuid, String, i64)> = sqlx::query_as(STATUS_SQL) - .bind(user.id) - .fetch_all(&self.pool) - .await?; + let res: Vec<(Uuid, String, i64)> = + sqlx::query_as(STATUS_SQL).bind(user.id).fetch_all(&self.pool).await?; let mut status = RecordStatus::new(); diff --git a/crates/atuin-server-sqlite/src/wrappers.rs b/crates/atuin-server-sqlite/src/wrappers.rs index 01d75802f..b15e1fa12 100644 --- a/crates/atuin-server-sqlite/src/wrappers.rs +++ b/crates/atuin-server-sqlite/src/wrappers.rs @@ -1,7 +1,8 @@ use ::sqlx::{FromRow, Result}; use atuin_domain::record::{EncryptedData, Host, Record, RecordTag, RecordVersion}; use atuin_server_database::models::{Session, User}; -use sqlx::{Row, sqlite::SqliteRow}; +use sqlx::Row; +use sqlx::sqlite::SqliteRow; #[derive(derive_more::Into)] pub struct DbUser(pub User); diff --git a/crates/atuin-server/src/bin/main.rs b/crates/atuin-server/src/bin/main.rs index 960bdf6e9..43e8c9495 100644 --- a/crates/atuin-server/src/bin/main.rs +++ b/crates/atuin-server/src/bin/main.rs @@ -6,18 +6,13 @@ use atuin_server::{Settings, example_config, launch, launch_metrics_server}; use atuin_server_database::DbType; use atuin_server_postgres::Postgres; use atuin_server_sqlite::Sqlite; - use clap::Parser; use eyre::{Context, Result, eyre}; -use tracing_subscriber::{EnvFilter, fmt, prelude::*}; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{EnvFilter, fmt}; #[derive(Parser, Debug)] -#[clap( - name = "atuin-server", - about = "Atuin sync server", - version, - infer_subcommands = true -)] +#[clap(name = "atuin-server", about = "Atuin sync server", version, infer_subcommands = true)] enum Cmd { /// Start the server Start { @@ -38,10 +33,7 @@ enum Cmd { async fn main() -> Result<()> { let cmd = Cmd::parse(); - tracing_subscriber::registry() - .with(fmt::layer()) - .with(EnvFilter::from_default_env()) - .init(); + tracing_subscriber::registry().with(fmt::layer()).with(EnvFilter::from_default_env()).init(); tracing::trace!(command = ?cmd, "server command"); diff --git a/crates/atuin-server/src/handlers/health.rs b/crates/atuin-server/src/handlers/health.rs index aebd1e8fc..de3c0118c 100644 --- a/crates/atuin-server/src/handlers/health.rs +++ b/crates/atuin-server/src/handlers/health.rs @@ -1,5 +1,5 @@ -use axum::{Json, http, response::IntoResponse}; - +use axum::response::IntoResponse; +use axum::{Json, http}; use serde::Serialize; #[derive(Serialize)] @@ -8,8 +8,5 @@ pub struct HealthResponse { } pub async fn health_check() -> impl IntoResponse { - ( - http::StatusCode::OK, - Json(HealthResponse { status: "healthy" }), - ) + (http::StatusCode::OK, Json(HealthResponse { status: "healthy" })) } diff --git a/crates/atuin-server/src/handlers/mod.rs b/crates/atuin-server/src/handlers/mod.rs index 6bcd9c614..e35f8a88c 100644 --- a/crates/atuin-server/src/handlers/mod.rs +++ b/crates/atuin-server/src/handlers/mod.rs @@ -2,7 +2,9 @@ use std::fmt; use atuin_domain::api::{ErrorResponse, IndexResponse}; use atuin_server_database::Database; -use axum::{Json, extract::State, http, response::IntoResponse}; +use axum::extract::State; +use axum::response::IntoResponse; +use axum::{Json, http}; use tracing::instrument; use crate::router::AppState; @@ -17,11 +19,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); pub async fn index(state: State>) -> Json { let homage = r#""Through the fathomless deeps of space swims the star turtle Great A'Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld." -- Sir Terry Pratchett"#; - let version = state - .settings - .fake_version - .clone() - .unwrap_or(VERSION.to_string()); + let version = state.settings.fake_version.clone().unwrap_or(VERSION.to_string()); Json(IndexResponse { homage: homage.to_string(), diff --git a/crates/atuin-server/src/handlers/user.rs b/crates/atuin-server/src/handlers/user.rs index e918cbf8f..13b9e411e 100644 --- a/crates/atuin-server/src/handlers/user.rs +++ b/crates/atuin-server/src/handlers/user.rs @@ -3,31 +3,22 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::time::Duration; -use argon2::{ - Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version, - password_hash::SaltString, -}; -use axum::{ - Json, - extract::{ConnectInfo, Path, State}, - http::StatusCode, -}; +use argon2::password_hash::SaltString; +use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version}; +use atuin_common::utils::crypto_random_string; +use atuin_domain::api::*; +use atuin_server_database::models::{NewSession, NewUser}; +use atuin_server_database::{Database, DbError}; +use axum::Json; +use axum::extract::{ConnectInfo, Path, State}; +use axum::http::StatusCode; use metrics::counter; - use rand::rngs::OsRng; +use reqwest::header::CONTENT_TYPE; use tracing::{debug, error, info, instrument, warn}; use super::{ErrorResponse, ErrorResponseStatus, RespExt}; use crate::router::{AppState, UserAuth}; -use atuin_server_database::{ - Database, DbError, - models::{NewSession, NewUser}, -}; - -use reqwest::header::CONTENT_TYPE; - -use atuin_common::utils::crypto_random_string; -use atuin_domain::api::*; pub fn verify_str(hash: &str, password: &str) -> bool { let arg2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default()); @@ -91,10 +82,8 @@ pub async fn register( Json(register): Json, ) -> Result, ErrorResponseStatus<'static>> { if !state.settings.open_registration { - return Err( - ErrorResponse::reply("this server is not open for registrations") - .with_status(StatusCode::BAD_REQUEST), - ); + return Err(ErrorResponse::reply("this server is not open for registrations") + .with_status(StatusCode::BAD_REQUEST)); } for c in register.username.chars() { @@ -193,10 +182,7 @@ pub async fn change_password( ) -> Result, ErrorResponseStatus<'static>> { let db = &state.0.database; - let verified = verify_str( - user.password.as_str(), - change_password.current_password.borrow(), - ); + let verified = verify_str(user.password.as_str(), change_password.current_password.borrow()); if !verified { return Err( ErrorResponse::reply("password is not correct").with_status(StatusCode::UNAUTHORIZED) diff --git a/crates/atuin-server/src/handlers/v0/me.rs b/crates/atuin-server/src/handlers/v0/me.rs index 767be41ec..f5dc232e0 100644 --- a/crates/atuin-server/src/handlers/v0/me.rs +++ b/crates/atuin-server/src/handlers/v0/me.rs @@ -1,11 +1,10 @@ +use atuin_domain::api::*; use axum::Json; use tracing::instrument; use crate::handlers::ErrorResponseStatus; use crate::router::UserAuth; -use atuin_domain::api::*; - #[instrument(skip_all, err(level = "warn"), fields(user.id = user.id))] pub async fn get( UserAuth(user): UserAuth, diff --git a/crates/atuin-server/src/handlers/v0/record.rs b/crates/atuin-server/src/handlers/v0/record.rs index 150046801..ba7e34cd3 100644 --- a/crates/atuin-server/src/handlers/v0/record.rs +++ b/crates/atuin-server/src/handlers/v0/record.rs @@ -1,15 +1,14 @@ -use axum::{Json, extract::Query, extract::State, http::StatusCode}; +use atuin_domain::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus, RecordTag}; +use atuin_server_database::Database; +use axum::Json; +use axum::extract::{Query, State}; +use axum::http::StatusCode; use metrics::counter; use serde::Deserialize; use tracing::{error, instrument}; -use crate::{ - handlers::{ErrorResponse, ErrorResponseStatus, RespExt}, - router::{AppState, UserAuth}, -}; -use atuin_server_database::Database; - -use atuin_domain::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus, RecordTag}; +use crate::handlers::{ErrorResponse, ErrorResponseStatus, RespExt}; +use crate::router::{AppState, UserAuth}; #[instrument(skip_all, err(level = "warn"), fields(user.id = user.id, record.count = records.len()))] pub async fn post( @@ -21,11 +20,7 @@ pub async fn post( database, settings, .. }) = state; - tracing::debug!( - count = records.len(), - user = user.username, - "request to add records" - ); + tracing::debug!(count = records.len(), user = user.username, "request to add records"); counter!("atuin_record_uploaded").increment(records.len() as u64); @@ -36,10 +31,8 @@ pub async fn post( if !keep { counter!("atuin_record_too_large").increment(1); - return Err( - ErrorResponse::reply("could not add records; record too large") - .with_status(StatusCode::BAD_REQUEST), - ); + return Err(ErrorResponse::reply("could not add records; record too large") + .with_status(StatusCode::BAD_REQUEST)); } if let Err(e) = database.add_records(&user, &records).await { diff --git a/crates/atuin-server/src/handlers/v0/store.rs b/crates/atuin-server/src/handlers/v0/store.rs index 983c1d73a..d2430be5f 100644 --- a/crates/atuin-server/src/handlers/v0/store.rs +++ b/crates/atuin-server/src/handlers/v0/store.rs @@ -1,13 +1,12 @@ -use axum::{extract::Query, extract::State, http::StatusCode}; +use atuin_server_database::Database; +use axum::extract::{Query, State}; +use axum::http::StatusCode; use metrics::counter; use serde::Deserialize; use tracing::{error, instrument}; -use crate::{ - handlers::{ErrorResponse, ErrorResponseStatus, RespExt}, - router::{AppState, UserAuth}, -}; -use atuin_server_database::Database; +use crate::handlers::{ErrorResponse, ErrorResponseStatus, RespExt}; +use crate::router::{AppState, UserAuth}; #[derive(Deserialize)] pub struct DeleteParams {} diff --git a/crates/atuin-server/src/lib.rs b/crates/atuin-server/src/lib.rs index a6ee46fd0..b82de8670 100644 --- a/crates/atuin-server/src/lib.rs +++ b/crates/atuin-server/src/lib.rs @@ -12,8 +12,7 @@ mod metrics; mod router; mod trace; -pub use settings::Settings; -pub use settings::example_config; +pub use settings::{Settings, example_config}; pub mod settings; @@ -36,19 +35,14 @@ async fn shutdown_signal() { #[cfg(target_family = "windows")] async fn shutdown_signal() { - signal::windows::ctrl_c() - .expect("failed to register signal handler") - .recv() - .await; + signal::windows::ctrl_c().expect("failed to register signal handler").recv().await; eprintln!("Shutting down gracefully..."); } pub async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { launch_with_tcp_listener::( settings, - TcpListener::bind(addr) - .await - .context("could not connect to socket")?, + TcpListener::bind(addr).await.context("could not connect to socket")?, shutdown_signal(), ) .await @@ -61,12 +55,9 @@ pub async fn launch_with_tcp_listener( ) -> Result<()> { let r = make_router::(settings).await?; - serve( - listener, - r.into_make_service_with_connect_info::(), - ) - .with_graceful_shutdown(shutdown) - .await?; + serve(listener, r.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown) + .await?; Ok(()) } @@ -74,9 +65,7 @@ pub async fn launch_with_tcp_listener( // The separate listener means it's much easier to ensure metrics are not accidentally exposed to // the public. pub async fn launch_metrics_server(host: String, port: u16) -> Result<()> { - let listener = TcpListener::bind((host, port)) - .await - .context("failed to bind metrics tcp")?; + let listener = TcpListener::bind((host, port)).await.context("failed to bind metrics tcp")?; let recorder_handle = metrics::setup_metrics_recorder(); @@ -85,9 +74,7 @@ pub async fn launch_metrics_server(host: String, port: u16) -> Result<()> { axum::routing::get(move || std::future::ready(recorder_handle.render())), ); - serve(listener, router.into_make_service()) - .with_graceful_shutdown(shutdown_signal()) - .await?; + serve(listener, router.into_make_service()).with_graceful_shutdown(shutdown_signal()).await?; Ok(()) } diff --git a/crates/atuin-server/src/metrics.rs b/crates/atuin-server/src/metrics.rs index 8525b79fc..edd963bd7 100644 --- a/crates/atuin-server/src/metrics.rs +++ b/crates/atuin-server/src/metrics.rs @@ -1,16 +1,13 @@ use std::time::Instant; -use axum::{ - extract::{MatchedPath, Request}, - middleware::Next, - response::IntoResponse, -}; +use axum::extract::{MatchedPath, Request}; +use axum::middleware::Next; +use axum::response::IntoResponse; use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; pub fn setup_metrics_recorder() -> PrometheusHandle { - const EXPONENTIAL_SECONDS: &[f64] = &[ - 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, - ]; + const EXPONENTIAL_SECONDS: &[f64] = + &[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]; PrometheusBuilder::new() .set_buckets_for_metric( @@ -43,11 +40,7 @@ pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { let latency = start.elapsed().as_secs_f64(); let status = response.status().as_u16().to_string(); - let labels = [ - ("method", method.to_string()), - ("path", path), - ("status", status), - ]; + let labels = [("method", method.to_string()), ("path", path), ("status", status)]; metrics::counter!("http_requests_total", &labels).increment(1); metrics::histogram!("http_requests_duration_seconds", &labels).record(latency); diff --git a/crates/atuin-server/src/router.rs b/crates/atuin-server/src/router.rs index 932de3baf..395cf9ecc 100644 --- a/crates/atuin-server/src/router.rs +++ b/crates/atuin-server/src/router.rs @@ -3,25 +3,23 @@ use std::sync::Arc; use atuin_domain::api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ErrorResponse}; use atuin_domain::caps::axum::{CapabilitiesRouterExt, get as capabilities_endpoint}; use atuin_domain::caps::{CapServer, CapabilitiesCap}; -use axum::{ - Router, - extract::{FromRequestParts, Request}, - http::{self, request::Parts}, - middleware::Next, - response::{IntoResponse, Response}, - routing::{delete, get, patch, post}, -}; +use atuin_server_database::models::User; +use atuin_server_database::{Database, DbError}; +use axum::Router; +use axum::extract::{FromRequestParts, Request}; +use axum::http::request::Parts; +use axum::http::{self}; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use axum::routing::{delete, get, patch, post}; use eyre::Result; use tower::ServiceBuilder; use tower_http::trace::TraceLayer; use super::handlers; -use crate::{ - handlers::{ErrorResponseStatus, RespExt}, - metrics, - settings::Settings, -}; -use atuin_server_database::{Database, DbError, models::User}; +use crate::handlers::{ErrorResponseStatus, RespExt}; +use crate::metrics; +use crate::settings::Settings; pub struct UserAuth(pub User); @@ -36,14 +34,11 @@ where req: &mut Parts, state: &AppState, ) -> Result { - let auth_header = req - .headers - .get(http::header::AUTHORIZATION) - .ok_or_else(|| { - tracing::debug!("request is missing the authorization header"); - ErrorResponse::reply("missing authorization header") - .with_status(http::StatusCode::BAD_REQUEST) - })?; + let auth_header = req.headers.get(http::header::AUTHORIZATION).ok_or_else(|| { + tracing::debug!("request is missing the authorization header"); + ErrorResponse::reply("missing authorization header") + .with_status(http::StatusCode::BAD_REQUEST) + })?; let auth_header = auth_header.to_str().map_err(|_| { tracing::debug!("authorization header is not valid ascii"); ErrorResponse::reply("invalid authorization header encoding") @@ -57,28 +52,21 @@ where if typ != "Token" { tracing::debug!(scheme = typ, "unsupported authorization scheme"); - return Err( - ErrorResponse::reply("invalid authorization header encoding") - .with_status(http::StatusCode::BAD_REQUEST), - ); + return Err(ErrorResponse::reply("invalid authorization header encoding") + .with_status(http::StatusCode::BAD_REQUEST)); } - let user = state - .database - .get_session_user(token) - .await - .map_err(|e| match e { - DbError::NotFound => { - tracing::warn!("presented session token was not recognised"); - ErrorResponse::reply("session not found") - .with_status(http::StatusCode::FORBIDDEN) - } - DbError::Other(e) => { - tracing::error!(error = ?e, "could not query user session"); - ErrorResponse::reply("could not query user session") - .with_status(http::StatusCode::INTERNAL_SERVER_ERROR) - } - })?; + let user = state.database.get_session_user(token).await.map_err(|e| match e { + DbError::NotFound => { + tracing::warn!("presented session token was not recognised"); + ErrorResponse::reply("session not found").with_status(http::StatusCode::FORBIDDEN) + } + DbError::Other(e) => { + tracing::error!(error = ?e, "could not query user session"); + ErrorResponse::reply("could not query user session") + .with_status(http::StatusCode::INTERNAL_SERVER_ERROR) + } + })?; tracing::debug!(user.id = user.id, user.username = %user.username, "request authenticated"); @@ -98,18 +86,14 @@ async fn clacks_overhead(request: Request, next: Next) -> Response { let gnu_terry_value = "GNU Terry Pratchett, Kris Nova"; let gnu_terry_header = "X-Clacks-Overhead"; - response - .headers_mut() - .insert(gnu_terry_header, gnu_terry_value.parse().unwrap()); + response.headers_mut().insert(gnu_terry_header, gnu_terry_value.parse().unwrap()); response } /// Ensure that we only try and sync with clients on the same major version async fn semver(request: Request, next: Next) -> Response { let mut response = next.run(request).await; - response - .headers_mut() - .insert(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION.parse().unwrap()); + response.headers_mut().insert(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION.parse().unwrap()); response } @@ -157,14 +141,11 @@ pub fn router(database: DB, settings: Settings) -> Router { Router::new().nest(path, routes) }; - routes - .fallback(teapot) - .with_state(AppState { database, settings }) - .layer( - ServiceBuilder::new() - .layer(axum::middleware::from_fn(clacks_overhead)) - .layer(TraceLayer::new_for_http().make_span_with(crate::trace::make_request_span)) - .layer(axum::middleware::from_fn(metrics::track_metrics)) - .layer(axum::middleware::from_fn(semver)), - ) + routes.fallback(teapot).with_state(AppState { database, settings }).layer( + ServiceBuilder::new() + .layer(axum::middleware::from_fn(clacks_overhead)) + .layer(TraceLayer::new_for_http().make_span_with(crate::trace::make_request_span)) + .layer(axum::middleware::from_fn(metrics::track_metrics)) + .layer(axum::middleware::from_fn(semver)), + ) } diff --git a/crates/atuin-server/src/settings.rs b/crates/atuin-server/src/settings.rs index 41093ec7e..ca0cc2c20 100644 --- a/crates/atuin-server/src/settings.rs +++ b/crates/atuin-server/src/settings.rs @@ -1,4 +1,5 @@ -use std::{io::prelude::*, path::PathBuf}; +use std::io::prelude::*; +use std::path::PathBuf; use atuin_server_database::DbSettings; use config::{Config, Environment, File as ConfigFile, FileFormat}; @@ -72,17 +73,11 @@ impl Settings { .set_default("metrics.enable", false)? .set_default("metrics.host", "127.0.0.1")? .set_default("metrics.port", 9001)? - .add_source( - Environment::with_prefix("atuin") - .prefix_separator("_") - .separator("__"), - ); + .add_source(Environment::with_prefix("atuin").prefix_separator("_").separator("__")); config_builder = if config_file.exists() { - config_builder.add_source(ConfigFile::new( - config_file.to_str().unwrap(), - FileFormat::Toml, - )) + config_builder + .add_source(ConfigFile::new(config_file.to_str().unwrap(), FileFormat::Toml)) } else { create_dir_all(config_file.parent().unwrap())?; let mut file = File::create(config_file)?; @@ -93,9 +88,7 @@ impl Settings { let config = config_builder.build()?; - config - .try_deserialize() - .map_err(|e| eyre!("failed to deserialize: {}", e)) + config.try_deserialize().map_err(|e| eyre!("failed to deserialize: {}", e)) } } diff --git a/crates/atuin-server/src/trace.rs b/crates/atuin-server/src/trace.rs index 0cfca8548..0e474826d 100644 --- a/crates/atuin-server/src/trace.rs +++ b/crates/atuin-server/src/trace.rs @@ -19,10 +19,8 @@ pub fn make_request_span(request: &Request) -> Span { .get::() .map_or_else(|| request.uri().path(), MatchedPath::as_str); - let client_ip = request - .extensions() - .get::>() - .map(|ConnectInfo(addr)| addr.ip()); + let client_ip = + request.extensions().get::>().map(|ConnectInfo(addr)| addr.ip()); let span = if route.ends_with("/healthz") { tracing::debug_span!( diff --git a/crates/atuin-server/tests/common/mod.rs b/crates/atuin-server/tests/common/mod.rs index 1a50f8656..300d9ab53 100644 --- a/crates/atuin-server/tests/common/mod.rs +++ b/crates/atuin-server/tests/common/mod.rs @@ -1,4 +1,5 @@ -use std::{env, time::Duration}; +use std::env; +use std::time::Duration; use atuin_client::api_client; use atuin_common::utils::uuid_v7; @@ -6,9 +7,12 @@ use atuin_server::{Settings as ServerSettings, launch_with_tcp_listener}; use atuin_server_database::DbSettings; use atuin_server_postgres::Postgres; use futures_util::TryFutureExt; -use tokio::{net::TcpListener, sync::oneshot, task::JoinHandle}; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tokio::task::JoinHandle; use tracing::{Dispatch, dispatcher}; -use tracing_subscriber::{EnvFilter, layer::SubscriberExt}; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; pub async fn start_server(path: &str) -> (url::Url, oneshot::Sender<()>, JoinHandle<()>) { let formatting_layer = tracing_tree::HierarchicalLayer::default() diff --git a/crates/atuin-server/tests/sync.rs b/crates/atuin-server/tests/sync.rs index 2726c7a55..985312af2 100644 --- a/crates/atuin-server/tests/sync.rs +++ b/crates/atuin-server/tests/sync.rs @@ -1,3 +1,6 @@ +use std::env::temp_dir; +use std::time::Duration; + use atuin_client::api_client; use atuin_client::record::sqlite_store::SqliteStore; use atuin_client::record::sync; @@ -9,8 +12,6 @@ use atuin_server_database::DbSettings; use atuin_server_sqlite::Sqlite; use futures_util::TryFutureExt; use rstest::{fixture, rstest}; -use std::env::temp_dir; -use std::time::Duration; use tokio::net::TcpListener; use tokio::sync::oneshot; use tokio::task::JoinHandle; @@ -30,15 +31,10 @@ impl TestServer { let password = uuid_v7().as_simple().to_string(); let email = format!("{}@example.com", uuid_v7().as_simple()); - let resp = api_client::register( - &self.address, - &username, - &email, - &password, - &Default::default(), - ) - .await - .unwrap(); + let resp = + api_client::register(&self.address, &username, &email, &password, &Default::default()) + .await + .unwrap(); api_client::Client::new( self.address.clone(), @@ -148,25 +144,20 @@ async fn download( let host = HostId(uuid_v7()); let tag = RecordTag::Other(uuid_v7().as_simple().to_string()); - let records: Vec> = (0..=remote_max) - .map(|idx| record(host, &tag, idx)) - .collect(); + let records: Vec> = + (0..=remote_max).map(|idx| record(host, &tag, idx)).collect(); client.post_records(&records).await.unwrap(); let store = SqliteStore::new(":memory:", 2.0).await.unwrap(); if let Some(local_max) = local_max { - store - .push_batch(records.iter().take(local_max as usize + 1)) - .await - .unwrap(); + store.push_batch(records.iter().take(local_max as usize + 1)).await.unwrap(); } let (diff, _) = sync::diff(&client, &store).await.unwrap(); let operations = sync::operations(diff, &store).await.unwrap(); - let (_, downloaded) = sync::sync_remote(&client, operations, &store, page_size, &key()) - .await - .unwrap(); + let (_, downloaded) = + sync::sync_remote(&client, operations, &store, page_size, &key()).await.unwrap(); let status = store.status().await.unwrap(); let local_idx = *status.hosts.get(&host).unwrap().get(&tag).unwrap(); @@ -219,17 +210,13 @@ async fn upload( store.push_batch(records.iter()).await.unwrap(); if let Some(remote_max) = remote_max { - client - .post_records(&records[..=remote_max as usize]) - .await - .unwrap(); + client.post_records(&records[..=remote_max as usize]).await.unwrap(); } let (diff, _) = sync::diff(&client, &store).await.unwrap(); let operations = sync::operations(diff, &store).await.unwrap(); - let (uploaded, _) = sync::sync_remote(&client, operations, &store, page_size, &key()) - .await - .unwrap(); + let (uploaded, _) = + sync::sync_remote(&client, operations, &store, page_size, &key()).await.unwrap(); let status = client.record_status().await.unwrap(); let remote_idx = *status.hosts.get(&host).unwrap().get(&tag).unwrap(); diff --git a/crates/atuin-server/tests/users.rs b/crates/atuin-server/tests/users.rs index e4fc2c017..1cd393840 100644 --- a/crates/atuin-server/tests/users.rs +++ b/crates/atuin-server/tests/users.rs @@ -3,11 +3,7 @@ use rstest::{fixture, rstest}; mod common; -type TestServer = ( - url::Url, - tokio::sync::oneshot::Sender<()>, - tokio::task::JoinHandle<()>, -); +type TestServer = (url::Url, tokio::sync::oneshot::Sender<()>, tokio::task::JoinHandle<()>); #[fixture] async fn server() -> TestServer { @@ -62,9 +58,7 @@ async fn change_password(#[future] server: TestServer) { let current_password = password; let new_password = uuid_v7().as_simple().to_string(); - let result = client - .change_password(current_password, new_password.clone()) - .await; + let result = client.change_password(current_password, new_password.clone()).await; // the password change request succeeded assert!(result.is_ok()); @@ -109,9 +103,7 @@ async fn multi_user_test(#[future] server: TestServer) { let current_password = password_one; let new_password = uuid_v7().as_simple().to_string(); - let result = client_one - .change_password(current_password, new_password.clone()) - .await; + let result = client_one.change_password(current_password, new_password.clone()).await; // the password change request succeeded assert!(result.is_ok()); diff --git a/crates/atuin/src/command/client.rs b/crates/atuin/src/command/client.rs index 4d29154e6..12fd61358 100644 --- a/crates/atuin/src/command/client.rs +++ b/crates/atuin/src/command/client.rs @@ -1,12 +1,12 @@ +use atuin_client::database::Sqlite; +use atuin_client::logs::FromSettings; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_client::theme; +use atuin_common::logs::{self, LogConfig}; use clap::Subcommand; use eyre::{Result, WrapErr}; -use atuin_client::logs::FromSettings; -use atuin_client::{ - database::Sqlite, record::sqlite_store::SqliteStore, settings::Settings, theme, -}; -use atuin_common::logs::{self, LogConfig}; - #[cfg(feature = "sync")] mod sync; @@ -103,7 +103,9 @@ pub enum Cmd { Update(update::Cmd), #[command()] - Wrapped { year: Option }, + Wrapped { + year: Option, + }, /// *Experimental* Manage the background daemon #[cfg(feature = "daemon")] @@ -305,10 +307,9 @@ impl Cmd { match self { Self::History(cmd) => cmd.log_config(), - Self::Search(cmd) if cmd.is_interactive() => Some(LogConfig::from_settings( - &settings.logs, - &settings.logs.search, - )), + Self::Search(cmd) if cmd.is_interactive() => { + Some(LogConfig::from_settings(&settings.logs, &settings.logs.search)) + } #[cfg(feature = "daemon")] Self::Daemon(cmd) => Some(LogConfig { diff --git a/crates/atuin/src/command/client/account.rs b/crates/atuin/src/command/client/account.rs index a1be65a59..1c5cccaa1 100644 --- a/crates/atuin/src/command/client/account.rs +++ b/crates/atuin/src/command/client/account.rs @@ -1,8 +1,7 @@ -use clap::{Args, Subcommand}; -use eyre::Result; - use atuin_client::record::sqlite_store::SqliteStore; use atuin_client::settings::Settings; +use clap::{Args, Subcommand}; +use eyre::Result; pub mod change_password; pub mod delete; diff --git a/crates/atuin/src/command/client/account/change_password.rs b/crates/atuin/src/command/client/account/change_password.rs index 06b7f2e20..a7adfdd0a 100644 --- a/crates/atuin/src/command/client/account/change_password.rs +++ b/crates/atuin/src/command/client/account/change_password.rs @@ -1,10 +1,7 @@ +use atuin_client::auth::{self, AuthClient, MutateResponse}; +use atuin_client::settings::Settings; use clap::Parser; use eyre::{Result, bail}; - -use atuin_client::{ - auth::{self, AuthClient, MutateResponse}, - settings::Settings, -}; use rpassword::prompt_password; #[derive(Parser, Debug)] diff --git a/crates/atuin/src/command/client/account/delete.rs b/crates/atuin/src/command/client/account/delete.rs index d809dfd26..8e3d54737 100644 --- a/crates/atuin/src/command/client/account/delete.rs +++ b/crates/atuin/src/command/client/account/delete.rs @@ -1,7 +1,5 @@ -use atuin_client::{ - auth::{self, AuthClient, MutateResponse}, - settings::Settings, -}; +use atuin_client::auth::{self, AuthClient, MutateResponse}; +use atuin_client::settings::Settings; use clap::Parser; use eyre::{Result, bail}; @@ -34,9 +32,7 @@ impl Cmd { let mut totp_code = self.totp_code.clone(); loop { - let response = client - .delete_account(&password, totp_code.as_deref()) - .await?; + let response = client.delete_account(&password, totp_code.as_deref()).await?; match response { MutateResponse::Success => break, diff --git a/crates/atuin/src/command/client/account/link.rs b/crates/atuin/src/command/client/account/link.rs index 610e26425..73aa8a32c 100644 --- a/crates/atuin/src/command/client/account/link.rs +++ b/crates/atuin/src/command/client/account/link.rs @@ -1,6 +1,5 @@ -use eyre::{Result, bail}; - use atuin_client::settings::Settings; +use eyre::{Result, bail}; pub async fn run(settings: &Settings) -> Result<()> { let meta = Settings::meta_store().await?; diff --git a/crates/atuin/src/command/client/account/login.rs b/crates/atuin/src/command/client/account/login.rs index 247ce06a8..7dafbe3bc 100644 --- a/crates/atuin/src/command/client/account/login.rs +++ b/crates/atuin/src/command/client/account/login.rs @@ -1,15 +1,12 @@ use std::io::{self, IsTerminal}; +use atuin_client::auth::{self, AuthClient, AuthResponse}; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::record::sync::{self, SyncError}; +use atuin_client::settings::{Settings, SyncAuth}; +use atuin_common::encryption::paseto_v4; use clap::Parser; use eyre::{Context, Result, bail}; - -use atuin_client::{ - auth::{self, AuthClient, AuthResponse}, - record::sqlite_store::SqliteStore, - record::sync::{self, SyncError}, - settings::{Settings, SyncAuth}, -}; -use atuin_common::encryption::paseto_v4; use rpassword::prompt_password; #[derive(Parser, Debug)] @@ -58,8 +55,8 @@ impl Cmd { } SyncAuth::HubViaCli { .. } => { println!( - "You have a legacy sync session. \ - Continuing login to upgrade to full Hub authentication." + "You have a legacy sync session. Continuing login to upgrade to full Hub \ + authentication." ); } SyncAuth::NotLoggedIn { .. } => {} @@ -97,9 +94,7 @@ impl Cmd { let mut totp_code = self.totp_code.clone(); let (session, auth_type) = loop { - let response = client - .login(username, &password, totp_code.as_deref()) - .await?; + let response = client.login(username, &password, totp_code.as_deref()).await?; match response { AuthResponse::Success { session, auth_type } => break (session, auth_type), @@ -118,8 +113,8 @@ impl Cmd { meta.save_session(&session).await?; println!("\nNote: Your account has not been fully migrated to Atuin Hub."); println!( - "Sync will continue to work, but you can visit hub.atuin.sh \ - to create an account and link it to your existing CLI account." + "Sync will continue to work, but you can visit hub.atuin.sh to create an \ + account and link it to your existing CLI account." ); } } else { @@ -195,14 +190,12 @@ impl Cmd { println!("IMPORTANT"); println!( - "If you are already logged in on another machine, you must ensure that the key you use here is the same as the key you used there." + "If you are already logged in on another machine, you must ensure that the key you \ + use here is the same as the key you used there." ); println!("You can find your key by running 'atuin key' on the other machine."); println!("Do not share this key with anyone."); - println!( - "\nRead more here: {} \n", - atuin_common::docs::url("guide/sync/#login") - ); + println!("\nRead more here: {} \n", atuin_common::docs::url("guide/sync/#login")); let interactive = self.interactive(); let mut flag_key = self.key.clone(); @@ -219,7 +212,8 @@ impl Cmd { if key.is_empty() { if !key_path.exists() { - let msg = "No key provided and no existing key file found. Please use 'atuin key' on your other machine, or recover your key from a backup"; + let msg = "No key provided and no existing key file found. Please use 'atuin \ + key' on your other machine, or recover your key from a backup"; if !interactive { bail!(msg); } @@ -300,7 +294,8 @@ async fn verify_key_against_remote( "\nThe encryption key on this machine does not match the data on the server." ); println!( - "You can find the correct key by running 'atuin key' on a machine that already syncs successfully." + "You can find the correct key by running 'atuin key' on a machine that \ + already syncs successfully." ); let input = read_user_input("encryption key [blank to log out and cancel]"); @@ -335,10 +330,9 @@ async fn logout_wrong_key() -> ! { } crate::print_error::print_error( "Wrong encryption key", - "The encryption key on this machine does not match the data on the server. \ - You have been logged out.\n\n\ - To fix this, find your existing key by running `atuin key` on a machine that \ - already syncs successfully, then run `atuin login` again here with that key.", + "The encryption key on this machine does not match the data on the server. You have been \ + logged out.\n\nTo fix this, find your existing key by running `atuin key` on a machine \ + that already syncs successfully, then run `atuin login` again here with that key.", ); std::process::exit(1); } @@ -376,7 +370,8 @@ mod tests { assert_eq!(mnemonic.entropy(), key.as_bytes().as_slice()); 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" + "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/crates/atuin/src/command/client/account/register.rs b/crates/atuin/src/command/client/account/register.rs index 6429f42d3..508e6a8bc 100644 --- a/crates/atuin/src/command/client/account/register.rs +++ b/crates/atuin/src/command/client/account/register.rs @@ -1,13 +1,11 @@ +use atuin_client::auth::{self, AuthClient, AuthResponse}; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::{Settings, SyncAuth}; +use atuin_common::encryption::paseto_v4; use clap::Parser; use eyre::{Result, bail}; use super::login::or_user_input; -use atuin_client::{ - auth::{self, AuthClient, AuthResponse}, - record::sqlite_store::SqliteStore, - settings::{Settings, SyncAuth}, -}; -use atuin_common::encryption::paseto_v4; #[derive(Parser, Debug)] pub struct Cmd { @@ -37,8 +35,8 @@ impl Cmd { } SyncAuth::HubViaCli { .. } => { println!( - "You already have a sync session. \ - Run 'atuin login' to upgrade to full Hub authentication." + "You already have a sync session. Run 'atuin login' to upgrade to full Hub \ + authentication." ); println!("Run 'atuin logout' first if you want to register a new account."); return Ok(()); @@ -48,17 +46,14 @@ impl Cmd { if settings.is_hub_sync() { let required_for_headless = 3; - let provided = [ - self.username.is_some(), - self.email.is_some(), - self.password.is_some(), - ] - .iter() - .filter(|&b| *b) - .count(); + let provided = [self.username.is_some(), self.email.is_some(), self.password.is_some()] + .iter() + .filter(|&b| *b) + .count(); if provided < required_for_headless { println!( - "Username, password, and email are all required for headless registration. Continuing with interactive registration.\n" + "Username, password, and email are all required for headless registration. \ + Continuing with interactive registration.\n" ); } @@ -88,8 +83,9 @@ impl Cmd { "\nNote: Your account has not been fully migrated to Atuin Hub." ); println!( - "Sync will continue to work, but you can visit hub.atuin.sh \ - to create a new Hub account and link it to your existing CLI account." + "Sync will continue to work, but you can visit hub.atuin.sh to \ + create a new Hub account and link it to your existing CLI \ + account." ); } } @@ -101,10 +97,12 @@ impl Cmd { let _key = paseto_v4::Key::try_load_or_generate(&settings.key_path)?; println!( - "Registration successful! Please make a note of your key (run 'atuin key') and keep it safe." + "Registration successful! Please make a note of your key (run 'atuin key') \ + and keep it safe." ); println!( - "You will need it to log in on other devices, and we cannot help recover it if you lose it." + "You will need it to log in on other devices, and we cannot help recover it \ + if you lose it." ); } else { // Interactive registration: delegate to the browser OAuth flow. @@ -126,10 +124,7 @@ impl Cmd { let username = or_user_input(self.username.clone(), "username"); let email = or_user_input(self.email.clone(), "email"); - let password = self - .password - .clone() - .unwrap_or_else(super::login::read_user_password); + let password = self.password.clone().unwrap_or_else(super::login::read_user_password); if password.is_empty() { bail!("please provide a password"); @@ -150,10 +145,12 @@ impl Cmd { let _key = paseto_v4::Key::try_load_or_generate(&settings.key_path)?; println!( - "Registration successful! Please make a note of your key (run 'atuin key') and keep it safe." + "Registration successful! Please make a note of your key (run 'atuin key') and \ + keep it safe." ); println!( - "You will need it to log in on other devices, and we cannot help recover it if you lose it." + "You will need it to log in on other devices, and we cannot help recover it if \ + you lose it." ); } diff --git a/crates/atuin/src/command/client/config.rs b/crates/atuin/src/command/client/config.rs index 406774ee7..bfc2f7bb4 100644 --- a/crates/atuin/src/command/client/config.rs +++ b/crates/atuin/src/command/client/config.rs @@ -82,9 +82,8 @@ impl GetCmd { match current { Some(item) if item.is_table() || item.is_inline_table() => { - let table = item - .as_table_like() - .expect("is_table()/is_inline_table() but no table"); + let table = + item.as_table_like().expect("is_table()/is_inline_table() but no table"); println!("{prefix}[{key}]"); dump_table(table, prefix, &mut vec![key.to_string()])?; } @@ -170,7 +169,7 @@ impl SetCmd { Settings::validate_str(&updated).map_err(|e| { eyre::eyre!( "cannot update config: setting '{key}' to '{}' would make your configuration \ - invalid\n\n{e}", + invalid\n\n{e}", self.value, ) })?; @@ -192,21 +191,17 @@ impl SetCmd { match effective_type { ValueType::String => Ok(Value::from(raw.as_str())), ValueType::Boolean => { - let b: bool = raw - .parse() - .map_err(|_| eyre::eyre!("invalid boolean value: {raw}"))?; + let b: bool = + raw.parse().map_err(|_| eyre::eyre!("invalid boolean value: {raw}"))?; Ok(Value::from(b)) } ValueType::Integer => { - let i: i64 = raw - .parse() - .map_err(|_| eyre::eyre!("invalid integer value: {raw}"))?; + let i: i64 = + raw.parse().map_err(|_| eyre::eyre!("invalid integer value: {raw}"))?; Ok(Value::from(i)) } ValueType::Float => { - let f: f64 = raw - .parse() - .map_err(|_| eyre::eyre!("invalid float value: {raw}"))?; + let f: f64 = raw.parse().map_err(|_| eyre::eyre!("invalid float value: {raw}"))?; Ok(Value::from(f)) } ValueType::Auto => { @@ -244,9 +239,7 @@ impl PrintCmd { if current.is_table() || current.is_inline_table() { println!("[{key}]"); dump_table( - current - .as_table_like() - .expect("is_table()/is_inline_table() but no table"), + current.as_table_like().expect("is_table()/is_inline_table() but no table"), "", &mut vec![key.clone()], )?; @@ -269,9 +262,7 @@ fn dump_table(table: &dyn TableLike, prefix: &str, stack: &mut Vec) -> R if value.is_table() || value.is_inline_table() { stack.push(key.to_string()); - let table = value - .as_table_like() - .expect("is_table()/is_inline_table() but no table"); + let table = value.as_table_like().expect("is_table()/is_inline_table() but no table"); println!("\n{}[{}]", prefix, stack.join(".")); @@ -291,9 +282,7 @@ fn get_deep_key<'doc>(doc: &'doc Document, key: &str) -> Option<&'doc It let mut current: Option<&Item> = Some(doc.as_item()); for part in parts { - current = current - .and_then(|item| item.as_table_like()) - .and_then(|table| table.get(part)); + current = current.and_then(|item| item.as_table_like()).and_then(|table| table.get(part)); } current @@ -375,9 +364,10 @@ fn set_deep_key(doc: &mut DocumentMut, key: &str, value: Value) -> Result<()> { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + /// Call [`set_deep_key`] and reserialize. fn set(input: &str, key: &str, value: Value) -> String { let mut doc: DocumentMut = input.parse().expect("test input should parse as TOML"); @@ -460,7 +450,8 @@ mod tests { "# atuin config\n\n## enable sync\n# auto_sync = true\n\nenter_accept = true\n", "auto_sync", Value::from(false), - "# atuin config\n\n## enable sync\n# auto_sync = true\n\nenter_accept = true\nauto_sync = false\n" + "# atuin config\n\n## enable sync\n# auto_sync = true\n\nenter_accept = true\nauto_sync = \ + false\n" )] fn set_adds_a_missing_key_without_touching_existing_content( #[case] input: &str, @@ -479,10 +470,7 @@ mod tests { Value::from("10m"), ); let twice = set(&once, "sync.frequency", Value::from("30m")); - assert_eq!( - twice, - "[sync]\n# how often to sync\nfrequency = \"30m\" # unit is flexible\n" - ); + assert_eq!(twice, "[sync]\n# how often to sync\nfrequency = \"30m\" # unit is flexible\n"); } /// Helper for building a [`SetCmd`]. @@ -519,9 +507,8 @@ mod tests { #[case] value: &str, #[case] expected: &str, ) { - let updated = set_cmd(key, value) - .get_updated_config(input) - .expect("the update should be accepted"); + let updated = + set_cmd(key, value).get_updated_config(input).expect("the update should be accepted"); assert_eq!(updated, expected); } @@ -555,10 +542,7 @@ mod tests { .to_string(); for fragment in expected_err { - assert!( - err.contains(fragment), - "error should mention `{fragment}`, got: {err}" - ); + assert!(err.contains(fragment), "error should mention `{fragment}`, got: {err}"); } } } diff --git a/crates/atuin/src/command/client/daemon.rs b/crates/atuin/src/command/client/daemon.rs index 1814d5956..d75122e0b 100644 --- a/crates/atuin/src/command/client/daemon.rs +++ b/crates/atuin/src/command/client/daemon.rs @@ -6,9 +6,10 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; -use atuin_client::{ - database::Sqlite, history::History, record::sqlite_store::SqliteStore, settings::Settings, -}; +use atuin_client::database::Sqlite; +use atuin_client::history::History; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use atuin_daemon::DaemonEvent; use atuin_daemon::client::{ControlClient, DaemonClientErrorKind, HistoryClient, classify_error}; use clap::Subcommand; @@ -114,10 +115,9 @@ impl PidfileGuard { match file.try_lock() { Ok(()) => {} - Err(TryLockError::WouldBlock) => bail!( - "daemon already running (pidfile lock busy at {})", - path.display() - ), + Err(TryLockError::WouldBlock) => { + bail!("daemon already running (pidfile lock busy at {})", path.display()) + } Err(TryLockError::Error(err)) => { return Err(err) .wrap_err_with(|| format!("could not lock daemon pidfile {}", path.display())); @@ -215,8 +215,7 @@ async fn wait_for_lock(path: &Path, timeout: Duration) -> Result { async fn wait_for_pidfile_available(path: &Path, timeout: Duration) -> Result<()> { let file = wait_for_lock(path, timeout).await?; - file.unlock() - .wrap_err_with(|| format!("failed to unlock {}", path.display()))?; + file.unlock().wrap_err_with(|| format!("failed to unlock {}", path.display()))?; Ok(()) } @@ -258,11 +257,7 @@ fn spawn_daemon_process() -> Result<()> { let exe = std::env::current_exe().wrap_err("could not locate atuin executable")?; let mut cmd = Command::new(exe); - cmd.arg("daemon") - .arg("start") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); + cmd.arg("daemon").arg("start").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); #[cfg(unix)] cmd.arg("--daemonize"); @@ -306,10 +301,7 @@ fn remove_sockets( && e.kind() != ErrorKind::NotFound { // Log the error because we only return the first error when multiple occur. - tracing::error!( - "failed to remove daemon socket {}: {e}", - socket_path.display() - ); + tracing::error!("failed to remove daemon socket {}: {e}", socket_path.display()); error.get_or_insert_with(|| RemoveSocketError { path: socket_path.into_owned(), source: e, @@ -366,7 +358,8 @@ fn ensure_autostart_supported(settings: &Settings) -> Result<()> { #[cfg(unix)] if settings.daemon.systemd_socket { bail!( - "daemon autostart is incompatible with `daemon.systemd_socket = true`; use systemd to manage the daemon" + "daemon autostart is incompatible with `daemon.systemd_socket = true`; use systemd to \ + manage the daemon" ); } #[cfg(not(unix))] @@ -593,10 +586,7 @@ async fn status_cmd(settings: &Settings) -> Result<()> { println!(" Protocol: {}", status.protocol); println!(" Healthy: {}", status.healthy); #[cfg(unix)] - println!( - " Socket: {}", - settings.daemon.existing_socket_path().display() - ); + println!(" Socket: {}", settings.daemon.existing_socket_path().display()); #[cfg(not(unix))] println!(" Port: {}", settings.daemon.tcp_port); } @@ -678,10 +668,7 @@ pub fn daemonize_current_process() -> Result<()> { let cwd = std::env::current_dir().wrap_err("could not determine current directory for daemon")?; - Daemonize::new() - .working_directory(cwd) - .start() - .wrap_err("failed to daemonize process")?; + Daemonize::new().working_directory(cwd).start().wrap_err("failed to daemonize process")?; Ok(()) } @@ -758,9 +745,10 @@ fn kill_process(pid: u32) { #[cfg(test)] mod tests { - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[rstest] #[case::matches(DAEMON_VERSION, DAEMON_PROTOCOL_VERSION, true)] #[case::wrong_version("0.0.0", DAEMON_PROTOCOL_VERSION, false)] diff --git a/crates/atuin/src/command/client/doctor.rs b/crates/atuin/src/command/client/doctor.rs index a40461230..62a7aa779 100644 --- a/crates/atuin/src/command/client/doctor.rs +++ b/crates/atuin/src/command/client/doctor.rs @@ -1,6 +1,7 @@ +use std::env; use std::path::PathBuf; use std::process::Command; -use std::{env, str::FromStr}; +use std::str::FromStr; use atuin_client::database::Sqlite; use atuin_client::settings::Settings; @@ -9,7 +10,6 @@ use atuin_common::utils; use colored::Colorize; use eyre::Result; use serde::Serialize; - use sysinfo::{Disks, System, get_current_pid}; #[derive(Debug, Serialize)] @@ -36,10 +36,7 @@ impl ShellInfo { // Every shell we support handles `shell -ic 'command'` fn shellvar_exists(shell: &str, var: &str) -> bool { let cmd = Command::new(shell) - .args([ - "-ic", - format!("[ -z ${var} ] || echo ATUIN_DOCTOR_ENV_FOUND").as_str(), - ]) + .args(["-ic", format!("[ -z ${var} ] || echo ATUIN_DOCTOR_ENV_FOUND").as_str()]) .output() .map_or(String::new(), |v| { let out = v.stdout; @@ -53,10 +50,8 @@ impl ShellInfo { if env::var("ATUIN_SESSION").ok().is_none() { None } else if shell.starts_with("bash") || shell == "sh" { - env::var("ATUIN_PREEXEC_BACKEND") - .ok() - .filter(|value| !value.is_empty()) - .and_then(|atuin_preexec_backend| { + env::var("ATUIN_PREEXEC_BACKEND").ok().filter(|value| !value.is_empty()).and_then( + |atuin_preexec_backend| { atuin_preexec_backend.rfind(':').and_then(|pos_colon| { u32::from_str(&atuin_preexec_backend[..pos_colon]) .ok() @@ -68,7 +63,8 @@ impl ShellInfo { }) .then(|| atuin_preexec_backend[pos_colon + 1..].to_string()) }) - }) + }, + ) } else { Some("built-in".to_string()) } @@ -113,12 +109,7 @@ impl ShellInfo { type PluginValidator = fn(&str, &sysinfo::Process, &str) -> Option; - let plugin_list: [( - &str, - PluginShellType, - PluginProbeType, - Option, - ); 3] = [ + let plugin_list: [(&str, PluginShellType, PluginProbeType, Option); 3] = [ ( "atuin", PluginShellType::Any, @@ -263,12 +254,9 @@ impl SyncInfo { // that a diagnostic command should not trigger. let meta = Settings::meta_store().await.ok(); let has_hub_token = match &meta { - Some(m) => m - .hub_session_token() - .await - .ok() - .flatten() - .is_some_and(|t| t.starts_with("atapi_")), + Some(m) => { + m.hub_session_token().await.ok().flatten().is_some_and(|t| t.starts_with("atapi_")) + } None => false, }; let has_cli_token = match &meta { @@ -358,10 +346,7 @@ impl AtuinInfo { }; let sqlite_version = match Sqlite::new("sqlite::memory:", 0.1).await { - Ok(db) => db - .sqlite_version() - .await - .unwrap_or_else(|_| "unknown".to_string()), + Ok(db) => db.sqlite_version().await.unwrap_or_else(|_| "unknown".to_string()), Err(_) => "error".to_string(), }; @@ -398,12 +383,18 @@ fn checks(info: &DoctorDump) { // let zfs_error = "[Filesystem] ZFS is known to have some issues with SQLite. Atuin uses SQLite heavily. If you are having poor performance, there are some workarounds here: https://github.com/atuinsh/atuin/issues/952".bold().red(); let bash_plugin_error = format!( - "[Shell] If you are using Bash, Atuin requires that either bash-preexec or ble.sh (>= 0.4) be installed. An older ble.sh may not be detected. so ignore this if you have ble.sh >= 0.4 set up! Read more here: {}", + "[Shell] If you are using Bash, Atuin requires that either bash-preexec or ble.sh (>= \ + 0.4) be installed. An older ble.sh may not be detected. so ignore this if you have \ + ble.sh >= 0.4 set up! Read more here: {}", atuin_common::docs::url("guide/installation/#installing-the-shell-plugin") ) .bold() .red(); - let blesh_integration_error = "[Shell] Atuin and ble.sh seem to be loaded in the session, but the integration does not seem to be working. Please check the setup in .bashrc.".bold().red(); + let blesh_integration_error = "[Shell] Atuin and ble.sh seem to be loaded in the session, but \ + the integration does not seem to be working. Please check the \ + setup in .bashrc." + .bold() + .red(); let openbsd_warning = "[System] OpenBSD is not officially supported.".bold().red(); if cfg!(target_os = "openbsd") { @@ -419,12 +410,7 @@ fn checks(info: &DoctorDump) { // Shell if info.shell.name == "bash" { - if !info - .shell - .plugins - .iter() - .any(|p| p == "blesh" || p == "bash-preexec") - { + if !info.shell.plugins.iter().any(|p| p == "blesh" || p == "bash-preexec") { println!("{bash_plugin_error}"); } diff --git a/crates/atuin/src/command/client/dotfiles.rs b/crates/atuin/src/command/client/dotfiles.rs index f42b18f2f..7da408af7 100644 --- a/crates/atuin/src/command/client/dotfiles.rs +++ b/crates/atuin/src/command/client/dotfiles.rs @@ -1,8 +1,8 @@ +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use clap::Subcommand; use eyre::Result; -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; - mod alias; mod var; diff --git a/crates/atuin/src/command/client/dotfiles/alias.rs b/crates/atuin/src/command/client/dotfiles/alias.rs index fd97f3f9c..dc66bdbb9 100644 --- a/crates/atuin/src/command/client/dotfiles/alias.rs +++ b/crates/atuin/src/command/client/dotfiles/alias.rs @@ -1,11 +1,11 @@ +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; +use atuin_dotfiles::shell::Alias; +use atuin_dotfiles::store::AliasStore; use clap::{Subcommand, ValueEnum}; use eyre::{Context, Result, eyre}; -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::encryption::paseto_v4; - -use atuin_dotfiles::{shell::Alias, store::AliasStore}; - #[derive(Clone, Copy, Debug, Default, ValueEnum)] pub enum SortBy { /// Sort by alias name @@ -19,10 +19,15 @@ pub enum SortBy { #[command(infer_subcommands = true)] pub enum Cmd { /// Set an alias - Set { name: String, value: String }, + Set { + name: String, + value: String, + }, /// Delete an alias - Delete { name: String }, + Delete { + name: String, + }, /// List all aliases List { @@ -62,10 +67,7 @@ impl Cmd { if found.is_empty() { println!("Aliasing '{name}={value}'."); } else { - println!( - "Overwriting alias '{name}={}' with '{name}={value}'.", - found[0].value - ); + println!("Overwriting alias '{name}={}' with '{name}={value}'.", found[0].value); } store.set(&name, &value).await?; @@ -151,7 +153,8 @@ impl Cmd { pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> { if !settings.dotfiles.enabled { eprintln!( - "Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your configuration file to enable them.\n" + "Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your \ + configuration file to enable them.\n" ); eprintln!("The default configuration file is located at ~/.config/atuin/config.toml."); return Ok(()); @@ -171,16 +174,7 @@ impl Cmd { reverse, name, value, - } => { - self.list( - &alias_store, - *sort_by, - *reverse, - name.clone(), - value.clone(), - ) - .await - } + } => self.list(&alias_store, *sort_by, *reverse, name.clone(), value.clone()).await, Self::Clear => self.clear(&alias_store).await, } } diff --git a/crates/atuin/src/command/client/dotfiles/var.rs b/crates/atuin/src/command/client/dotfiles/var.rs index add75bf42..532270015 100644 --- a/crates/atuin/src/command/client/dotfiles/var.rs +++ b/crates/atuin/src/command/client/dotfiles/var.rs @@ -1,11 +1,11 @@ +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; +use atuin_dotfiles::shell::Var; +use atuin_dotfiles::store::var::VarStore; use clap::{Subcommand, ValueEnum}; use eyre::{Context, Result}; -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::encryption::paseto_v4; - -use atuin_dotfiles::{shell::Var, store::var::VarStore}; - #[derive(Clone, Copy, Debug, Default, ValueEnum)] pub enum SortBy { /// Sort by variable name @@ -28,7 +28,9 @@ pub enum Cmd { }, /// Delete a variable - Delete { name: String }, + Delete { + name: String, + }, /// List all variables List { @@ -62,7 +64,11 @@ impl Cmd { async fn set(&self, store: VarStore, name: String, value: String, export: bool) -> Result<()> { let vars = store.vars().await?; let found: Vec = vars.into_iter().filter(|a| a.name == name).collect(); - let show_export = if export { "export " } else { "" }; + let show_export = if export { + "export " + } else { + "" + }; if found.is_empty() { println!("Setting '{show_export}{name}={value}'."); @@ -151,7 +157,8 @@ impl Cmd { pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> { if !settings.dotfiles.enabled { eprintln!( - "Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your configuration file to enable them.\n" + "Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your \ + configuration file to enable them.\n" ); eprintln!("The default configuration file is located at ~/.config/atuin/config.toml."); return Ok(()); @@ -168,10 +175,7 @@ impl Cmd { name, value, no_export, - } => { - self.set(var_store, name.clone(), value.clone(), !no_export) - .await - } + } => self.set(var_store, name.clone(), value.clone(), !no_export).await, Self::Delete { name } => self.delete(var_store, name.clone()).await, Self::List { sort_by, diff --git a/crates/atuin/src/command/client/history.rs b/crates/atuin/src/command/client/history.rs index 4980e73bb..c75087675 100644 --- a/crates/atuin/src/command/client/history.rs +++ b/crates/atuin/src/command/client/history.rs @@ -1,48 +1,36 @@ -use std::{ - fmt::{self, Display}, - io::{self, IsTerminal, Write}, - time::Duration, -}; +use std::fmt::{self, Display}; +use std::io::{self, IsTerminal, Write}; +use std::time::Duration; +use atuin_client::database::{Database, Sqlite, current_context}; +use atuin_client::history::History; +use atuin_client::history::store::HistoryStore; +#[cfg(feature = "sync")] +use atuin_client::record; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::FilterMode::{Directory, Global, Session}; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; use atuin_common::logs::LogConfig; +use atuin_common::string::{EscapeNonPrintablePosixExt as _, NonNulStr}; use atuin_common::time::{DurationExt, OffsetDateTimeExt, UtcOffsetSpec}; -use atuin_common::{ - string::{EscapeNonPrintablePosixExt as _, NonNulStr}, - utils, -}; -use clap::Subcommand; -use eyre::{Context, Result, bail}; -use runtime_format::{FormatKey, FormatKeyError, ParseSegment, ParsedFmt}; - -#[cfg(feature = "daemon")] -use super::daemon as daemon_cmd; -#[cfg(feature = "daemon")] -use colored::Colorize; -#[cfg(feature = "daemon")] -use serde::Serialize; - +use atuin_common::utils; #[cfg(feature = "daemon")] use atuin_common::utils::normalize_optional_string; #[cfg(feature = "daemon")] use atuin_daemon::history::{HistoryEventKind, TailHistoryReply}; - -use atuin_client::{ - database::{Database, Sqlite, current_context}, - history::{History, store::HistoryStore}, - record::sqlite_store::SqliteStore, - settings::{ - FilterMode::{Directory, Global, Session}, - Settings, - }, -}; -use atuin_common::encryption::paseto_v4; - -#[cfg(feature = "sync")] -use atuin_client::record; - +use clap::Subcommand; +#[cfg(feature = "daemon")] +use colored::Colorize; +use eyre::{Context, Result, bail}; +use runtime_format::{FormatKey, FormatKeyError, ParseSegment, ParsedFmt}; +#[cfg(feature = "daemon")] +use serde::Serialize; use time::OffsetDateTime; use tracing::{debug, warn}; +#[cfg(feature = "daemon")] +use super::daemon as daemon_cmd; #[cfg(feature = "daemon")] use super::daemon; @@ -213,12 +201,8 @@ pub fn print_list( let mut w = w.lock(); let fmt_str = match list_mode { - ListMode::Human => format - .unwrap_or("{time} · {duration}\t{command}") - .replace("\\t", "\t"), - ListMode::Regular => format - .unwrap_or("{time}\t{command}\t{duration}") - .replace("\\t", "\t"), + ListMode::Human => format.unwrap_or("{time} · {duration}\t{command}").replace("\\t", "\t"), + ListMode::Regular => format.unwrap_or("{time}\t{command}\t{duration}").replace("\\t", "\t"), // not used ListMode::CmdOnly => String::new(), }; @@ -234,7 +218,11 @@ pub fn print_list( Box::new(h.iter()) as Box> }; - let entry_terminator = if print0 { "\0" } else { "\n" }; + let entry_terminator = if print0 { + "\0" + } else { + "\n" + }; let flush_each_line = print0; for history in iterator { @@ -268,10 +256,12 @@ pub fn print_list( Err(_) => { eprintln!("ERROR: Format string caused a formatting error."); eprintln!( - "This may be due to an unsupported format string containing special characters." + "This may be due to an unsupported format string containing special \ + characters." ); eprintln!( - "Please check your format string syntax and ensure literal braces are properly escaped." + "Please check your format string syntax and ensure literal braces are \ + properly escaped." ); std::process::exit(1); } @@ -337,12 +327,7 @@ impl FormatKey for FmtHistory<'_> { write!(f, "{}", dur.display().largest_unit())?; } "time" => { - self.history - .timestamp - .to_offset(self.tz.0) - .display() - .ymd_hms() - .fmt(f)?; + self.history.timestamp.to_offset(self.tz.0).display().ymd_hms().fmt(f)?; } "relativetime" => { let d = OffsetDateTime::now_utc().saturating_duration_since(self.history.timestamp); @@ -356,12 +341,9 @@ impl FormatKey for FmtHistory<'_> { )?, "author" => f.write_str(&self.history.author)?, "intent" => f.write_str(self.history.intent.as_deref().unwrap_or_default())?, - "user" => f.write_str( - self.history - .hostname - .split_once(':') - .map_or("", |(_, user)| user), - )?, + "user" => { + f.write_str(self.history.hostname.split_once(':').map_or("", |(_, user)| user))?; + } "session" => f.write_str(&self.history.session)?, "uuid" => f.write_str(&self.history.id.0)?, _ => return Err(FormatKeyError::UnknownKey), @@ -382,7 +364,8 @@ fn parse_fmt(format: &str) -> ParsedFmt<'_> { eprintln!("Example: '{{\"command\":\"{{command}}\",\"time\":\"{{time}}\"}}'"); } else { eprintln!( - "If your formatting string contains literal curly braces, you need to escape them by doubling:" + "If your formatting string contains literal curly braces, you need to escape \ + them by doubling:" ); eprintln!("Use {{{{ for literal {{ and }}}} for literal }}"); } @@ -401,12 +384,8 @@ fn normalize_command_for_storage<'a>(command: &'a str, settings: &Settings) -> & return command; } - let trailing_backslashes = trimmed - .as_bytes() - .iter() - .rev() - .take_while(|&&byte| byte == b'\\') - .count(); + let trailing_backslashes = + trimmed.as_bytes().iter().rev().take_while(|&&byte| byte == b'\\').count(); if trailing_backslashes % 2 == 1 { command @@ -430,10 +409,7 @@ fn make_starting_history( // (argv entries are C strings), so it can only come from a broken caller such // as an agent hook. Drop it rather than committing garbage to history. if let Err(err) = NonNulStr::new(command) { - debug!( - "dropping command containing a NUL byte at index {}", - err.index - ); + debug!("dropping command containing a NUL byte at index {}", err.index); return None; } @@ -718,13 +694,7 @@ impl TailEvent { event: self.kind.as_str(), history: TailJsonHistory { id: &self.history.id.0, - timestamp: self - .history - .timestamp - .to_offset(tz.0) - .display() - .ymd_hms() - .to_string(), + timestamp: self.history.timestamp.to_offset(tz.0).display().ymd_hms().to_string(), timestamp_unix_ns: u64::try_from(self.history.timestamp.unix_timestamp_nanos()) .context("history timestamp predates unix epoch")?, command: &self.history.command, @@ -738,10 +708,7 @@ impl TailEvent { exit: self.exit_value(), duration_ns: self.duration_value(), duration: self.duration_value().map(|d| { - Duration::saturating_from_nanos_i64(d) - .display() - .largest_unit() - .to_string() + Duration::saturating_from_nanos_i64(d).display().largest_unit().to_string() }), success: self.success_value(), finished_at: self @@ -784,13 +751,7 @@ impl TailEvent { push_pretty_field( &mut out, "start", - &self - .history - .timestamp - .to_offset(tz.0) - .display() - .ymd_hms() - .to_string(), + &self.history.timestamp.to_offset(tz.0).display().ymd_hms().to_string(), ); push_pretty_field(&mut out, "history", &self.history.id.0); push_pretty_field(&mut out, "session", &self.history.session); @@ -827,10 +788,7 @@ impl TailEvent { } fn user(&self) -> &str { - self.history - .hostname - .split_once(':') - .map_or("", |(_, user)| user) + self.history.hostname.split_once(':').map_or("", |(_, user)| user) } fn exit_value(&self) -> Option { @@ -862,10 +820,9 @@ impl TailEvent { fn duration_display(&self) -> String { match self.duration_value() { - Some(duration) if duration >= 0 => Duration::saturating_from_nanos_i64(duration) - .display() - .largest_unit() - .to_string(), + Some(duration) if duration >= 0 => { + Duration::saturating_from_nanos_i64(duration).display().largest_unit().to_string() + } Some(_) => "unknown".bright_yellow().to_string(), None => "running".bright_yellow().to_string(), } @@ -955,15 +912,10 @@ impl Cmd { (true, true) => [Session, Directory], (true, false) => [Session, Global], (false, true) => [Global, Directory], - (false, false) => [ - settings.default_filter_mode(context.git_root.is_some()), - Global, - ], + (false, false) => [settings.default_filter_mode(context.git_root.is_some()), Global], }; - let history = db - .list(&filters, &context, None, false, include_deleted, None) - .await?; + let history = db.list(&filters, &context, None, false, include_deleted, None).await?; print_list( &history, @@ -1042,7 +994,8 @@ impl Cmd { ) -> Result<()> { if dupkeep == 0 { eprintln!( - "\"--dupkeep 0\" would keep 0 copies of duplicate commands and thus delete all of them! Use \"atuin search --delete ...\" if you really want that." + "\"--dupkeep 0\" would keep 0 copies of duplicate commands and thus delete all of \ + them! Use \"atuin search --delete ...\" if you really want that." ); std::process::exit(1); } @@ -1228,11 +1181,11 @@ impl Cmd { #[cfg(test)] mod tests { + use rstest::{fixture, rstest}; #[cfg(feature = "daemon")] use time::macros::datetime; use super::*; - use rstest::{fixture, rstest}; #[fixture] async fn db() -> Sqlite { @@ -1272,9 +1225,7 @@ mod tests { ..Settings::utc() }; - handle_start(&db, &settings, "ls \t", None, None) - .await - .unwrap(); + handle_start(&db, &settings, "ls \t", None, None).await.unwrap(); let history = db .before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1) @@ -1292,15 +1243,11 @@ mod tests { // A command containing a NUL byte can never have been executed by a shell; // it should be dropped rather than committed to history. - let id = handle_start(&db, &settings, "hello\0world", None, None) - .await - .unwrap(); + let id = handle_start(&db, &settings, "hello\0world", None, None).await.unwrap(); assert!(id.is_none()); - let stored = db - .before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1) - .await - .unwrap(); + let stored = + db.before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1).await.unwrap(); assert!(stored.is_empty()); } @@ -1338,9 +1285,7 @@ mod tests { #[cfg(feature = "daemon")] #[rstest] fn test_tail_json_output_contains_history_fields(tail_event: TailEvent) { - let json = tail_event - .render(false, UtcOffsetSpec(time::UtcOffset::UTC)) - .unwrap(); + let json = tail_event.render(false, UtcOffsetSpec(time::UtcOffset::UTC)).unwrap(); let value: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(value["event"], "ended"); @@ -1355,12 +1300,8 @@ mod tests { fn test_tail_pretty_output_shows_pending_fields_for_started_events( #[with(TailKind::Started)] tail_event: TailEvent, ) { - let rendered = tail_event - .render(true, UtcOffsetSpec(time::UtcOffset::UTC)) - .unwrap(); - let plain = regex::Regex::new(r"\x1b\[[0-9;]*m") - .unwrap() - .replace_all(&rendered, ""); + let rendered = tail_event.render(true, UtcOffsetSpec(time::UtcOffset::UTC)).unwrap(); + let plain = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap().replace_all(&rendered, ""); assert!(plain.contains("STARTED git status")); assert!(plain.contains("exit:")); diff --git a/crates/atuin/src/command/client/hook.rs b/crates/atuin/src/command/client/hook.rs index 87a277f91..2037951ca 100644 --- a/crates/atuin/src/command/client/hook.rs +++ b/crates/atuin/src/command/client/hook.rs @@ -46,8 +46,7 @@ enum PathRoot { /// empty value as unset. Taking it literally would resolve to a relative path /// and install under the current directory, where the agent never looks. fn xdg_config_home(var: Option) -> PathBuf { - var.filter(|value| !value.is_empty()) - .map_or_else(|| home_dir().join(".config"), PathBuf::from) + var.filter(|value| !value.is_empty()).map_or_else(|| home_dir().join(".config"), PathBuf::from) } struct AgentSpec { @@ -107,16 +106,13 @@ struct Agent(&'static AgentSpec); impl Agent { fn from_name(name: &str) -> Result { - AGENTS - .iter() - .copied() - .find(|spec| spec.aliases.contains(&name)) - .map(Self) - .ok_or_else(|| { + AGENTS.iter().copied().find(|spec| spec.aliases.contains(&name)).map(Self).ok_or_else( + || { eyre::eyre!( "unknown agent: {name}. Supported agents: claude-code, codex, opencode, pi" ) - }) + }, + ) } fn actor_name(&self) -> &'static str { @@ -182,7 +178,8 @@ async fn handle(agent_name: &str, settings: &Settings) -> Result<()> { if let InstallKind::Extension { reload_hint, .. } = agent.install_kind() { bail!( - "`atuin hook {agent_name}` is not supported. Use `atuin hook install {agent_name}`. {reload_hint}" + "`atuin hook {agent_name}` is not supported. Use `atuin hook install {agent_name}`. \ + {reload_hint}" ); } @@ -320,14 +317,11 @@ fn add_hook_entries(hooks: &mut Value, agent: &Agent) -> Result<()> { .ok_or_else(|| eyre::eyre!("hooks.{event_type} is not an array"))?; let already_installed = arr.iter().any(|entry| { - entry - .get("hooks") - .and_then(Value::as_array) - .is_some_and(|hooks| { - hooks.iter().any(|hook| { - hook.get("command").and_then(Value::as_str) == Some(hook_command) - }) - }) + entry.get("hooks").and_then(Value::as_array).is_some_and(|hooks| { + hooks + .iter() + .any(|hook| hook.get("command").and_then(Value::as_str) == Some(hook_command)) + }) }); if already_installed { @@ -347,23 +341,19 @@ fn add_hook_entries(hooks: &mut Value, agent: &Agent) -> Result<()> { #[cfg(test)] mod tests { - use super::*; - use crate::{ - Atuin, - command::{AtuinCmd, client}, - }; use atuin_client::history::is_known_agent; use clap::Parser; use rstest::rstest; + use super::*; + use crate::Atuin; + use crate::command::{AtuinCmd, client}; + #[test] fn parse_hook_agent_command() { let cmd = Cmd::try_parse_from(["hook", "codex"]).unwrap(); - assert!(matches!( - (cmd.action, cmd.agent.as_deref()), - (None, Some("codex")) - )); + assert!(matches!((cmd.action, cmd.agent.as_deref()), (None, Some("codex")))); } #[rstest] @@ -385,10 +375,7 @@ mod tests { fn agent_from_name_supports_extension_agents(#[case] agent_name: &str) { let agent = Agent::from_name(agent_name).unwrap(); assert_eq!(agent.actor_name(), agent_name); - assert!(matches!( - agent.install_kind(), - InstallKind::Extension { .. } - )); + assert!(matches!(agent.install_kind(), InstallKind::Extension { .. })); } /// An agent missing from `KNOWN_AGENTS` would be installable but invisible @@ -426,10 +413,7 @@ mod tests { let root = xdg_config_home(std::env::var_os("XDG_CONFIG_HOME")); let installed = agent.path(extension_path); - assert!( - installed.starts_with(&root), - "{installed:?} is not under {root:?}" - ); + assert!(installed.starts_with(&root), "{installed:?} is not under {root:?}"); assert!(installed.ends_with("opencode/plugins/atuin.ts")); } diff --git a/crates/atuin/src/command/client/hook/event.rs b/crates/atuin/src/command/client/hook/event.rs index eaae59555..1fda86a6a 100644 --- a/crates/atuin/src/command/client/hook/event.rs +++ b/crates/atuin/src/command/client/hook/event.rs @@ -4,9 +4,8 @@ //! an event we care about. If we don't know how to deserialize it/don't care for it, we need to //! drop that on the floor. The [`HookEvent`] type represents agent events we know/care about. -use serde_json::error::Category; - use atuin_common::string::NonNulStr; +use serde_json::error::Category; use super::wire::{HookEventName, WireHookEvent, WireToolName}; @@ -39,7 +38,10 @@ pub enum HookEvent { tool_use_id: String, }, /// A Bash command finished; close the matching history entry. - End { tool_use_id: String, exit: i64 }, + End { + tool_use_id: String, + exit: i64, + }, } impl From for Option { @@ -78,10 +80,7 @@ impl From for Option { }) } HookEventName::PostToolUse => { - let exit = wire - .tool_response - .and_then(|response| response.exit_code) - .unwrap_or(0); + let exit = wire.tool_response.and_then(|response| response.exit_code).unwrap_or(0); Some(HookEvent::End { tool_use_id, exit }) } HookEventName::PostToolUseFailure => Some(HookEvent::End { @@ -117,11 +116,12 @@ impl HookEvent { #[cfg(test)] mod tests { - use super::*; use proptest::prelude::*; use rstest::rstest; use serde_json::json; + use super::*; + fn non_nul(s: &str) -> NonNulStr { NonNulStr::new(s.to_owned()).unwrap() } @@ -290,10 +290,7 @@ mod tests { None )] fn parses_agent_event(#[case] input: serde_json::Value, #[case] expected: Option) { - assert_eq!( - HookEvent::from_json_str(&input.to_string()).unwrap(), - expected - ); + assert_eq!(HookEvent::from_json_str(&input.to_string()).unwrap(), expected); } /// Well-formed JSON that isn't a hook event we model is skipped, not an @@ -317,10 +314,7 @@ mod tests { let ParseError::MalformedJson { line, column, .. } = HookEvent::from_json_str(input).unwrap_err(); - assert!( - line >= 1 && column >= 1, - "position should be 1-based, got {line}:{column}" - ); + assert!(line >= 1 && column >= 1, "position should be 1-based, got {line}:{column}"); } proptest! { diff --git a/crates/atuin/src/command/client/hook/wire.rs b/crates/atuin/src/command/client/hook/wire.rs index 76a82b381..811d5d5f9 100644 --- a/crates/atuin/src/command/client/hook/wire.rs +++ b/crates/atuin/src/command/client/hook/wire.rs @@ -3,9 +3,8 @@ //! Claude Code and Codex invoke `atuin hook ` for each tool use and //! pass the event as JSON on stdin. -use serde::Deserialize; - use atuin_common::string::NonNulStr; +use serde::Deserialize; #[derive(Debug, Deserialize)] pub enum WireToolName { diff --git a/crates/atuin/src/command/client/import.rs b/crates/atuin/src/command/client/import.rs index 1f72de30e..74e6bfe9c 100644 --- a/crates/atuin/src/command/client/import.rs +++ b/crates/atuin/src/command/client/import.rs @@ -1,20 +1,24 @@ use std::env; use async_trait::async_trait; +use atuin_client::database::Database; +use atuin_client::history::History; +use atuin_client::import::bash::Bash; +use atuin_client::import::fish::Fish; +use atuin_client::import::nu::Nu; +use atuin_client::import::nu_histdb::NuHistDb; +use atuin_client::import::powershell::PowerShell; +use atuin_client::import::replxx::Replxx; +use atuin_client::import::resh::Resh; +use atuin_client::import::xonsh::Xonsh; +use atuin_client::import::xonsh_sqlite::XonshSqlite; +use atuin_client::import::zsh::Zsh; +use atuin_client::import::zsh_histdb::ZshHistDb; +use atuin_client::import::{Importer, Loader}; use clap::Parser; use eyre::Result; use indicatif::ProgressBar; -use atuin_client::{ - database::Database, - history::History, - import::{ - Importer, Loader, bash::Bash, fish::Fish, nu::Nu, nu_histdb::NuHistDb, - powershell::PowerShell, replxx::Replxx, resh::Resh, xonsh::Xonsh, - xonsh_sqlite::XonshSqlite, zsh::Zsh, zsh_histdb::ZshHistDb, - }, -}; - #[derive(Parser, Debug)] #[command(infer_subcommands = true)] pub enum Cmd { diff --git a/crates/atuin/src/command/client/info.rs b/crates/atuin/src/command/client/info.rs index 416a39ed9..98fb37820 100644 --- a/crates/atuin/src/command/client/info.rs +++ b/crates/atuin/src/command/client/info.rs @@ -1,31 +1,32 @@ -use atuin_client::settings::Settings; - -use crate::{SHA, VERSION}; - -pub fn run(settings: &Settings) { - let config = atuin_common::utils::config_dir(); - let mut config_file = config.clone(); - config_file.push("config.toml"); - let mut sever_config = config; - sever_config.push("server.toml"); - - let config_paths = format!( - "Config files:\nclient config: {:?}\nserver config: {:?}\nclient db path: {}\nkey path: {}\nmeta db path: {}", - config_file.to_string_lossy(), - sever_config.to_string_lossy(), - settings.db_path.display(), - settings.key_path.display(), - settings.meta.db_path - ); - - let env_vars = format!( - "Env Vars:\nATUIN_CONFIG_DIR = {:?}", - std::env::var("ATUIN_CONFIG_DIR").unwrap_or_else(|_| "None".into()) - ); - - let general_info = format!("Version info:\nversion: {VERSION}\ncommit: {SHA}"); - - let print_out = format!("{config_paths}\n\n{env_vars}\n\n{general_info}"); - - println!("{print_out}"); -} +use atuin_client::settings::Settings; + +use crate::{SHA, VERSION}; + +pub fn run(settings: &Settings) { + let config = atuin_common::utils::config_dir(); + let mut config_file = config.clone(); + config_file.push("config.toml"); + let mut sever_config = config; + sever_config.push("server.toml"); + + let config_paths = format!( + "Config files:\nclient config: {:?}\nserver config: {:?}\nclient db path: {}\nkey path: \ + {}\nmeta db path: {}", + config_file.to_string_lossy(), + sever_config.to_string_lossy(), + settings.db_path.display(), + settings.key_path.display(), + settings.meta.db_path + ); + + let env_vars = format!( + "Env Vars:\nATUIN_CONFIG_DIR = {:?}", + std::env::var("ATUIN_CONFIG_DIR").unwrap_or_else(|_| "None".into()) + ); + + let general_info = format!("Version info:\nversion: {VERSION}\ncommit: {SHA}"); + + let print_out = format!("{config_paths}\n\n{env_vars}\n\n{general_info}"); + + println!("{print_out}"); +} diff --git a/crates/atuin/src/command/client/init.rs b/crates/atuin/src/command/client/init.rs index a81eb9f36..dffc40e50 100644 --- a/crates/atuin/src/command/client/init.rs +++ b/crates/atuin/src/command/client/init.rs @@ -1,9 +1,8 @@ -use atuin_client::{ - record::sqlite_store::SqliteStore, - settings::{Settings, Tmux}, -}; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::{Settings, Tmux}; use atuin_common::encryption::paseto_v4; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use clap::{Parser, ValueEnum}; use eyre::{Result, WrapErr}; @@ -146,7 +145,8 @@ impl Cmd { Shell::Nu => atuin_pty_proxy::Shell::Nu, Shell::Xonsh | Shell::PowerShell => { eprintln!( - "atuin: pty_proxy.enabled is set, but atuin pty-proxy does not support this shell" + "atuin: pty_proxy.enabled is set, but atuin pty-proxy does not support this \ + shell" ); return; } @@ -159,7 +159,8 @@ impl Cmd { fn pty_proxy_init(&self, settings: &Settings) { if settings.pty_proxy.enabled { eprintln!( - "atuin: pty_proxy.enabled is set, but this build of atuin does not include pty-proxy support" + "atuin: pty_proxy.enabled is set, but this build of atuin does not include \ + pty-proxy support" ); } } @@ -167,7 +168,8 @@ impl Cmd { pub async fn run(self, settings: &Settings) -> Result<()> { if !settings.paths_ok() { eprintln!( - "Atuin settings paths are broken. Disabling atuin shell hooks. Run `atuin doctor` to diagnose." + "Atuin settings paths are broken. Disabling atuin shell hooks. Run `atuin doctor` \ + to diagnose." ); return Ok(()); } diff --git a/crates/atuin/src/command/client/init/bash.rs b/crates/atuin/src/command/client/init/bash.rs index ead05f406..6ec2819d0 100644 --- a/crates/atuin/src/command/client/init/bash.rs +++ b/crates/atuin/src/command/client/init/bash.rs @@ -1,9 +1,12 @@ +use std::io::{self, Write}; + +use atuin_client::settings::Tmux; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; +use eyre::Result; + use super::StaticInitOptions; use crate::shell::BASH; -use atuin_client::settings::Tmux; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; -use eyre::Result; -use std::io::{self, Write}; fn write_tmux_config(writer: &mut W, tmux: &Tmux) -> io::Result<()> { if tmux.enabled { @@ -24,10 +27,7 @@ fn write_static_init(writer: &mut W, options: &StaticInitOptions<'_>) writeln!(writer, "{} && {{", BASH.include_guard)?; if std::env::var_os("ATUIN_NO_BUILTIN_PREEXEC").is_none_or(|s| s.is_empty()) { - writeln!( - writer, - "# Set ATUIN_NO_BUILTIN_PREEXEC=1 to disable loading bash-preexec" - )?; + writeln!(writer, "# Set ATUIN_NO_BUILTIN_PREEXEC=1 to disable loading bash-preexec")?; writeln!(writer, "__atuin_load_builtin_preexec() {{")?; for line in BASH.preexec.lines() { writeln!(writer, " {line}")?; diff --git a/crates/atuin/src/command/client/init/fish.rs b/crates/atuin/src/command/client/init/fish.rs index 826de051c..3a1098f91 100644 --- a/crates/atuin/src/command/client/init/fish.rs +++ b/crates/atuin/src/command/client/init/fish.rs @@ -1,8 +1,10 @@ -use super::StaticInitOptions; use atuin_client::settings::Tmux; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use eyre::Result; +use super::StaticInitOptions; + fn print_tmux_config(tmux: &Tmux) { if tmux.enabled { println!("set -gx ATUIN_TMUX_POPUP_WIDTH '{}'", tmux.width); diff --git a/crates/atuin/src/command/client/init/powershell.rs b/crates/atuin/src/command/client/init/powershell.rs index aa2dd0ad2..eef1fdc3c 100644 --- a/crates/atuin/src/command/client/init/powershell.rs +++ b/crates/atuin/src/command/client/init/powershell.rs @@ -1,5 +1,7 @@ +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; + use super::StaticInitOptions; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; pub fn init_static(options: &StaticInitOptions<'_>) { let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() { @@ -34,5 +36,9 @@ pub async fn init( } fn ps_bool(value: bool) -> &'static str { - if value { "$true" } else { "$false" } + if value { + "$true" + } else { + "$false" + } } diff --git a/crates/atuin/src/command/client/init/xonsh.rs b/crates/atuin/src/command/client/init/xonsh.rs index d096530e1..cffd51992 100644 --- a/crates/atuin/src/command/client/init/xonsh.rs +++ b/crates/atuin/src/command/client/init/xonsh.rs @@ -1,7 +1,9 @@ -use super::StaticInitOptions; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use eyre::Result; +use super::StaticInitOptions; + pub fn init_static(options: &StaticInitOptions<'_>) { let (bind_ctrl_r, bind_up_arrow) = if std::env::var("ATUIN_NOBIND").is_ok() { (false, false) @@ -12,11 +14,19 @@ pub fn init_static(options: &StaticInitOptions<'_>) { // TODO: tmux popup for xonsh println!( "_ATUIN_BIND_CTRL_R={}", - if bind_ctrl_r { "True" } else { "False" } + if bind_ctrl_r { + "True" + } else { + "False" + } ); println!( "_ATUIN_BIND_UP_ARROW={}", - if bind_up_arrow { "True" } else { "False" } + if bind_up_arrow { + "True" + } else { + "False" + } ); println!("{}", crate::shell::XONSH); } diff --git a/crates/atuin/src/command/client/init/zsh.rs b/crates/atuin/src/command/client/init/zsh.rs index 88d81eacb..fe59e8343 100644 --- a/crates/atuin/src/command/client/init/zsh.rs +++ b/crates/atuin/src/command/client/init/zsh.rs @@ -1,8 +1,10 @@ -use super::StaticInitOptions; use atuin_client::settings::Tmux; -use atuin_dotfiles::store::{AliasStore, var::VarStore}; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use eyre::Result; +use super::StaticInitOptions; + fn print_tmux_config(tmux: &Tmux) { if tmux.enabled { println!("export ATUIN_TMUX_POPUP_WIDTH='{}'", tmux.width); diff --git a/crates/atuin/src/command/client/kv.rs b/crates/atuin/src/command/client/kv.rs index 9d4f5fbd1..7cfbfc577 100644 --- a/crates/atuin/src/command/client/kv.rs +++ b/crates/atuin/src/command/client/kv.rs @@ -1,11 +1,11 @@ use std::io::{self, IsTerminal, Read}; -use clap::Subcommand; -use eyre::{Context, Result, eyre}; - -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use atuin_common::encryption::paseto_v4; use atuin_kv::store::KvStore; +use clap::Subcommand; +use eyre::{Context, Result, eyre}; #[derive(Subcommand, Debug)] #[command(infer_subcommands = true)] @@ -91,9 +91,7 @@ impl Cmd { .context("failed to read value from stdin")?; buf } else { - return Err(eyre!( - "no value provided. Pass as an argument or pipe via stdin" - )); + return Err(eyre!("no value provided. Pass as an argument or pipe via stdin")); }; kv_store.set(namespace, key, &value).await diff --git a/crates/atuin/src/command/client/lab.rs b/crates/atuin/src/command/client/lab.rs index 137855fbf..82a5e2cd6 100644 --- a/crates/atuin/src/command/client/lab.rs +++ b/crates/atuin/src/command/client/lab.rs @@ -3,14 +3,13 @@ use std::process::{Command, Stdio}; #[cfg(unix)] use std::time::{Duration, Instant}; +use atuin_client::settings::Settings; use clap::Subcommand; #[cfg(unix)] use eyre::bail; use eyre::{Result, WrapErr}; use url::Url; -use atuin_client::settings::Settings; - #[derive(Subcommand, Debug)] pub enum Cmd { /// Share your terminal with others (experimental) @@ -143,10 +142,8 @@ impl Cmd { /// before any session, pidfile lock, spawned child, or raw-mode terminal /// exists, so there is nothing whose `Drop` must run. fn report_refusal(result: Result<()>) -> Result<()> { - if let Some(refusal @ atuin_lab_share::Error::ActiveShareUnsupported) = result - .as_ref() - .err() - .and_then(eyre::Report::downcast_ref::) + if let Some(refusal @ atuin_lab_share::Error::ActiveShareUnsupported) = + result.as_ref().err().and_then(eyre::Report::downcast_ref::) { eprintln!("error: {refusal}"); std::process::exit(1); @@ -184,20 +181,21 @@ const STOP_TIMED_OUT: &str = #[cfg(unix)] const URL_STILL_CONNECTING: &str = "the share is still connecting; try again shortly"; #[cfg(unix)] -const SPAWN_TIMED_OUT: &str = "timed out waiting for the background share to connect; run \ - `atuin lab share --active --foreground` to debug, or `atuin lab share --stop` to clean up"; +const SPAWN_TIMED_OUT: &str = "timed out waiting for the background share to connect; run `atuin \ + lab share --active --foreground` to debug, or `atuin lab share \ + --stop` to clean up"; #[cfg(unix)] -const SPAWN_CHILD_DIED: &str = "the background share exited before publishing a URL; run \ - `atuin lab share --active --foreground` to see why"; +const SPAWN_CHILD_DIED: &str = "the background share exited before publishing a URL; run `atuin \ + lab share --active --foreground` to see why"; /// The success copy after the background share published its URL. Pure /// ASCII, byte-pinned by a test. #[cfg(unix)] fn attach_copy(url: &str) -> String { format!( - "Sharing this session at: {url}\n\ - Run `atuin lab share --stop` to end sharing. `atuin lab share --url` reprints the link.\n\ - This session has no warning bar; viewers stay connected until you stop or the shell exits." + "Sharing this session at: {url}\nRun `atuin lab share --stop` to end sharing. `atuin lab \ + share --url` reprints the link.\nThis session has no warning bar; viewers stay connected \ + until you stop or the shell exits." ) } @@ -247,12 +245,8 @@ async fn spawn_background_share(write: bool, yes: bool) -> Result<()> { cmd.arg("--write"); } cmd.env(lifecycle::SPAWN_ID_ENV, &spawn_id); - cmd.stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - let mut child = cmd - .spawn() - .wrap_err("failed to spawn the background share process")?; + cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + let mut child = cmd.spawn().wrap_err("failed to spawn the background share process")?; let start = Instant::now(); loop { @@ -299,10 +293,7 @@ async fn stop_share() -> Result<()> { } let Some(pid) = lifecycle::read_pidfile_pid(&pidfile) else { - bail!( - "a share is running but its pidfile at {} is unreadable", - pidfile.display() - ); + bail!("a share is running but its pidfile at {} is unreadable", pidfile.display()); }; // Remember whose URL file we are about to orphan, so the backstop @@ -370,7 +361,11 @@ fn lab_ws_url(settings: &Settings) -> Result { return Url::parse(&u).wrap_err("ATUIN_LAB_HUB_URL is not a valid URL"); } let mut url = settings.hub_endpoint(); - let ws_scheme = if url.scheme() == "http" { "ws" } else { "wss" }; + let ws_scheme = if url.scheme() == "http" { + "ws" + } else { + "wss" + }; let _ = url.set_scheme(ws_scheme); Ok(url) } @@ -408,9 +403,9 @@ mod tests { let copy = attach_copy("https://hub.example/s/abc#key"); assert_eq!( copy, - "Sharing this session at: https://hub.example/s/abc#key\n\ - Run `atuin lab share --stop` to end sharing. `atuin lab share --url` reprints the link.\n\ - This session has no warning bar; viewers stay connected until you stop or the shell exits." + "Sharing this session at: https://hub.example/s/abc#key\nRun `atuin lab share --stop` \ + to end sharing. `atuin lab share --url` reprints the link.\nThis session has no \ + warning bar; viewers stay connected until you stop or the shell exits." ); assert!(copy.is_ascii()); } diff --git a/crates/atuin/src/command/client/scripts.rs b/crates/atuin/src/command/client/scripts.rs index 91c4431a4..76b0e6fc5 100644 --- a/crates/atuin/src/command/client/scripts.rs +++ b/crates/atuin/src/command/client/scripts.rs @@ -1,21 +1,19 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::io::IsTerminal; -use std::io::Read; +use std::collections::{HashMap, HashSet}; +use std::io::{IsTerminal, Read}; use std::path::PathBuf; -use atuin_scripts::execution::template_script; -use atuin_scripts::{ - execution::{build_executable_script, execute_script_interactive, template_variables}, - store::{ScriptStore, script::Script}, -}; -use clap::{Parser, Subcommand}; -use eyre::OptionExt; -use eyre::{Result, bail}; -use tempfile::NamedTempFile; - -use atuin_client::{database::Database, record::sqlite_store::SqliteStore, settings::Settings}; +use atuin_client::database::Database; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use atuin_common::encryption::paseto_v4; +use atuin_scripts::execution::{ + build_executable_script, execute_script_interactive, template_script, template_variables, +}; +use atuin_scripts::store::ScriptStore; +use atuin_scripts::store::script::Script; +use clap::{Parser, Subcommand}; +use eyre::{OptionExt, Result, bail}; +use tempfile::NamedTempFile; use tracing::debug; #[derive(Parser, Debug)] @@ -141,10 +139,7 @@ impl Cmd { let parts = shlex::split(&editor_str).ok_or_eyre("Failed to parse editor command")?; let (command, args) = parts.split_first().ok_or_eyre("No editor command found")?; - let status = std::process::Command::new(command) - .args(args) - .arg(&path) - .status()?; + let status = std::process::Command::new(command).args(args).arg(&path).status()?; if !status.success() { bail!("failed to open editor"); } @@ -235,9 +230,8 @@ impl Cmd { // Get the last N+1 commands, filtering by the default mode let filters = [settings.default_filter_mode(context.git_root.is_some())]; - let mut history = history_db - .list(&filters, &context, Some(count), false, false, None) - .await?; + let mut history = + history_db.list(&filters, &context, Some(count), false, false, None).await?; // Reverse to get chronological order history.reverse(); @@ -308,10 +302,8 @@ impl Cmd { for var_str in &run.var { if let Some((key, value)) = var_str.split_once('=') { // Add to variable values - variable_values.insert( - key.to_string(), - serde_json::Value::String(value.to_string()), - ); + variable_values + .insert(key.to_string(), serde_json::Value::String(value.to_string())); debug!("Using CLI variable: {}={}", key, value); } else { eprintln!("Warning: Ignoring malformed variable specification: {var_str}"); @@ -320,10 +312,8 @@ impl Cmd { } // Collect variables that are still needed (not specified via CLI) - let remaining_vars: HashSet = variables - .into_iter() - .filter(|var| !variable_values.contains_key(var)) - .collect(); + let remaining_vars: HashSet = + variables.into_iter().filter(|var| !variable_values.contains_key(var)).collect(); // If there are variables in the template that weren't specified on the command line, prompt for them if !remaining_vars.is_empty() { @@ -404,10 +394,7 @@ impl Cmd { if let Some(script) = script { if get.script { // Just print the executable script with shebang - print!( - "{}", - build_executable_script(script.script.clone(), script.shebang) - ); + print!("{}", build_executable_script(script.script.clone(), script.shebang)); return Ok(()); } @@ -536,10 +523,7 @@ impl Cmd { if let Some(script) = script { // If not force, confirm deletion if !delete.force { - println!( - "Are you sure you want to delete script '{}'? [y/N]", - delete.name - ); + println!("Are you sure you want to delete script '{}'? [y/N]", delete.name); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; diff --git a/crates/atuin/src/command/client/search.rs b/crates/atuin/src/command/client/search.rs index 4df499c7f..41509c3b4 100644 --- a/crates/atuin/src/command/client/search.rs +++ b/crates/atuin/src/command/client/search.rs @@ -1,21 +1,19 @@ use std::fs::File; use std::io::{IsTerminal as _, Write, stderr, stdout}; +use atuin_client::database::{Database, OptFilters, current_context}; +use atuin_client::history::store::HistoryStore; +use atuin_client::history::{AuthorPattern, History}; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::{FilterMode, KeymapMode, RequestedSearchMode, Settings}; +use atuin_client::theme::Theme; +use atuin_common::encryption::paseto_v4; use atuin_common::filter::OrFilter; -use atuin_common::{string::EscapeNonPrintablePosixExt as _, utils}; +use atuin_common::string::EscapeNonPrintablePosixExt as _; +use atuin_common::utils; use clap::Parser; use eyre::Result; -use atuin_client::{ - database::Database, - database::{OptFilters, current_context}, - history::{AuthorPattern, History, store::HistoryStore}, - record::sqlite_store::SqliteStore, - settings::{FilterMode, KeymapMode, RequestedSearchMode, Settings}, - theme::Theme, -}; -use atuin_common::encryption::paseto_v4; - use super::history::ListMode; mod cursor; @@ -182,12 +180,7 @@ impl Cmd { let query = if self.query.is_empty() { std::env::var("ATUIN_QUERY").map_or_else( |_| vec![], - |query| { - query - .split(' ') - .map(std::string::ToString::to_string) - .collect() - }, + |query| query.split(' ').map(std::string::ToString::to_string).collect(), ) } else { self.query @@ -207,7 +200,8 @@ impl Cmd { 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" + "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(()); } @@ -303,10 +297,7 @@ impl Cmd { entries = run_non_interactive(settings, opt_filter, &query, &db).await?; } } else { - let format = self - .format - .as_deref() - .unwrap_or(settings.history_format.as_str()); + let format = self.format.as_deref().unwrap_or(settings.history_format.as_str()); let tz = self.timezone.unwrap_or(settings.timezone); super::history::print_list( @@ -372,10 +363,11 @@ pub async fn prepare_index(settings: &Settings) -> Result<()> { #[cfg(test)] mod tests { - use super::{AuthorPattern, Cmd}; use clap::Parser; use rstest::rstest; + use super::{AuthorPattern, Cmd}; + #[rstest] // triple_dash: Issue #3028 - searching for `---` should not be treated as a CLI flag #[case::triple_dash(vec!["search", "---"], vec!["---"])] @@ -390,13 +382,10 @@ mod tests { fn search_author_cli_flag() { let cmd = Cmd::try_parse_from(["search", "--author", "codex", "--author", "ellie"]).unwrap(); - assert_eq!( - cmd.author, - vec![ - AuthorPattern::Name("codex".to_owned()), - AuthorPattern::Name("ellie".to_owned()), - ], - ); + assert_eq!(cmd.author, vec![ + AuthorPattern::Name("codex".to_owned()), + AuthorPattern::Name("ellie".to_owned()), + ],); } #[rstest] @@ -411,14 +400,11 @@ mod tests { "$all-users", ]) .unwrap(); - assert_eq!( - cmd.author, - vec![ - AuthorPattern::AllUser, - AuthorPattern::AllAgent, - // Not a special value; a typo'd one is an author name, as it was before. - AuthorPattern::Name("$all-users".to_owned()), - ], - ); + assert_eq!(cmd.author, vec![ + AuthorPattern::AllUser, + AuthorPattern::AllAgent, + // Not a special value; a typo'd one is an author name, as it was before. + AuthorPattern::Name("$all-users".to_owned()), + ],); } } diff --git a/crates/atuin/src/command/client/search/cursor.rs b/crates/atuin/src/command/client/search/cursor.rs index f0a5a2b46..4d2998584 100644 --- a/crates/atuin/src/command/client/search/cursor.rs +++ b/crates/atuin/src/command/client/search/cursor.rs @@ -1,6 +1,7 @@ +use std::ops::Range; + use atuin_client::settings::WordJumpMode; use itertools::Itertools; -use std::ops::Range; /// Like [`str::char_indices`], but only yields characters whose byte positions /// are in `start..end`. @@ -16,9 +17,7 @@ fn chars_within( // `ceil_char_boundary` clamps to `string.len()` if out of bounds. Use // `.max` to prevent `end` from being less than `start`. let end = string.ceil_char_boundary(range.end).max(start); - string[start..end] - .char_indices() - .map(move |(i, c)| (start + i, c)) + string[start..end].char_indices().map(move |(i, c)| (start + i, c)) } pub struct Cursor { @@ -261,7 +260,11 @@ impl Cursor { } pub fn back(&mut self) -> Option { - if self.left() { self.remove() } else { None } + if self.left() { + self.remove() + } else { + None + } } pub fn clear(&mut self) { @@ -294,10 +297,10 @@ impl Cursor { #[cfg(test)] mod cursor_tests { - use super::Cursor; - use super::*; use rstest::rstest; + use super::{Cursor, *}; + static EMACS_WORD_JUMPER: WordJumper = WordJumper { word_chars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", word_jump_mode: WordJumpMode::Emacs, diff --git a/crates/atuin/src/command/client/search/engines.rs b/crates/atuin/src/command/client/search/engines.rs index 8885c57d2..66068a5d2 100644 --- a/crates/atuin/src/command/client/search/engines.rs +++ b/crates/atuin/src/command/client/search/engines.rs @@ -1,8 +1,6 @@ -use atuin_client::{ - database::{Context, Database, DbSearchMode, OptFilters}, - history::{History, HistoryId, all_user_author_filter}, - settings::{FilterMode, SearchMode, Settings, Shells}, -}; +use atuin_client::database::{Context, Database, DbSearchMode, OptFilters}; +use atuin_client::history::{History, HistoryId, all_user_author_filter}; +use atuin_client::settings::{FilterMode, SearchMode, Settings, Shells}; use enum_dispatch::enum_dispatch; use eyre::Result; @@ -38,12 +36,8 @@ pub struct SearchState { impl SearchState { pub(crate) fn rotate_filter_mode(&mut self, settings: &Settings, offset: isize) { - let mut i = settings - .search - .filters - .iter() - .position(|&m| m == self.filter_mode) - .unwrap_or_default(); + let mut i = + settings.search.filters.iter().position(|&m| m == self.filter_mode).unwrap_or_default(); for _ in 0..settings.search.filters.len() { i = (i.wrapping_add_signed(offset)) % settings.search.filters.len(); let mode = settings.search.filters[i]; @@ -75,18 +69,12 @@ pub trait SearchEngine: Send + Sync + 'static { if state.input.as_str().is_empty() { let shells = state.shells.to_filter(); Ok(db - .search( - DbSearchMode::FullText, - state.filter_mode, - &state.context, - "", - OptFilters { - limit: Some(200), - authors: all_user_author_filter(), - shells: shells.as_filter(), - ..Default::default() - }, - ) + .search(DbSearchMode::FullText, state.filter_mode, &state.context, "", OptFilters { + limit: Some(200), + authors: all_user_author_filter(), + shells: shells.as_filter(), + ..Default::default() + }) .await? .into_iter() .collect::>()) diff --git a/crates/atuin/src/command/client/search/engines/daemon.rs b/crates/atuin/src/command/client/search/engines/daemon.rs index 3bb1303e0..5dfb9a2b7 100644 --- a/crates/atuin/src/command/client/search/engines/daemon.rs +++ b/crates/atuin/src/command/client/search/engines/daemon.rs @@ -1,8 +1,6 @@ -use atuin_client::{ - database::{Database, DbSearchMode, OptFilters}, - history::{History, all_user_author_filter}, - settings::Settings, -}; +use atuin_client::database::{Database, DbSearchMode, OptFilters}; +use atuin_client::history::{History, all_user_author_filter}; +use atuin_client::settings::Settings; use atuin_daemon::client::{SearchClient, SearchParams}; use atuin_daemon::search::{normalize_diacritics, truncate_query}; use eyre::Result; @@ -274,10 +272,8 @@ impl SearchEngine for Search { .enumerate() .map(|(char_idx, (byte_idx, _))| (byte_idx, char_idx)) .collect(); - let command_char_to_byte: Vec = command - .char_indices() - .map(|(byte_idx, _)| byte_idx) - .collect(); + let command_char_to_byte: Vec = + command.char_indices().map(|(byte_idx, _)| byte_idx).collect(); let mut bytes: Vec = indices .into_iter() .filter_map(|i| matchable_byte_to_char.get(&(i as usize))) diff --git a/crates/atuin/src/command/client/search/engines/db.rs b/crates/atuin/src/command/client/search/engines/db.rs index 7c279707e..4dce4ef81 100644 --- a/crates/atuin/src/command/client/search/engines/db.rs +++ b/crates/atuin/src/command/client/search/engines/db.rs @@ -1,16 +1,14 @@ -use super::{SearchEngine, SearchState}; -use atuin_client::{ - database::Database, - database::OptFilters, - database::{DbSearchMode, QueryToken, QueryTokenizer}, - history::{History, all_user_author_filter}, -}; +use std::ops::Range; + +use atuin_client::database::{Database, DbSearchMode, OptFilters, QueryToken, QueryTokenizer}; +use atuin_client::history::{History, all_user_author_filter}; use eyre::Result; use norm::Metric; use norm::fzf::{FzfParser, FzfV2}; -use std::ops::Range; use tracing::{Level, instrument}; +use super::{SearchEngine, SearchState}; + pub struct Search(pub DbSearchMode); impl SearchEngine for Search { @@ -22,18 +20,12 @@ impl SearchEngine for Search { ) -> Result> { let shells = state.shells.to_filter(); let results = db - .search( - self.0, - state.filter_mode, - &state.context, - state.input.as_str(), - OptFilters { - limit: Some(200), - authors: all_user_author_filter(), - shells: shells.as_filter(), - ..Default::default() - }, - ) + .search(self.0, state.filter_mode, &state.context, state.input.as_str(), OptFilters { + limit: Some(200), + authors: all_user_author_filter(), + shells: shells.as_filter(), + ..Default::default() + }) .await // ignore errors as it may be caused by incomplete regex .map_or(Vec::new(), |r| r.into_iter().collect()); diff --git a/crates/atuin/src/command/client/search/history_list.rs b/crates/atuin/src/command/client/search/history_list.rs index 0bea89dba..ca8c8639d 100644 --- a/crates/atuin/src/command/client/search/history_list.rs +++ b/crates/atuin/src/command/client/search/history_list.rs @@ -1,28 +1,23 @@ use std::time::Duration; +use atuin_client::history::History; +use atuin_client::settings::{UiColumn, UiColumnType}; +use atuin_client::theme::{Meaning, Theme}; +use atuin_common::string::align::Alignment; +use atuin_common::string::ellipsis::{Indicator, Pos}; +use atuin_common::string::{EllipsizeExt as _, EscapeNonPrintablePosixExt as _, Measure}; +use atuin_common::time::{DurationExt, OffsetDateTimeExt}; +use itertools::Itertools; +use ratatui::backend::FromCrossterm; +use ratatui::buffer::Buffer; +use ratatui::crossterm::style; +use ratatui::layout::Rect; +use ratatui::style::{Modifier, Style}; +use ratatui::widgets::{Block, StatefulWidget, Widget}; +use time::{OffsetDateTime, UtcOffset}; + use super::engines::{AnySearchEngine, SearchEngine}; use super::syntax; -use atuin_client::{ - history::History, - settings::{UiColumn, UiColumnType}, - theme::{Meaning, Theme}, -}; -use atuin_common::string::EllipsizeExt as _; -use atuin_common::string::EscapeNonPrintablePosixExt as _; -use atuin_common::string::Measure; -use atuin_common::string::align::Alignment; -use atuin_common::string::ellipsis::{Indicator, Pos}; -use atuin_common::time::{DurationExt, OffsetDateTimeExt}; -use itertools::Itertools; -use ratatui::{ - backend::FromCrossterm, - buffer::Buffer, - crossterm::style, - layout::Rect, - style::{Modifier, Style}, - widgets::{Block, StatefulWidget, Widget}, -}; -use time::{OffsetDateTime, UtcOffset}; pub struct HistoryHighlighter<'a> { pub engine: &'a AnySearchEngine, @@ -31,8 +26,7 @@ pub struct HistoryHighlighter<'a> { impl HistoryHighlighter<'_> { pub fn get_highlight_indices(&self, command: &str) -> Vec { - self.engine - .get_highlight_indices(command, self.search_input) + self.engine.get_highlight_indices(command, self.search_input) } } @@ -211,16 +205,8 @@ impl DrawState<'_> { // Calculate the width for the expanding column // Fixed columns use their configured width + 1 (trailing space) let indicator_width: u16 = 3; - let fixed_width: u16 = self - .columns - .iter() - .filter(|c| !c.expand) - .map(|c| c.width + 1) - .sum(); - let expand_width = self - .list_area - .width - .saturating_sub(indicator_width + fixed_width); + let fixed_width: u16 = self.columns.iter().filter(|c| !c.expand).map(|c| c.width + 1).sum(); + let expand_width = self.list_area.width.saturating_sub(indicator_width + fixed_width); let style = self.theme.as_style(Meaning::Base); // Render each configured column @@ -250,7 +236,11 @@ impl DrawState<'_> { if !self.show_numeric_shortcuts { let i = self.y as usize + self.state.offset; let is_selected = i == self.state.selected(); - let prompt: &str = if is_selected { self.indicator } else { " " }; + let prompt: &str = if is_selected { + self.indicator + } else { + " " + }; self.draw(prompt, Style::default()); return; } @@ -291,10 +281,7 @@ impl DrawState<'_> { fn time(&mut self, h: &History, width: u16) { let style = self.theme.as_style(Meaning::Guidance); - let time = (self.now)() - .saturating_duration_since(h.timestamp) - .display() - .largest_unit(); + let time = (self.now)().saturating_duration_since(h.timestamp).display().largest_unit(); // Format as "Xs ago" right-aligned within column width let w = width as usize; @@ -321,11 +308,8 @@ impl DrawState<'_> { } // Build the normalized command string (whitespace-collapsed, control chars escaped) - let normalized: String = h - .command - .escape_non_printable() - .split_ascii_whitespace() - .join(" "); + let normalized: String = + h.command.escape_non_printable().split_ascii_whitespace().join(" "); let highlight_indices = self.history_highlighter.get_highlight_indices(&normalized); @@ -371,12 +355,7 @@ impl DrawState<'_> { /// Render the absolute datetime column (e.g., "2025-01-22 14:35") fn datetime(&mut self, h: &History, width: u16) { let style = self.theme.as_style(Meaning::Annotation); - let formatted = h - .timestamp - .to_offset(self.tz) - .display() - .ymd_hm() - .to_string(); + let formatted = h.timestamp.to_offset(self.tz).display().ymd_hm().to_string(); let w = width as usize; let display = formatted.pad_ellipsize( Measure::Columns(w), @@ -409,12 +388,8 @@ impl DrawState<'_> { let w = width as usize; // Database stores hostname as "hostname:username" let host = h.hostname.split(':').next().unwrap_or(&h.hostname); - let display = host.pad_ellipsize( - Measure::Columns(w), - Pos::End, - Indicator::UNICODE, - Alignment::Start, - ); + let display = + host.pad_ellipsize(Measure::Columns(w), Pos::End, Indicator::UNICODE, Alignment::Start); self.draw(&display, Style::from_crossterm(style)); } @@ -424,12 +399,8 @@ impl DrawState<'_> { let w = width as usize; // Database stores hostname as "hostname:username" let user = h.hostname.split(':').nth(1).unwrap_or(""); - let display = user.pad_ellipsize( - Measure::Columns(w), - Pos::End, - Indicator::UNICODE, - Alignment::Start, - ); + let display = + user.pad_ellipsize(Measure::Columns(w), Pos::End, Indicator::UNICODE, Alignment::Start); self.draw(&display, Style::from_crossterm(style)); } diff --git a/crates/atuin/src/command/client/search/inspector.rs b/crates/atuin/src/command/client/search/inspector.rs index 18de9d611..b165087a0 100644 --- a/crates/atuin/src/command/client/search/inspector.rs +++ b/crates/atuin/src/command/client/search/inspector.rs @@ -1,29 +1,28 @@ use std::time::Duration; -use time::macros::format_description; -use atuin_client::{ - history::{History, HistoryStats}, - settings::Settings, -}; +use atuin_client::history::{History, HistoryStats}; +use atuin_client::settings::Settings; use atuin_common::string::EscapeNonPrintablePosixExt as _; -use ratatui::{ - Frame, - backend::FromCrossterm, - layout::Rect, - prelude::{Constraint, Direction, Layout}, - style::Style, - text::{Span, Text}, - widgets::{Bar, BarChart, BarGroup, Block, Borders, Padding, Paragraph, Row, Table}, -}; - use atuin_common::time::{DurationExt, UtcOffsetSpec}; +use ratatui::Frame; +use ratatui::backend::FromCrossterm; +use ratatui::layout::Rect; +use ratatui::prelude::{Constraint, Direction, Layout}; +use ratatui::style::Style; +use ratatui::text::{Span, Text}; +use ratatui::widgets::{Bar, BarChart, BarGroup, Block, Borders, Padding, Paragraph, Row, Table}; +use time::macros::format_description; use super::super::theme::{Meaning, Theme}; use super::interactive::{Compactness, to_compactness}; #[allow(clippy::cast_sign_loss)] fn u64_or_zero(num: i64) -> u64 { - if num < 0 { 0 } else { num as u64 } + if num < 0 { + 0 + } else { + num as u64 + } } pub fn draw_commands( @@ -41,17 +40,9 @@ pub fn draw_commands( Direction::Horizontal }) .constraints(if compact { - [ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Min(0), - ] + [Constraint::Length(1), Constraint::Length(1), Constraint::Min(0)] } else { - [ - Constraint::Ratio(1, 4), - Constraint::Ratio(1, 2), - Constraint::Ratio(1, 4), - ] + [Constraint::Ratio(1, 4), Constraint::Ratio(1, 2), Constraint::Ratio(1, 4)] }) .split(parent); @@ -125,24 +116,15 @@ pub fn draw_stats_table( let rows = [ Row::new(vec!["Host".to_string(), host.to_string()]), Row::new(vec!["User".to_string(), user.to_string()]), - Row::new(vec![ - "Time".to_string(), - history.timestamp.to_offset(tz.0).to_string(), - ]), - Row::new(vec![ - "Duration".to_string(), - duration.display().largest_unit().to_string(), - ]), + Row::new(vec!["Time".to_string(), history.timestamp.to_offset(tz.0).to_string()]), + Row::new(vec!["Duration".to_string(), duration.display().largest_unit().to_string()]), Row::new(vec![ "Avg duration".to_string(), avg_duration.display().largest_unit().to_string(), ]), Row::new(vec!["Exit".to_string(), history.exit.to_string()]), Row::new(vec!["Directory".to_string(), history.cwd.clone()]), - Row::new(vec![ - "Intent".to_string(), - history.intent.clone().unwrap_or_default(), - ]), + Row::new(vec!["Intent".to_string(), history.intent.clone().unwrap_or_default()]), Row::new(vec!["Session".to_string(), history.session.clone()]), Row::new(vec!["Total runs".to_string(), stats.total.to_string()]), ]; @@ -192,10 +174,7 @@ fn sort_duration_over_time(durations: &[(String, i64)]) -> Vec<(String, i64)> { durations .iter() .map(|(date, duration)| { - ( - date.format(output).expect("failed to format sqlite date"), - *duration, - ) + (date.format(output).expect("failed to format sqlite date"), *duration) }) .collect() } @@ -204,11 +183,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them let exits: Vec = stats .exits .iter() - .map(|(exit, count)| { - Bar::default() - .label(exit.to_string()) - .value(u64_or_zero(*count)) - }) + .map(|(exit, count)| Bar::default().label(exit.to_string()).value(u64_or_zero(*count))) .collect(); let exits = BarChart::default() @@ -229,9 +204,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them .day_of_week .iter() .map(|(day, count)| { - Bar::default() - .label(num_to_day(day.as_str())) - .value(u64_or_zero(*count)) + Bar::default().label(num_to_day(day.as_str())).value(u64_or_zero(*count)) }) .collect(); @@ -277,11 +250,7 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them let layout = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ]) + .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)]) .split(parent); f.render_widget(exits, layout[0]); @@ -341,15 +310,15 @@ pub fn draw_full( #[cfg(test)] mod tests { - use super::draw_ultracompact; - use atuin_client::{ - history::{History, HistoryId, HistoryStats}, - theme::ThemeManager, - }; - use ratatui::{backend::TestBackend, prelude::*}; + use atuin_client::history::{History, HistoryId, HistoryStats}; + use atuin_client::theme::ThemeManager; + use ratatui::backend::TestBackend; + use ratatui::prelude::*; use rstest::*; use time::OffsetDateTime; + use super::draw_ultracompact; + #[fixture] fn mock_history_stats() -> (History, HistoryStats) { let history = History { @@ -470,13 +439,7 @@ mod tests { .collect(); // NUL is shown as caret notation ^@, never as a raw NUL byte - assert!( - rendered.contains("^@"), - "expected caret-escaped NUL (^@) in rendered output" - ); - assert!( - !rendered.contains('\u{0000}'), - "raw NUL byte leaked to the terminal" - ); + assert!(rendered.contains("^@"), "expected caret-escaped NUL (^@) in rendered output"); + assert!(!rendered.contains('\u{0000}'), "raw NUL byte leaked to the terminal"); } } diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index 948e60800..8d1142bd9 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -1,60 +1,48 @@ -use std::{ - io::{IsTerminal, Write, stdout}, - time::Duration, -}; - #[cfg(unix)] use std::io::Read as _; +use std::io::{IsTerminal, Write, stdout}; +use std::time::Duration; -use atuin_common::{shell::Shell, string::EscapeNonPrintablePosixExt as _}; +use atuin_client::database::{Context, Database, current_context}; +use atuin_client::history::store::HistoryStore; +use atuin_client::history::{History, HistoryId, HistoryStats}; +use atuin_client::settings::{ + CursorStyle, ExitMode, FilterMode, KeymapMode, PreviewStrategy, RequestedSearchMode, + SearchMode, Settings, UiColumn, +}; +use atuin_common::shell::Shell; +use atuin_common::string::EscapeNonPrintablePosixExt as _; use eyre::Result; use futures_util::FutureExt; -use semver::Version; -use time::{OffsetDateTime, UtcOffset}; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; - -use super::{ - cursor::Cursor, - engines::{AnySearchEngine, SearchEngine, SearchState}, - history_list::{HistoryList, ListState}, -}; -use atuin_client::{ - database::{Context, Database, current_context}, - history::{History, HistoryId, HistoryStats, store::HistoryStore}, - settings::{ - CursorStyle, ExitMode, FilterMode, KeymapMode, PreviewStrategy, RequestedSearchMode, - SearchMode, Settings, UiColumn, - }, -}; - -use crate::command::client::search::history_list::HistoryHighlighter; -use crate::command::client::search::keybindings::KeymapSet; -use crate::command::client::theme::{Meaning, Theme}; -use crate::{VERSION, command::client::search::engines}; - -use ratatui::{ - Frame, Terminal, TerminalOptions, Viewport, - backend::{CrosstermBackend, FromCrossterm}, - crossterm::{ - cursor::SetCursorStyle, - event::{self, Event, KeyEvent, MouseEvent}, - execute, queue, terminal, - }, - layout::{Alignment, Constraint, Direction, Layout}, - prelude::*, - style::{Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, Tabs}, -}; - +use ratatui::backend::{CrosstermBackend, FromCrossterm}; +use ratatui::crossterm::cursor::SetCursorStyle; +use ratatui::crossterm::event::{self, Event, KeyEvent, MouseEvent}; #[cfg(not(target_os = "windows"))] use ratatui::crossterm::event::{ KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; - +use ratatui::crossterm::{execute, queue, terminal}; +use ratatui::layout::{Alignment, Constraint, Direction, Layout}; +use ratatui::prelude::*; +use ratatui::style::{Modifier, Style}; +use ratatui::text::{Line, Span, Text}; +use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, Tabs}; +use ratatui::{Frame, Terminal, TerminalOptions, Viewport}; +use semver::Version; +use time::{OffsetDateTime, UtcOffset}; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; #[cfg(windows)] use windows_sys::Win32::System::Console::{GetConsoleOutputCP, SetConsoleOutputCP}; +use super::cursor::Cursor; +use super::engines::{AnySearchEngine, SearchEngine, SearchState}; +use super::history_list::{HistoryList, ListState}; +use crate::VERSION; +use crate::command::client::search::engines; +use crate::command::client::search::history_list::HistoryHighlighter; +use crate::command::client::search::keybindings::KeymapSet; +use crate::command::client::theme::{Meaning, Theme}; + const TAB_TITLES: [&str; 2] = ["Search", "Inspect"]; pub enum InputAction { @@ -229,10 +217,7 @@ impl State { self.results_len = results.len(); if settings.smart_sort { - Ok(atuin_history::sort::sort( - self.search.input.as_str(), - results, - )) + Ok(atuin_history::sort::sort(self.search.input.as_str(), results)) } else { Ok(results) } @@ -347,9 +332,8 @@ impl State { } fn handle_key_input(&mut self, settings: &Settings, input: &KeyEvent) -> InputAction { - use super::keybindings::Action; - use super::keybindings::EvalContext; use super::keybindings::key::{KeyCodeValue, KeyInput, SingleKey}; + use super::keybindings::{Action, EvalContext}; // Skip release events if input.kind == event::KeyEventKind::Release { @@ -421,10 +405,7 @@ impl State { }; (Some(Action::Noop), Some(c)) } else { - ( - keymap.resolve(&KeyInput::Single(single.clone()), &ctx), - None, - ) + (keymap.resolve(&KeyInput::Single(single.clone()), &ctx), None) } }; @@ -461,8 +442,7 @@ impl State { fn scroll_up(&mut self, scroll_len: usize) { let i = self.results_state.selected() + scroll_len; - self.results_state - .select(i.min(self.results_len.saturating_sub(1))); + self.results_state.select(i.min(self.results_len.saturating_sub(1))); self.inspecting_state.reset(); } @@ -494,15 +474,11 @@ impl State { InputAction::Continue } Action::CursorWordLeft => { - self.search - .input - .prev_word(&settings.word_chars, settings.word_jump_mode); + self.search.input.prev_word(&settings.word_chars, settings.word_jump_mode); InputAction::Continue } Action::CursorWordRight => { - self.search - .input - .next_word(&settings.word_chars, settings.word_jump_mode); + self.search.input.next_word(&settings.word_chars, settings.word_jump_mode); InputAction::Continue } Action::CursorWordEnd => { @@ -528,15 +504,11 @@ impl State { InputAction::Continue } Action::DeleteWordBefore => { - self.search - .input - .remove_prev_word(&settings.word_chars, settings.word_jump_mode); + self.search.input.remove_prev_word(&settings.word_chars, settings.word_jump_mode); InputAction::Continue } Action::DeleteWordAfter => { - self.search - .input - .remove_next_word(&settings.word_chars, settings.word_jump_mode); + self.search.input.remove_next_word(&settings.word_chars, settings.word_jump_mode); InputAction::Continue } Action::DeleteToWordBoundary => { @@ -583,11 +555,9 @@ impl State { } // -- Page/half-page scroll (invert-aware) -- Action::ScrollHalfPageUp => { - let scroll_len = self - .results_state - .max_entries() - .saturating_sub(settings.scroll_context_lines) - / 2; + let scroll_len = + self.results_state.max_entries().saturating_sub(settings.scroll_context_lines) + / 2; if settings.invert { self.scroll_down(scroll_len); } else { @@ -596,11 +566,9 @@ impl State { InputAction::Continue } Action::ScrollHalfPageDown => { - let scroll_len = self - .results_state - .max_entries() - .saturating_sub(settings.scroll_context_lines) - / 2; + let scroll_len = + self.results_state.max_entries().saturating_sub(settings.scroll_context_lines) + / 2; if settings.invert { self.scroll_up(scroll_len); } else { @@ -609,10 +577,8 @@ impl State { InputAction::Continue } Action::ScrollPageUp => { - let scroll_len = self - .results_state - .max_entries() - .saturating_sub(settings.scroll_context_lines); + let scroll_len = + self.results_state.max_entries().saturating_sub(settings.scroll_context_lines); if settings.invert { self.scroll_down(scroll_len); } else { @@ -621,10 +587,8 @@ impl State { InputAction::Continue } Action::ScrollPageDown => { - let scroll_len = self - .results_state - .max_entries() - .saturating_sub(settings.scroll_context_lines); + let scroll_len = + self.results_state.max_entries().saturating_sub(settings.scroll_context_lines); if settings.invert { self.scroll_up(scroll_len); } else { @@ -661,8 +625,7 @@ impl State { let top = self.results_state.offset(); let visible = self.results_state.max_entries().min(self.results_len); let bottom = top + visible.saturating_sub(1); - self.results_state - .select(bottom.min(self.results_len.saturating_sub(1))); + self.results_state.select(bottom.min(self.results_len.saturating_sub(1))); self.inspecting_state.reset(); InputAction::Continue } @@ -671,8 +634,7 @@ impl State { let top = self.results_state.offset(); let visible = self.results_state.max_entries().min(self.results_len); let middle = top + visible / 2; - self.results_state - .select(middle.min(self.results_len.saturating_sub(1))); + self.results_state.select(middle.min(self.results_len.saturating_sub(1))); self.inspecting_state.reset(); InputAction::Continue } @@ -810,11 +772,8 @@ impl State { { let length_current_cmd = results[selected].command.width() as u16; // calculate the number of newlines in the command - let num_newlines = results[selected] - .command - .chars() - .filter(|&c| c == '\n') - .count() as u16; + let num_newlines = + results[selected].command.chars().filter(|&c| c == '\n').count() as u16; if num_newlines > 0 { std::cmp::min( settings.max_preview_height, @@ -842,9 +801,8 @@ impl State { && settings.preview.strategy == PreviewStrategy::Static && tab_index == 0 { - let longest_command = results - .iter() - .max_by(|h1, h2| h1.command.len().cmp(&h2.command.len())); + 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( settings.max_preview_height, @@ -930,30 +888,50 @@ impl State { .constraints::<&[Constraint]>( if invert { [ - Constraint::Length(1 + border_size), // input - Constraint::Min(1), // results list - Constraint::Length(preview_height), // preview - Constraint::Length(if show_tabs { 1 } else { 0 }), // tabs - Constraint::Length(if show_help { 1 } else { 0 }), // header (sic) - Constraint::Length(warning_height), // skim warning + Constraint::Length(1 + border_size), // input + Constraint::Min(1), // results list + Constraint::Length(preview_height), // preview + Constraint::Length(if show_tabs { + 1 + } else { + 0 + }), // tabs + Constraint::Length(if show_help { + 1 + } else { + 0 + }), // header (sic) + Constraint::Length(warning_height), // skim warning ] } else { match compactness { Compactness::Ultracompact => [ - Constraint::Length(if show_help { 1 } else { 0 }), // header - Constraint::Length(0), // tabs - Constraint::Min(1), // results list - Constraint::Length(0), // no input - Constraint::Length(0), // no preview - Constraint::Length(warning_height), // skim warning + Constraint::Length(if show_help { + 1 + } else { + 0 + }), // header + Constraint::Length(0), // tabs + Constraint::Min(1), // results list + Constraint::Length(0), // no input + Constraint::Length(0), // no preview + Constraint::Length(warning_height), // skim warning ], _ => [ - Constraint::Length(if show_help { 1 } else { 0 }), // header - Constraint::Length(if show_tabs { 1 } else { 0 }), // tabs - Constraint::Min(1), // results list - Constraint::Length(1 + border_size), // input - Constraint::Length(preview_height), // preview - Constraint::Length(warning_height), // skim warning + Constraint::Length(if show_help { + 1 + } else { + 0 + }), // header + Constraint::Length(if show_tabs { + 1 + } else { + 0 + }), // tabs + Constraint::Min(1), // results list + Constraint::Length(1 + border_size), // input + Constraint::Length(preview_height), // preview + Constraint::Length(warning_height), // skim warning ], } } @@ -961,11 +939,31 @@ impl State { ) .split(area); - let input_chunk = if invert { chunks[0] } else { chunks[3] }; - let results_list_chunk = if invert { chunks[1] } else { chunks[2] }; - let preview_chunk = if invert { chunks[2] } else { chunks[4] }; - let tabs_chunk = if invert { chunks[3] } else { chunks[1] }; - let header_chunk = if invert { chunks[4] } else { chunks[0] }; + let input_chunk = if invert { + chunks[0] + } else { + chunks[3] + }; + let results_list_chunk = if invert { + chunks[1] + } else { + chunks[2] + }; + let preview_chunk = if invert { + chunks[2] + } else { + chunks[4] + }; + let tabs_chunk = if invert { + chunks[3] + } else { + chunks[1] + }; + let header_chunk = if invert { + chunks[4] + } else { + chunks[0] + }; // Always last, so it is the bottom row whichever way the layout is stacked. let warning_chunk = chunks[5]; @@ -993,12 +991,8 @@ impl State { let header_chunks = Layout::default() .direction(Direction::Horizontal) .constraints::<&[Constraint]>( - [ - Constraint::Ratio(1, 5), - Constraint::Ratio(3, 5), - Constraint::Ratio(1, 5), - ] - .as_ref(), + [Constraint::Ratio(1, 5), Constraint::Ratio(3, 5), Constraint::Ratio(1, 5)] + .as_ref(), ) .split(header_chunk); @@ -1020,23 +1014,12 @@ impl State { if self.switched_search_mode { format!( "S{}>", - self.search_mode_state - .raw_mode() - .as_str() - .chars() - .next() - .unwrap() + self.search_mode_state.raw_mode().as_str().chars().next().unwrap() ) } else if self.search.custom_context.is_some() { - format!( - "C{}>", - self.search.filter_mode.as_str().chars().next().unwrap() - ) + format!("C{}>", self.search.filter_mode.as_str().chars().next().unwrap()) } else { - format!( - "{}> ", - self.search.filter_mode.as_str().chars().next().unwrap() - ) + format!("{}> ", self.search.filter_mode.as_str().chars().next().unwrap()) } } _ => " > ".to_string(), @@ -1250,11 +1233,8 @@ impl State { .add_modifier(Modifier::BOLD); Text::from(vec![ - Span::styled( - "Warning: \"skim\" mode was removed; falling back to \"fuzzy\"", - style, - ) - .into(), + Span::styled("Warning: \"skim\" mode was removed; falling back to \"fuzzy\"", style) + .into(), vec![ Span::styled("Set ", style), Span::styled("search_mode = \"daemon-fuzzy\"", code_style), @@ -1267,8 +1247,7 @@ impl State { fn build_stats(&self, theme: &Theme) -> Paragraph<'_> { Paragraph::new(Text::from(Span::raw( - self.history_count - .map_or_else(String::new, |count| format!("history count: {count}")), + self.history_count.map_or_else(String::new, |count| format!("history count: {count}")), ))) .style(Style::from_crossterm(theme.as_style(Meaning::Annotation))) .alignment(Alignment::Right) @@ -1433,10 +1412,7 @@ impl TerminalWriter { #[cfg(unix)] { Ok(TerminalWriter::Tty( - std::fs::File::options() - .read(true) - .write(true) - .open("/dev/tty")?, + std::fs::File::options().read(true).write(true).open("/dev/tty")?, )) } @@ -1445,10 +1421,7 @@ impl TerminalWriter { // TUI to render properly. We'll set it back to its previous value upon exit. #[cfg(windows)] { - let file = std::fs::File::options() - .read(true) - .write(true) - .open("CONOUT$")?; + let file = std::fs::File::options().read(true).write(true).open("CONOUT$")?; let initial_console_output_cp = unsafe { GetConsoleOutputCP() }; if initial_console_output_cp != Self::CP_UTF8 { @@ -1457,10 +1430,7 @@ impl TerminalWriter { } } - Ok(TerminalWriter::ConOut( - std::io::LineWriter::new(file), - initial_console_output_cp, - )) + Ok(TerminalWriter::ConOut(std::io::LineWriter::new(file), initial_console_output_cp)) } #[cfg(not(any(unix, windows)))] @@ -1608,13 +1578,8 @@ fn restore_popup_area(saved: &SavedScreen, popup_rect: Rect, scroll_offset: u16) } } - let _ = execute!( - stdout, - MoveTo( - saved.cursor_col, - saved.cursor_row.saturating_sub(scroll_offset) - ) - ); + let _ = + execute!(stdout, MoveTo(saved.cursor_col, saved.cursor_row.saturating_sub(scroll_offset))); let _ = stdout.flush(); } @@ -1734,11 +1699,7 @@ fn compute_popup_placement( // 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, - clippy::too_many_lines, - clippy::cognitive_complexity -)] +#[allow(clippy::cast_possible_truncation, clippy::too_many_lines, clippy::cognitive_complexity)] pub async fn history( query: &[String], settings: &Settings, @@ -1747,9 +1708,7 @@ pub async fn history( theme: &Theme, ) -> Result { let inline_height = if settings.shell_up_key_binding { - settings - .inline_height_shell_up_key_binding - .unwrap_or(settings.inline_height) + settings.inline_height_shell_up_key_binding.unwrap_or(settings.inline_height) } else { settings.inline_height }; @@ -1830,29 +1789,21 @@ pub async fn history( ); for row in popup_rect.y..popup_rect.y.saturating_add(popup_rect.height) { let _ = queue!(raw_stdout, MoveTo(popup_rect.x, row)); - let _ = write!( - raw_stdout, - "{:width$}", - "", - width = popup_rect.width as usize - ); + let _ = write!(raw_stdout, "{:width$}", "", width = popup_rect.width as usize); } let _ = raw_stdout.flush(); } let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::with_options( - backend, - TerminalOptions { - viewport: if popup_mode { - Viewport::Fixed(popup_rect) - } else if inline_height > 0 { - Viewport::Inline(inline_height) - } else { - Viewport::Fullscreen - }, + let mut terminal = Terminal::with_options(backend, TerminalOptions { + viewport: if popup_mode { + Viewport::Fixed(popup_rect) + } else if inline_height > 0 { + Viewport::Inline(inline_height) + } else { + Viewport::Fullscreen }, - )?; + })?; let original_query = query.join(" "); @@ -1954,15 +1905,7 @@ pub async fn history( let accept; let result = 'render: loop { terminal.draw(|f| { - app.draw( - f, - &results, - stats.clone(), - inspecting.as_ref(), - settings, - theme, - popup_mode, - ); + app.draw(f, &results, stats.clone(), inspecting.as_ref(), settings, theme, popup_mode); })?; let initial_input = app.search.input.as_str().to_owned(); @@ -2232,22 +2175,20 @@ fn set_clipboard(_s: String) -> Result<(), std::convert::Infallible> { #[cfg(test)] mod tests { - use rstest::{fixture, rstest}; - use atuin_client::database::Context; use atuin_client::history::History; use atuin_client::settings::{ FilterMode, KeymapMode, Preview, PreviewStrategy, RequestedSearchMode, SearchMode, Settings, Shells, }; + use rstest::{fixture, rstest}; use time::OffsetDateTime; + use super::{Compactness, InputAction, InspectingState, KeymapSet, SearchModeState, State}; use crate::command::client::search::engines::{self, SearchState}; use crate::command::client::search::history_list::ListState; use crate::command::client::search::keybindings::Action; - use super::{Compactness, InputAction, InspectingState, KeymapSet, SearchModeState, State}; - #[fixture] fn settings() -> Settings { Settings::utc() @@ -2320,14 +2261,21 @@ mod tests { let cmd_124: History = History::capture() .timestamp(time::OffsetDateTime::now_utc()) - .command("echo 'Aurea prima sata est aetas, quae vindice nullo, sponte sua, sine lege fidem rectumque colebat. Poena metusque aberant'") + .command( + "echo 'Aurea prima sata est aetas, quae vindice nullo, sponte sua, sine lege \ + fidem rectumque colebat. Poena metusque aberant'", + ) .cwd("/") .build() .into(); let cmd_200: History = History::capture() .timestamp(time::OffsetDateTime::now_utc()) - .command("CREATE USER atuin WITH ENCRYPTED PASSWORD 'supersecretpassword'; CREATE DATABASE atuin WITH OWNER = atuin; \\c atuin; REVOKE ALL PRIVILEGES ON SCHEMA public FROM PUBLIC; echo 'All done. 200 characters'") + .command( + "CREATE USER atuin WITH ENCRYPTED PASSWORD 'supersecretpassword'; CREATE DATABASE \ + atuin WITH OWNER = atuin; \\c atuin; REVOKE ALL PRIVILEGES ON SCHEMA public FROM \ + PUBLIC; echo 'All done. 200 characters'", + ) .cwd("/") .build() .into(); @@ -2408,10 +2356,7 @@ mod tests { let tab_event = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE); let result = state.handle_key_input(&settings, &tab_event); - assert!( - matches!(result, super::InputAction::Accept(_)), - "Tab should always accept" - ); + assert!(matches!(result, super::InputAction::Accept(_)), "Tab should always accept"); // Test left arrow with accept_past_line_start disabled (should continue) let left_event = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE); @@ -2555,10 +2500,8 @@ mod tests { state.results_state.select(50); state.pending_vim_key = Some('g'); - let r = state.handle_key_input( - &settings, - &KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL), - ); + let r = state + .handle_key_input(&settings, &KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)); assert!(matches!(r, InputAction::Continue)); assert_eq!(state.pending_vim_key, None); } @@ -2729,9 +2672,10 @@ mod tests { #[with(KeymapMode::Emacs, 100, 0)] mut state: State, mut settings: Settings, ) { - use crate::command::client::search::keybindings::Action; use atuin_client::settings::ExitMode; + use crate::command::client::search::keybindings::Action; + settings.exit_mode = ExitMode::ReturnOriginal; let result = state.execute_action(&Action::Exit, &settings); assert!(matches!(result, super::InputAction::ReturnOriginal)); @@ -2940,9 +2884,10 @@ mod tests { #[with(KeymapMode::Emacs, 100, 0, FilterMode::Global, "test query")] mut state: State, mut settings: Settings, ) { + use std::collections::HashMap; + use atuin_client::settings::KeyBindingConfig; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - use std::collections::HashMap; // Configure tab to return-query settings.keymap.emacs = HashMap::from([( diff --git a/crates/atuin/src/command/client/search/keybindings/actions.rs b/crates/atuin/src/command/client/search/keybindings/actions.rs index 5a16556fb..9cc154652 100644 --- a/crates/atuin/src/command/client/search/keybindings/actions.rs +++ b/crates/atuin/src/command/client/search/keybindings/actions.rs @@ -242,9 +242,10 @@ impl<'de> Deserialize<'de> for Action { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + #[rstest] #[case::cursor_left("cursor-left", Action::CursorLeft)] #[case::accept("accept", Action::Accept)] diff --git a/crates/atuin/src/command/client/search/keybindings/conditions.rs b/crates/atuin/src/command/client/search/keybindings/conditions.rs index dd7ef37e5..9e5bc5e92 100644 --- a/crates/atuin/src/command/client/search/keybindings/conditions.rs +++ b/crates/atuin/src/command/client/search/keybindings/conditions.rs @@ -369,9 +369,10 @@ impl<'de> Deserialize<'de> for ConditionExpr { #[cfg(test)] mod tests { - use super::*; use rstest::rstest; + use super::*; + fn ctx( cursor: usize, width: usize, @@ -646,10 +647,7 @@ mod tests { let expr = ConditionExpr::Atom(ConditionAtom::CursorAtStart) .or(ConditionExpr::Atom(ConditionAtom::InputEmpty)) .and(ConditionExpr::Atom(ConditionAtom::NoResults)); - assert_eq!( - expr.to_string(), - "(cursor-at-start || input-empty) && no-results" - ); + assert_eq!(expr.to_string(), "(cursor-at-start || input-empty) && no-results"); } #[test] @@ -658,10 +656,7 @@ mod tests { let inner_and = ConditionExpr::Atom(ConditionAtom::InputEmpty) .and(ConditionExpr::Atom(ConditionAtom::NoResults)); let expr = ConditionExpr::Atom(ConditionAtom::CursorAtStart).or(inner_and); - assert_eq!( - expr.to_string(), - "cursor-at-start || input-empty && no-results" - ); + assert_eq!(expr.to_string(), "cursor-at-start || input-empty && no-results"); } // -- Display round-trip -- @@ -697,9 +692,7 @@ mod tests { let parsed: ConditionExpr = serde_json::from_str(json).unwrap(); let expected = ConditionExpr::And( Box::new(ConditionExpr::Atom(ConditionAtom::CursorAtStart)), - Box::new(ConditionExpr::Not(Box::new(ConditionExpr::Atom( - ConditionAtom::InputEmpty, - )))), + Box::new(ConditionExpr::Not(Box::new(ConditionExpr::Atom(ConditionAtom::InputEmpty)))), ); assert_eq!(parsed, expected); } @@ -728,9 +721,6 @@ mod tests { .and(ConditionExpr::from(ConditionAtom::InputEmpty).not()) .or(ConditionExpr::from(ConditionAtom::NoResults)); // And binds tighter than Or, so no parens needed around the And - assert_eq!( - expr.to_string(), - "cursor-at-start && !input-empty || no-results" - ); + assert_eq!(expr.to_string(), "cursor-at-start && !input-empty || no-results"); } } diff --git a/crates/atuin/src/command/client/search/keybindings/defaults.rs b/crates/atuin/src/command/client/search/keybindings/defaults.rs index 439a8d165..a455f4b0f 100644 --- a/crates/atuin/src/command/client/search/keybindings/defaults.rs +++ b/crates/atuin/src/command/client/search/keybindings/defaults.rs @@ -29,13 +29,10 @@ fn bind_scroll_key( ) { let k = key(key_str); if scroll_exits && toward_index_zero { - km.bind_conditional( - k, - vec![ - KeyRule::when(ConditionAtom::ListAtStart, Action::Exit), - KeyRule::always(action), - ], - ); + km.bind_conditional(k, vec![ + KeyRule::when(ConditionAtom::ListAtStart, Action::Exit), + KeyRule::always(action), + ]); } else { km.bind(k, action); } @@ -119,13 +116,10 @@ pub fn default_emacs_keymap(settings: &Settings) -> Keymap { // right: behavior at end of line if settings.keys.accept_past_line_end { - km.bind_conditional( - key("right"), - vec![ - KeyRule::when(ConditionAtom::CursorAtEnd, Action::ReturnSelection), - KeyRule::always(Action::CursorRight), - ], - ); + km.bind_conditional(key("right"), vec![ + KeyRule::when(ConditionAtom::CursorAtEnd, Action::ReturnSelection), + KeyRule::always(Action::CursorRight), + ]); } else { km.bind(key("right"), Action::CursorRight); } @@ -133,21 +127,15 @@ pub fn default_emacs_keymap(settings: &Settings) -> Keymap { // left: behavior at start of line // accept_past_line_start takes precedence over exit_past_line_start if settings.keys.accept_past_line_start { - km.bind_conditional( - key("left"), - vec![ - KeyRule::when(ConditionAtom::CursorAtStart, Action::ReturnSelection), - KeyRule::always(Action::CursorLeft), - ], - ); + km.bind_conditional(key("left"), vec![ + KeyRule::when(ConditionAtom::CursorAtStart, Action::ReturnSelection), + KeyRule::always(Action::CursorLeft), + ]); } else if settings.keys.exit_past_line_start { - km.bind_conditional( - key("left"), - vec![ - KeyRule::when(ConditionAtom::CursorAtStart, Action::Exit), - KeyRule::always(Action::CursorLeft), - ], - ); + km.bind_conditional(key("left"), vec![ + KeyRule::when(ConditionAtom::CursorAtStart, Action::Exit), + KeyRule::always(Action::CursorLeft), + ]); } else { km.bind(key("left"), Action::CursorLeft); } @@ -162,13 +150,10 @@ pub fn default_emacs_keymap(settings: &Settings) -> Keymap { // backspace: behavior at start of line if settings.keys.accept_with_backspace { - km.bind_conditional( - key("backspace"), - vec![ - KeyRule::when(ConditionAtom::CursorAtStart, Action::ReturnSelection), - KeyRule::always(Action::DeleteCharBefore), - ], - ); + km.bind_conditional(key("backspace"), vec![ + KeyRule::when(ConditionAtom::CursorAtStart, Action::ReturnSelection), + KeyRule::always(Action::DeleteCharBefore), + ]); } else { km.bind(key("backspace"), Action::DeleteCharBefore); } @@ -188,10 +173,7 @@ pub fn default_emacs_keymap(settings: &Settings) -> Keymap { "alt" }; for n in 1..=9u8 { - km.bind( - key(&format!("{num_mod}-{n}")), - Action::ReturnSelectionNth(n), - ); + km.bind(key(&format!("{num_mod}-{n}")), Action::ReturnSelectionNth(n)); } // --- Cursor movement --- @@ -219,13 +201,10 @@ pub fn default_emacs_keymap(settings: &Settings) -> Keymap { km.bind(key("alt-d"), Action::DeleteWordAfter); km.bind(key("delete"), Action::DeleteCharAfter); // ctrl-d: if input empty → return original, otherwise delete char - km.bind_conditional( - key("ctrl-d"), - vec![ - KeyRule::when(ConditionAtom::InputEmpty, Action::ReturnOriginal), - KeyRule::always(Action::DeleteCharAfter), - ], - ); + km.bind_conditional(key("ctrl-d"), vec![ + KeyRule::when(ConditionAtom::InputEmpty, Action::ReturnOriginal), + KeyRule::always(Action::DeleteCharAfter), + ]); km.bind(key("ctrl-w"), Action::DeleteToWordBoundary); km.bind(key("ctrl-u"), Action::ClearLine); @@ -390,10 +369,7 @@ pub fn default_inspector_keymap(settings: &Settings) -> Keymap { km.bind(key("pagedown"), Action::InspectNext); // For vim users, add j/k navigation - if matches!( - settings.keymap_mode, - KeymapMode::VimNormal | KeymapMode::VimInsert - ) { + if matches!(settings.keymap_mode, KeymapMode::VimNormal | KeymapMode::VimInsert) { km.bind(key("j"), Action::InspectNext); km.bind(key("k"), Action::InspectPrevious); } @@ -412,13 +388,10 @@ pub fn default_prefix_keymap() -> Keymap { km.bind(key("d"), Action::Delete); km.bind(key("D"), Action::DeleteAll); km.bind(key("a"), Action::CursorStart); - km.bind_conditional( - key("c"), - vec![ - KeyRule::when(ConditionAtom::HasContext, Action::ClearContext), - KeyRule::always(Action::SwitchContext), - ], - ); + km.bind_conditional(key("c"), vec![ + KeyRule::when(ConditionAtom::HasContext, Action::ClearContext), + KeyRule::always(Action::SwitchContext), + ]); km } @@ -533,9 +506,10 @@ impl KeymapSet { #[cfg(test)] mod tests { + use rstest::rstest; + use super::*; use crate::command::client::search::keybindings::conditions::EvalContext; - use rstest::rstest; fn make_ctx(cursor: usize, width: usize, selected: usize, len: usize) -> EvalContext { EvalContext { @@ -744,10 +718,7 @@ mod tests { let ctx = make_ctx(0, 0, 0, 10); // ctrl-x should be prefix mode - assert_eq!( - km.resolve(&key("ctrl-x"), &ctx), - Some(Action::EnterPrefixMode) - ); + assert_eq!(km.resolve(&key("ctrl-x"), &ctx), Some(Action::EnterPrefixMode)); // ctrl-a should now be CursorStart (not prefix) assert_eq!(km.resolve(&key("ctrl-a"), &ctx), Some(Action::CursorStart)); } @@ -760,10 +731,7 @@ mod tests { let ctx = make_ctx(0, 0, 0, 10); // ctrl-1 should work - assert_eq!( - km.resolve(&key("ctrl-1"), &ctx), - Some(Action::ReturnSelectionNth(1)) - ); + assert_eq!(km.resolve(&key("ctrl-1"), &ctx), Some(Action::ReturnSelectionNth(1))); // alt-1 should NOT be bound assert_eq!(km.resolve(&key("alt-1"), &ctx), None); } @@ -775,10 +743,7 @@ mod tests { let ctx = make_ctx(0, 0, 0, 10); // alt-1 should work by default - assert_eq!( - km.resolve(&key("alt-1"), &ctx), - Some(Action::ReturnSelectionNth(1)) - ); + assert_eq!(km.resolve(&key("alt-1"), &ctx), Some(Action::ReturnSelectionNth(1))); } // ----------------------------------------------------------------------- @@ -835,24 +800,20 @@ mod tests { #[test] fn config_override_replaces_key() { - use atuin_client::settings::KeyBindingConfig; use std::collections::HashMap; + use atuin_client::settings::KeyBindingConfig; + let mut settings = default_settings(); let set = KeymapSet::defaults(&settings); // Default: ctrl-c → ReturnOriginal let ctx = make_ctx(0, 0, 0, 10); - assert_eq!( - set.emacs.resolve(&key("ctrl-c"), &ctx), - Some(Action::ReturnOriginal) - ); + assert_eq!(set.emacs.resolve(&key("ctrl-c"), &ctx), Some(Action::ReturnOriginal)); // Override ctrl-c → Exit via config - settings.keymap.emacs = HashMap::from([( - "ctrl-c".to_string(), - KeyBindingConfig::Simple("exit".to_string()), - )]); + settings.keymap.emacs = + HashMap::from([("ctrl-c".to_string(), KeyBindingConfig::Simple("exit".to_string()))]); let set = KeymapSet::from_settings(&settings); assert_eq!(set.emacs.resolve(&key("ctrl-c"), &ctx), Some(Action::Exit)); @@ -860,15 +821,14 @@ mod tests { #[test] fn config_override_preserves_unoverridden_keys() { - use atuin_client::settings::KeyBindingConfig; use std::collections::HashMap; + use atuin_client::settings::KeyBindingConfig; + let mut settings = default_settings(); // Override only ctrl-c; enter should keep its default - settings.keymap.emacs = HashMap::from([( - "ctrl-c".to_string(), - KeyBindingConfig::Simple("exit".to_string()), - )]); + settings.keymap.emacs = + HashMap::from([("ctrl-c".to_string(), KeyBindingConfig::Simple("exit".to_string()))]); let set = KeymapSet::from_settings(&settings); let ctx = make_ctx(0, 0, 0, 10); @@ -876,17 +836,15 @@ mod tests { // ctrl-c overridden assert_eq!(set.emacs.resolve(&key("ctrl-c"), &ctx), Some(Action::Exit)); // enter still has default (enter_accept=false → ReturnSelection) - assert_eq!( - set.emacs.resolve(&key("enter"), &ctx), - Some(Action::ReturnSelection) - ); + assert_eq!(set.emacs.resolve(&key("enter"), &ctx), Some(Action::ReturnSelection)); } #[test] fn config_conditional_override() { - use atuin_client::settings::{KeyBindingConfig, KeyRuleConfig}; use std::collections::HashMap; + use atuin_client::settings::{KeyBindingConfig, KeyRuleConfig}; + let mut settings = default_settings(); // Override "up" with a custom conditional settings.keymap.emacs = HashMap::from([( @@ -911,10 +869,7 @@ mod tests { // With results → select-previous let ctx = make_ctx(0, 0, 0, 10); - assert_eq!( - set.emacs.resolve(&key("up"), &ctx), - Some(Action::SelectPrevious) - ); + assert_eq!(set.emacs.resolve(&key("up"), &ctx), Some(Action::SelectPrevious)); } #[test] @@ -925,9 +880,7 @@ mod tests { // Verify a sample of keys produce the same results let ctx = make_ctx(0, 0, 0, 10); - let test_keys = [ - "ctrl-c", "enter", "esc", "tab", "up", "down", "left", "right", - ]; + let test_keys = ["ctrl-c", "enter", "esc", "tab", "up", "down", "left", "right"]; for k in &test_keys { assert_eq!( defaults.emacs.resolve(&key(k), &ctx), @@ -961,10 +914,8 @@ mod tests { // With [keymap] present (even just one override), [keys] is ignored // so the standard defaults (scroll_exits=true) apply - settings.keymap.emacs = HashMap::from([( - "ctrl-c".to_string(), - KeyBindingConfig::Simple("exit".to_string()), - )]); + settings.keymap.emacs = + HashMap::from([("ctrl-c".to_string(), KeyBindingConfig::Simple("exit".to_string()))]); let set_keymap = KeymapSet::from_settings(&settings); // Not at boundary (selected=5): should SelectNext normally @@ -1001,10 +952,8 @@ mod tests { ); // Add a [keymap] entry (for a different key) - settings.keymap.emacs = HashMap::from([( - "ctrl-c".to_string(), - KeyBindingConfig::Simple("exit".to_string()), - )]); + settings.keymap.emacs = + HashMap::from([("ctrl-c".to_string(), KeyBindingConfig::Simple("exit".to_string()))]); let set_keymap = KeymapSet::from_settings(&settings); // Now left should use standard defaults (exit_past_line_start=true) @@ -1042,9 +991,10 @@ mod tests { #[test] fn original_input_empty_condition_in_config() { - use atuin_client::settings::{KeyBindingConfig, KeyRuleConfig}; use std::collections::HashMap; + use atuin_client::settings::{KeyBindingConfig, KeyRuleConfig}; + let mut settings = default_settings(); // Configure esc to: if original-input-empty -> return-query, else return-original settings.keymap.emacs = HashMap::from([( diff --git a/crates/atuin/src/command/client/search/keybindings/key.rs b/crates/atuin/src/command/client/search/keybindings/key.rs index e016fab60..073c9aef0 100644 --- a/crates/atuin/src/command/client/search/keybindings/key.rs +++ b/crates/atuin/src/command/client/search/keybindings/key.rs @@ -311,10 +311,11 @@ impl<'de> Deserialize<'de> for KeyInput { #[cfg(test)] mod tests { - use super::*; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use rstest::rstest; + use super::*; + // (A) parse -> (code, flags). Asserting ALL flags is a strengthening, // verified consistent with the parse logic. #[rstest] @@ -359,10 +360,7 @@ mod tests { // (B) super aliases parse equal. #[rstest] fn super_aliases_parse_equal(#[values("super-a", "cmd-a", "win-a")] input: &str) { - assert_eq!( - SingleKey::parse(input).unwrap(), - SingleKey::parse("super-a").unwrap() - ); + assert_eq!(SingleKey::parse(input).unwrap(), SingleKey::parse("super-a").unwrap()); } // (C) parse errors. @@ -413,15 +411,7 @@ mod tests { false, true )] - #[case::f1( - KeyCode::F(1), - KeyModifiers::NONE, - KeyCodeValue::F(1), - false, - false, - false, - false - )] + #[case::f1(KeyCode::F(1), KeyModifiers::NONE, KeyCodeValue::F(1), false, false, false, false)] #[case::f12_ctrl( KeyCode::F(12), KeyModifiers::CONTROL, @@ -504,11 +494,7 @@ mod tests { s: &str, ) { let k = KeyInput::parse(s).unwrap(); - assert_eq!( - k, - KeyInput::parse(&k.to_string()).unwrap(), - "round-trip failed for {s}" - ); + assert_eq!(k, KeyInput::parse(&k.to_string()).unwrap(), "round-trip failed for {s}"); } // (G) exact display. diff --git a/crates/atuin/src/command/client/search/keybindings/keymap.rs b/crates/atuin/src/command/client/search/keybindings/keymap.rs index 8c7fcfa88..ec13823fb 100644 --- a/crates/atuin/src/command/client/search/keybindings/keymap.rs +++ b/crates/atuin/src/command/client/search/keybindings/keymap.rs @@ -145,13 +145,10 @@ mod tests { fn conditional_first_match_wins() { let mut keymap = Keymap::new(); let key = KeyInput::parse("left").unwrap(); - keymap.bind_conditional( - key.clone(), - vec![ - KeyRule::when(ConditionAtom::CursorAtStart, Action::Exit), - KeyRule::always(Action::CursorLeft), - ], - ); + keymap.bind_conditional(key.clone(), vec![ + KeyRule::when(ConditionAtom::CursorAtStart, Action::Exit), + KeyRule::always(Action::CursorLeft), + ]); // Cursor at start → Exit let ctx = make_ctx(0, 5, 0, 10); @@ -175,10 +172,10 @@ mod tests { let mut keymap = Keymap::new(); let key = KeyInput::parse("left").unwrap(); // Only one rule with a condition that won't match - keymap.bind_conditional( - key.clone(), - vec![KeyRule::when(ConditionAtom::CursorAtStart, Action::Exit)], - ); + keymap.bind_conditional(key.clone(), vec![KeyRule::when( + ConditionAtom::CursorAtStart, + Action::Exit, + )]); // Cursor not at start → no match let ctx = make_ctx(3, 5, 0, 10); diff --git a/crates/atuin/src/command/client/search/syntax.rs b/crates/atuin/src/command/client/search/syntax.rs index 6be569736..59dec6263 100644 --- a/crates/atuin/src/command/client/search/syntax.rs +++ b/crates/atuin/src/command/client/search/syntax.rs @@ -10,12 +10,7 @@ use atuin_client::theme::Meaning; /// /// Rows are re-classified on every redraw while typing or scrolling, so /// results are memoized; repeat frames cost a hash lookup, not a parse. -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "illumos" -))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "illumos"))] pub fn classify(cmd: &str, shell: Option<&str>) -> Vec { use std::cell::RefCell; use std::collections::HashMap; @@ -29,19 +24,11 @@ pub fn classify(cmd: &str, shell: Option<&str>) -> Vec { if cache.len() > 4096 { cache.clear(); } - cache - .entry(key) - .or_insert_with(|| parse(cmd, shell)) - .clone() + cache.entry(key).or_insert_with(|| parse(cmd, shell)).clone() }) } -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "illumos" -))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "illumos"))] fn parse(cmd: &str, shell: Option<&str>) -> Vec { let mut meanings = vec![Meaning::Base; cmd.len()]; @@ -78,12 +65,7 @@ pub fn classify(cmd: &str, _shell: Option<&str>) -> Vec { vec![Meaning::Base; cmd.len()] } -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "illumos" -))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "illumos"))] fn walk(node: tree_sitter::Node, src: &[u8], meanings: &mut [Meaning]) { let meaning = match node.kind() { "comment" => Some(Meaning::SyntaxComment), @@ -124,10 +106,7 @@ fn walk(node: tree_sitter::Node, src: &[u8], meanings: &mut [Meaning]) { // An expansion is uniformly a variable; don't let its `$`/`${`/`}` child // tokens overwrite it as operators. - if matches!( - node.kind(), - "simple_expansion" | "expansion" | "variable_expansion" - ) { + if matches!(node.kind(), "simple_expansion" | "expansion" | "variable_expansion") { return; } @@ -139,12 +118,7 @@ fn walk(node: tree_sitter::Node, src: &[u8], meanings: &mut [Meaning]) { } } -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "illumos" -))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "illumos"))] fn highlight_powershell(node: tree_sitter::Node, src: &[u8], meanings: &mut [Meaning]) { // PowerShell has a different syntax than most shells, so it's handled separately. @@ -181,17 +155,13 @@ fn highlight_powershell(node: tree_sitter::Node, src: &[u8], meanings: &mut [Mea #[cfg(all( test, - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "illumos" - ) + any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "illumos") ))] mod tests { - use super::{Meaning, classify}; use rstest::rstest; + use super::{Meaning, classify}; + /// Render the classification as one char per byte for compact assertions. fn render_shell(cmd: &str, shell: Option<&str>) -> String { classify(cmd, shell) diff --git a/crates/atuin/src/command/client/setup.rs b/crates/atuin/src/command/client/setup.rs index 8de73d625..37d84358d 100644 --- a/crates/atuin/src/command/client/setup.rs +++ b/crates/atuin/src/command/client/setup.rs @@ -1,8 +1,8 @@ -use atuin_client::settings::Settings; +use std::io::{self, Write}; +use atuin_client::settings::Settings; use colored::Colorize; use eyre::Result; -use std::io::{self, Write}; use toml_edit::{DocumentMut, value}; pub async fn run(_settings: &Settings) -> Result<()> { @@ -10,7 +10,8 @@ pub async fn run(_settings: &Settings) -> Result<()> { "Atuin AI", "This will enable command generation and other AI features via the question mark key", Some( - "By default, Atuin AI only has access to the name and version of your operating system and shell - your shell history is not sent to the AI.", + "By default, Atuin AI only has access to the name and version of your operating \ + system and shell - your shell history is not sent to the AI.", ), )?; @@ -46,25 +47,16 @@ pub async fn run(_settings: &Settings) -> Result<()> { if changed { tokio::fs::write(config_file, doc.to_string()).await?; - println!( - "{check} Settings updated successfully", - check = "✓".bold().bright_green() - ); + println!("{check} Settings updated successfully", check = "✓".bold().bright_green()); } else { - println!( - "{check} No settings changed", - check = "✓".bold().bright_green() - ); + println!("{check} No settings changed", check = "✓".bold().bright_green()); } Ok(()) } pub fn prompt(feature: &str, description: &str, note: Option<&str>) -> Result { - println!( - "> Enable {feature}?", - feature = feature.bold().bright_blue() - ); + println!("> Enable {feature}?", feature = feature.bold().bright_blue()); if let Some(note) = note { println!(" {description}"); print!(" {note} {q} ", q = "[Y/n]".bold()); diff --git a/crates/atuin/src/command/client/stats.rs b/crates/atuin/src/command/client/stats.rs index c505f51cb..0b644131d 100644 --- a/crates/atuin/src/command/client/stats.rs +++ b/crates/atuin/src/command/client/stats.rs @@ -1,20 +1,14 @@ +use atuin_client::database::{Database, current_context}; +use atuin_client::settings::{FilterMode, Settings}; +use atuin_client::theme::Theme; +use atuin_history::stats::{compute, pretty_print}; use clap::Parser; use eyre::Result; use interim::parse_date_string; use time::{Duration, OffsetDateTime, Time}; -use atuin_client::{ - database::{Database, current_context}, - settings::{FilterMode, Settings}, - theme::Theme, -}; - -use atuin_history::stats::{compute, pretty_print}; - fn parse_ngram_size(s: &str) -> Result { - let value = s - .parse::() - .map_err(|_| format!("'{s}' is not a valid window size"))?; + let value = s.parse::().map_err(|_| format!("'{s}' is not a valid window size"))?; if value == 0 { return Err("ngram window size must be at least 1".to_string()); @@ -25,7 +19,8 @@ fn parse_ngram_size(s: &str) -> Result { fn period_long_help() -> String { format!( - "Compute statistics for the specified period, leave blank for statistics since the beginning. See [this]({}) for more details.", + "Compute statistics for the specified period, leave blank for statistics since the \ + beginning. See [this]({}) for more details.", atuin_common::docs::url("reference/stats/") ) } @@ -90,9 +85,7 @@ impl Cmd { Some((start, end)) }; - let history = db - .list(filter.as_slice(), &context, None, false, false, range) - .await?; + let history = db.list(filter.as_slice(), &context, None, false, false, range).await?; let stats = compute(settings, &history, self.count, self.ngram_size); diff --git a/crates/atuin/src/command/client/store.rs b/crates/atuin/src/command/client/store.rs index 9681bdabf..66bb82530 100644 --- a/crates/atuin/src/command/client/store.rs +++ b/crates/atuin/src/command/client/store.rs @@ -1,8 +1,9 @@ +use atuin_client::database::Database; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use atuin_common::time::{OffsetDateTimeExt, UtcOffsetExt}; use clap::Subcommand; use eyre::Result; - -use atuin_client::{database::Database, record::sqlite_store::SqliteStore, settings::Settings}; use itertools::Itertools; use time::OffsetDateTime; diff --git a/crates/atuin/src/command/client/store/pull.rs b/crates/atuin/src/command/client/store/pull.rs index 30b36a1f4..524f67ab2 100644 --- a/crates/atuin/src/command/client/store/pull.rs +++ b/crates/atuin/src/command/client/store/pull.rs @@ -1,14 +1,12 @@ -use clap::Args; -use eyre::Result; - -use atuin_client::{ - database::Database, - record::sync::Operation, - record::{sqlite_store::SqliteStore, sync}, - settings::Settings, -}; +use atuin_client::database::Database; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::record::sync; +use atuin_client::record::sync::Operation; +use atuin_client::settings::Settings; use atuin_common::encryption::paseto_v4; use atuin_domain::record::RecordTag; +use clap::Args; +use eyre::Result; #[derive(Args, Debug)] pub struct Pull { diff --git a/crates/atuin/src/command/client/store/purge.rs b/crates/atuin/src/command/client/store/purge.rs index 15f92ebdc..af709fe69 100644 --- a/crates/atuin/src/command/client/store/purge.rs +++ b/crates/atuin/src/command/client/store/purge.rs @@ -1,9 +1,9 @@ +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; use clap::Args; use eyre::Result; -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::encryption::paseto_v4; - #[derive(Args, Debug)] pub struct Purge {} diff --git a/crates/atuin/src/command/client/store/push.rs b/crates/atuin/src/command/client/store/push.rs index 216375637..10e381c18 100644 --- a/crates/atuin/src/command/client/store/push.rs +++ b/crates/atuin/src/command/client/store/push.rs @@ -1,16 +1,14 @@ +use atuin_client::api_client::Client; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::record::sync; +use atuin_client::record::sync::Operation; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; use atuin_domain::record::{HostId, RecordTag}; use clap::Args; use eyre::Result; use uuid::Uuid; -use atuin_client::{ - api_client::Client, - record::sync::Operation, - record::{sqlite_store::SqliteStore, sync}, - settings::Settings, -}; -use atuin_common::encryption::paseto_v4; - #[derive(Args, Debug)] pub struct Push { /// The tag to push (eg, 'history'). Defaults to all tags diff --git a/crates/atuin/src/command/client/store/rebuild.rs b/crates/atuin/src/command/client/store/rebuild.rs index 11a295b75..d8e18216c 100644 --- a/crates/atuin/src/command/client/store/rebuild.rs +++ b/crates/atuin/src/command/client/store/rebuild.rs @@ -1,4 +1,10 @@ -use atuin_dotfiles::store::{AliasStore, var::VarStore}; +use atuin_client::database::Database; +use atuin_client::history::store::HistoryStore; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use atuin_scripts::store::ScriptStore; use clap::Args; use eyre::{Result, bail}; @@ -6,12 +12,6 @@ use eyre::{Result, bail}; #[cfg(feature = "daemon")] use crate::command::client::daemon as daemon_cmd; -use atuin_client::{ - database::Database, history::store::HistoryStore, record::sqlite_store::SqliteStore, - settings::Settings, -}; -use atuin_common::encryption::paseto_v4; - #[derive(Args, Debug)] pub struct Rebuild { pub tag: String, @@ -30,8 +30,7 @@ impl Rebuild { // binary big match self.tag.as_str() { "history" => { - self.rebuild_history(settings, store.clone(), database) - .await?; + self.rebuild_history(settings, store.clone(), database).await?; } "dotfiles" => { diff --git a/crates/atuin/src/command/client/store/rekey.rs b/crates/atuin/src/command/client/store/rekey.rs index e75518965..08b49b441 100644 --- a/crates/atuin/src/command/client/store/rekey.rs +++ b/crates/atuin/src/command/client/store/rekey.rs @@ -1,9 +1,9 @@ +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; use clap::Args; use eyre::Result; -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::encryption::paseto_v4; - #[derive(Args, Debug)] pub struct Rekey { /// The new key to use for encryption. Omit for a randomly-generated key diff --git a/crates/atuin/src/command/client/store/verify.rs b/crates/atuin/src/command/client/store/verify.rs index 5e08cdd2e..63c7fc683 100644 --- a/crates/atuin/src/command/client/store/verify.rs +++ b/crates/atuin/src/command/client/store/verify.rs @@ -1,9 +1,9 @@ +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_common::encryption::paseto_v4; use clap::Args; use eyre::Result; -use atuin_client::{record::sqlite_store::SqliteStore, settings::Settings}; -use atuin_common::encryption::paseto_v4; - #[derive(Args, Debug)] pub struct Verify {} diff --git a/crates/atuin/src/command/client/sync.rs b/crates/atuin/src/command/client/sync.rs index 4d666122e..cab1ebc36 100644 --- a/crates/atuin/src/command/client/sync.rs +++ b/crates/atuin/src/command/client/sync.rs @@ -1,14 +1,12 @@ -use clap::Subcommand; -use eyre::{Result, WrapErr}; - -use atuin_client::{ - database::Database, - history::store::HistoryStore, - record::{sqlite_store::SqliteStore, sync}, - settings::Settings, -}; +use atuin_client::database::Database; +use atuin_client::history::store::HistoryStore; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::record::sync; +use atuin_client::settings::Settings; use atuin_common::encryption::paseto_v4; use atuin_domain::record::RecordTag; +use clap::Subcommand; +use eyre::{Result, WrapErr}; mod status; diff --git a/crates/atuin/src/command/client/sync/status.rs b/crates/atuin/src/command/client/sync/status.rs index 8873296a4..107567ca9 100644 --- a/crates/atuin/src/command/client/sync/status.rs +++ b/crates/atuin/src/command/client/sync/status.rs @@ -1,8 +1,10 @@ -use crate::{SHA, VERSION}; -use atuin_client::{api_client, settings::Settings}; +use atuin_client::api_client; +use atuin_client::settings::Settings; use colored::Colorize; use eyre::{Result, bail}; +use crate::{SHA, VERSION}; + pub async fn run(settings: &Settings) -> Result<()> { if !settings.logged_in().await? { bail!("You are not logged in to a sync server - cannot show sync status"); diff --git a/crates/atuin/src/command/client/update.rs b/crates/atuin/src/command/client/update.rs index 8845db434..fbed86d90 100644 --- a/crates/atuin/src/command/client/update.rs +++ b/crates/atuin/src/command/client/update.rs @@ -40,7 +40,8 @@ impl Cmd { let current_exe = std::env::current_exe()?; let receipt_prefix = updater.install_prefix_root()?; bail!( - "This atuin binary ({}) is not the one the standalone installer installed to {}. Are multiple copies of atuin installed?", + "This atuin binary ({}) is not the one the standalone installer installed to {}. \ + Are multiple copies of atuin installed?", current_exe.display(), receipt_prefix, ); diff --git a/crates/atuin/src/command/client/wrapped.rs b/crates/atuin/src/command/client/wrapped.rs index 190503603..256d612fd 100644 --- a/crates/atuin/src/command/client/wrapped.rs +++ b/crates/atuin/src/command/client/wrapped.rs @@ -1,15 +1,15 @@ -use crossterm::style::{ResetColor, SetAttribute}; -use eyre::Result; use std::collections::{HashMap, HashSet}; -use time::{Date, Duration, Month, OffsetDateTime, Time}; -use atuin_client::{ - database::Database, record::sqlite_store::SqliteStore, settings::Settings, theme::Theme, -}; +use atuin_client::database::Database; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; +use atuin_client::theme::Theme; use atuin_common::encryption::paseto_v4; use atuin_dotfiles::store::AliasStore; - use atuin_history::stats::{Stats, compute}; +use crossterm::style::{ResetColor, SetAttribute}; +use eyre::Result; +use time::{Date, Duration, Month, OffsetDateTime, Time}; #[derive(Debug)] struct WrappedStats { @@ -34,13 +34,7 @@ impl WrappedStats { let expand_alias = |cmd: &str| -> String { alias_map.get(cmd).map_or_else( || cmd.to_string(), - |expanded| { - expanded - .split_whitespace() - .next() - .unwrap_or(cmd) - .to_string() - }, + |expanded| expanded.split_whitespace().next().unwrap_or(cmd).to_string(), ) }; @@ -119,12 +113,7 @@ impl WrappedStats { let mut hours: HashMap = HashMap::new(); for entry in history { - let raw_cmd = entry - .command - .split_whitespace() - .next() - .unwrap_or("") - .to_string(); + let raw_cmd = entry.command.split_whitespace().next().unwrap_or("").to_string(); let cmd = expand_alias(&raw_cmd); let (total, errors) = command_errors.entry(cmd.clone()).or_insert((0, 0)); *total += 1; @@ -201,7 +190,8 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { if wrapped_stats.git_percentage > 0.05 { println!( - "{bold}🌟 You're a Git Power User!{reset} {bold}{:.1}%{reset} of your commands were Git operations\n", + "{bold}🌟 You're a Git Power User!{reset} {bold}{:.1}%{reset} of your commands were \ + Git operations\n", wrapped_stats.git_percentage * 100.0 ); } @@ -209,7 +199,8 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { let nav_percentage = wrapped_stats.nav_commands as f64 / stats.total_commands as f64 * 100.0; if nav_percentage > 0.05 { println!( - "{bold}🚀 You're a Navigator!{reset} {bold}{nav_percentage:.1}%{reset} of your time was spent navigating directories\n", + "{bold}🚀 You're a Navigator!{reset} {bold}{nav_percentage:.1}%{reset} of your time \ + was spent navigating directories\n", ); } @@ -228,7 +219,8 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { // Error patterns let error_percentage = wrapped_stats.error_rate * 100.0; println!( - "{bold}🚨 Error Analysis{reset}: Your commands failed {bold}{error_percentage:.1}%{reset} of the time\n", + "{bold}🚨 Error Analysis{reset}: Your commands failed {bold}{error_percentage:.1}%{reset} \ + of the time\n", ); // Command evolution @@ -246,11 +238,8 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { } // Find new favorite commands (in top 5 of second half but not in first half) - let first_half_set: HashSet<_> = wrapped_stats - .first_half_commands - .iter() - .map(|(cmd, _)| cmd) - .collect(); + let first_half_set: HashSet<_> = + wrapped_stats.first_half_commands.iter().map(|(cmd, _)| cmd).collect(); let new_favorites: Vec<_> = wrapped_stats .second_half_commands .iter() @@ -270,12 +259,7 @@ fn print_fun_facts(wrapped_stats: &WrappedStats, stats: &Stats, year: i32) { println!("\n🕘 Most Productive Hour: {bold}{hour}{reset} ({count} commands)"); // Night owl or early bird - let hour_num = hour - .split(':') - .next() - .unwrap_or("0") - .parse::() - .unwrap_or(0); + let hour_num = hour.split(':').next().unwrap_or("0").parse::().unwrap_or(0); if hour_num >= 22 || hour_num <= 4 { println!(" You're quite the night owl! 🦉"); } else if (5..=7).contains(&hour_num) { @@ -319,7 +303,8 @@ pub async fn run( let history = db.range(start, end).await?; if history.is_empty() { println!( - "Your history for {year} is empty!\nMaybe 'atuin import' could help you import your previous history 🪄" + "Your history for {year} is empty!\nMaybe 'atuin import' could help you import your \ + previous history 🪄" ); return Ok(()); } @@ -352,10 +337,7 @@ pub async fn run( print_wrapped_header(year); println!("🎉 In {year}, you typed {} commands!", stats.total_commands); - println!( - " That's ~{} commands every day\n", - stats.total_commands / 365 - ); + println!(" That's ~{} commands every day\n", stats.total_commands / 365); println!("Your Top Commands:"); atuin_history::stats::pretty_print(stats.clone(), 1, theme); diff --git a/crates/atuin/src/command/external.rs b/crates/atuin/src/command/external.rs index 5d875e9d3..d5d8ee353 100644 --- a/crates/atuin/src/command/external.rs +++ b/crates/atuin/src/command/external.rs @@ -70,7 +70,8 @@ fn render_not_found(subcommand: &str, bin: &str) -> StyledStr { let _ = write!(output, "{error}error:{error:#} "); let _ = write!( output, - "'{invalid}{subcommand}{invalid:#}' is an official atuin plugin, but it's not installed" + "'{invalid}{subcommand}{invalid:#}' is an official atuin plugin, but it's not \ + installed" ); let _ = write!(output, "\n\n"); let _ = write!(output, "{install_message}"); @@ -82,21 +83,13 @@ fn render_not_found(subcommand: &str, bin: &str) -> StyledStr { let usage = atuin_cmd.render_usage(); let _ = write!(output, "{error}error:{error:#} "); - let _ = write!( - output, - "unrecognized subcommand '{invalid}{subcommand}{invalid:#}' " - ); - let _ = write!( - output, - "and no executable named '{invalid}{bin}{invalid:#}' found in your PATH" - ); + let _ = write!(output, "unrecognized subcommand '{invalid}{subcommand}{invalid:#}' "); + let _ = + write!(output, "and no executable named '{invalid}{bin}{invalid:#}' found in your PATH"); let _ = write!(output, "\n\n"); let _ = write!(output, "{usage}"); let _ = write!(output, "\n\n"); - let _ = write!( - output, - "For more information, try '{literal}--help{literal:#}'." - ); + let _ = write!(output, "For more information, try '{literal}--help{literal:#}'."); output } diff --git a/crates/atuin/src/command/gen_completions.rs b/crates/atuin/src/command/gen_completions.rs index 10d4f6898..f198f9600 100644 --- a/crates/atuin/src/command/gen_completions.rs +++ b/crates/atuin/src/command/gen_completions.rs @@ -70,12 +70,7 @@ impl Cmd { generate_to(shell, &mut cli, env!("CARGO_PKG_NAME"), &out_dir)?; } None => { - generate( - shell, - &mut cli, - env!("CARGO_PKG_NAME"), - &mut std::io::stdout(), - ); + generate(shell, &mut cli, env!("CARGO_PKG_NAME"), &mut std::io::stdout()); } } diff --git a/crates/atuin/src/command/mod.rs b/crates/atuin/src/command/mod.rs index 05fbf5cfa..faf9b842e 100644 --- a/crates/atuin/src/command/mod.rs +++ b/crates/atuin/src/command/mod.rs @@ -1,7 +1,6 @@ use atuin_common::logs::LogConfig; use clap::Subcommand; use eyre::Result; - #[cfg(not(windows))] use rustix::{fs::Mode, process::umask}; @@ -112,10 +111,7 @@ fn semantic_command_capture_sink() -> Option(128); std::thread::spawn(move || { - let Ok(runtime) = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - else { + let Ok(runtime) = tokio::runtime::Builder::new_current_thread().enable_all().build() else { return; }; diff --git a/crates/atuin/src/logs.rs b/crates/atuin/src/logs.rs index 23d75f96b..13c6021df 100644 --- a/crates/atuin/src/logs.rs +++ b/crates/atuin/src/logs.rs @@ -1,6 +1,7 @@ -use atuin_common::logs::{FileConfig, LogConfig, StderrConfig}; use std::fs::OpenOptions; use std::io::IsTerminal; + +use atuin_common::logs::{FileConfig, LogConfig, StderrConfig}; use tracing::Level; use tracing_appender::rolling::{self, RollingFileAppender, Rotation}; use tracing_subscriber::filter::{self, EnvFilter, LevelFilter}; @@ -20,10 +21,7 @@ pub fn init_logging(config: &LogConfig) { } fn get_base_filter(config: &LogConfig) -> EnvFilter { - let level = config - .file - .as_ref() - .map_or(Level::WARN, |f| f.level.to_tracing()); + let level = config.file.as_ref().map_or(Level::WARN, |f| f.level.to_tracing()); EnvFilter::default().add_directive(level.into()) } @@ -73,10 +71,7 @@ enum FileWriterError { } fn make_file_writer(config: &FileConfig) -> Result { - let prefix = config - .name() - .to_str() - .ok_or(FileWriterError::NonUtf8Filename)?; + let prefix = config.name().to_str().ok_or(FileWriterError::NonUtf8Filename)?; let writer = RollingFileAppender::builder() .rotation(Rotation::DAILY) .filename_prefix(prefix) @@ -90,10 +85,7 @@ where { // ATUIN_LOG env var overrides config file level settings let filter: EnvFilter = std::env::var("ATUIN_LOG") - .map_or_else( - |_| get_base_filter(config), - |s| filter::Builder::default().parse_lossy(s), - ) + .map_or_else(|_| get_base_filter(config), |s| filter::Builder::default().parse_lossy(s)) .add_directive("sqlx_sqlite::regexp=off".parse().unwrap()); if let Some(file) = &config.file { @@ -102,10 +94,7 @@ where let file_layer = config.file.as_ref().map(|file| { let writer = make_file_writer(file)?; - let layer = fmt::layer() - .with_writer(writer) - .with_ansi(false) - .with_filter(filter.clone()); + let layer = fmt::layer().with_writer(writer).with_ansi(false).with_filter(filter.clone()); Ok::<_, FileWriterError>(layer) }); @@ -124,12 +113,7 @@ where } else { value }; - let file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(path) - .ok()?; + let file = OpenOptions::new().create(true).truncate(true).write(true).open(path).ok()?; let layer = fmt::layer() .json() .with_writer(file) diff --git a/crates/atuin/src/main.rs b/crates/atuin/src/main.rs index c977c8e8a..50821d10d 100644 --- a/crates/atuin/src/main.rs +++ b/crates/atuin/src/main.rs @@ -4,9 +4,8 @@ use clap::Parser; use clap::builder::Styles; use clap::builder::styling::{AnsiColor, Effects}; -use eyre::Result; - use command::AtuinCmd; +use eyre::Result; mod command; pub(crate) mod logs; diff --git a/crates/atuin/src/print_error.rs b/crates/atuin/src/print_error.rs index 2f8f9bda4..d590904f5 100644 --- a/crates/atuin/src/print_error.rs +++ b/crates/atuin/src/print_error.rs @@ -40,10 +40,10 @@ pub fn format_sync_error(e: SyncError) -> eyre::Report { if matches!(e, SyncError::WrongKey) { print_error( "Wrong encryption key", - "Your local encryption key cannot decrypt the data on the server. \ - This usually means another machine wrote records with a different key.\n\n\ - To fix this, find the correct key by running `atuin key` on a machine that \ - already syncs successfully, then run `atuin store rekey ` here.", + "Your local encryption key cannot decrypt the data on the server. This usually means \ + another machine wrote records with a different key.\n\nTo fix this, find the correct \ + key by running `atuin key` on a machine that already syncs successfully, then run \ + `atuin store rekey ` here.", ); std::process::exit(1); } @@ -83,9 +83,10 @@ fn wrap_text(text: &str, width: usize) -> Vec { #[cfg(test)] mod tests { - use super::wrap_text; use rstest::rstest; + use super::wrap_text; + #[rstest] #[case::width_20(20)] fn wraps_long_text(#[case] width: usize) { diff --git a/crates/atuin/src/sync.rs b/crates/atuin/src/sync.rs index 57d5bb842..0fbe3877e 100644 --- a/crates/atuin/src/sync.rs +++ b/crates/atuin/src/sync.rs @@ -1,14 +1,14 @@ -use atuin_dotfiles::store::{AliasStore, var::VarStore}; -use atuin_scripts::store::ScriptStore; -use eyre::{Context, Result}; - -use atuin_client::{ - database::Database, history::store::HistoryStore, record::sqlite_store::SqliteStore, - settings::Settings, -}; +use atuin_client::database::Database; +use atuin_client::history::store::HistoryStore; +use atuin_client::record::sqlite_store::SqliteStore; +use atuin_client::settings::Settings; use atuin_common::encryption::paseto_v4; use atuin_domain::record::RecordId; +use atuin_dotfiles::store::AliasStore; +use atuin_dotfiles::store::var::VarStore; use atuin_kv::store::KvStore; +use atuin_scripts::store::ScriptStore; +use eyre::{Context, Result}; // This is the only crate that ties together all other crates. // Therefore, it's the only crate where functions tying together all stores can live diff --git a/crates/tests-database/src/helpers.rs b/crates/tests-database/src/helpers.rs index d02210f2a..c1591d0d2 100644 --- a/crates/tests-database/src/helpers.rs +++ b/crates/tests-database/src/helpers.rs @@ -55,10 +55,11 @@ pub async fn destroy_test_db(settings: &DbSettings) -> eyre::Result<()> { #[cfg(test)] mod tests { - use super::*; use regex::Regex; use rstest::rstest; + use super::*; + #[rstest] #[case::none(None, r"sqlite://.*[\\/]atuin_test_db_\d+")] #[case::with_param( diff --git a/crates/tests-database/tests/db_story.rs b/crates/tests-database/tests/db_story.rs index 46ee3a2c2..ce0645b61 100644 --- a/crates/tests-database/tests/db_story.rs +++ b/crates/tests-database/tests/db_story.rs @@ -1,9 +1,7 @@ use atuin_common::utils::{crypto_random_string, uuid_v7}; use atuin_domain::record::{EncryptedData, Host, HostId, Record, RecordIdx, RecordTag}; -use atuin_server_database::{ - Database, DbSettings, DbType, - models::{NewSession, NewUser, User}, -}; +use atuin_server_database::models::{NewSession, NewUser, User}; +use atuin_server_database::{Database, DbSettings, DbType}; use atuin_server_postgres::Postgres; use atuin_server_sqlite::Sqlite; use rstest::rstest; @@ -24,10 +22,7 @@ impl Drop for TestDb { fn drop(&mut self) { let settings = self.settings.clone(); let _ = std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); + let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); rt.block_on(async { if let Err(e) = destroy_test_db(&settings).await { eprintln!("Failed to destroy test db: {:?}", e); @@ -111,48 +106,24 @@ async fn run_the_test(settings: &DbSettings) -> eyre::Result<()> { let status = db.status(&user).await?; assert!(status.hosts.contains_key(&host_a.id)); assert!(status.hosts.contains_key(&host_b.id)); - assert_eq!( - status - .hosts - .get(&host_a.id) - .unwrap() - .get(&RecordTag::History) - .unwrap() - .clone(), - 6 - ); - assert_eq!( - status - .hosts - .get(&host_b.id) - .unwrap() - .get(&RecordTag::History) - .unwrap() - .clone(), - 2 - ); + assert_eq!(status.hosts.get(&host_a.id).unwrap().get(&RecordTag::History).unwrap().clone(), 6); + assert_eq!(status.hosts.get(&host_b.id).unwrap().get(&RecordTag::History).unwrap().clone(), 2); // Get 3 records from the beginning - let recs = db - .next_records(&user, host_a.id, RecordTag::History, None, 3) - .await?; + let recs = db.next_records(&user, host_a.id, RecordTag::History, None, 3).await?; assert_eq!(recs.len(), 3); assert_eq!(recs[0].idx, 1); assert_eq!(recs.last().unwrap().idx, 3); // Get from the end, for host a. Get more than exists - let recs = db - .next_records(&user, host_a.id, RecordTag::History, Some(4), 10) - .await?; + let recs = db.next_records(&user, host_a.id, RecordTag::History, Some(4), 10).await?; assert_eq!(recs.len(), 3); assert_eq!(recs[0].idx, 4); // check the head record is idx 4 assert_eq!(recs.last().unwrap().idx, 6); // delete_store db.delete_store(&user).await?; - let recs = db - .next_records(&user, host_a.id, RecordTag::History, Some(4), 10) - .await?; + let recs = db.next_records(&user, host_a.id, RecordTag::History, Some(4), 10).await?; assert_eq!(recs.len(), 0); Ok(())