aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/atuin_client/settings.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-12 01:54:21 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-12 01:54:21 +0200
commitbbdf38018b47328b5faa2cef635c37095045be72 (patch)
tree8983817d547551ae12508a8ae8731b622d990af4 /crates/turtle/src/atuin_client/settings.rs
parentfeat(server): Make user stuff stateless (diff)
downloadatuin-bbdf38018b47328b5faa2cef635c37095045be72.zip
feat(server): Really make users stateless (with tests)
This commit also remove another load of unneeded features.
Diffstat (limited to 'crates/turtle/src/atuin_client/settings.rs')
-rw-r--r--crates/turtle/src/atuin_client/settings.rs225
1 files changed, 89 insertions, 136 deletions
diff --git a/crates/turtle/src/atuin_client/settings.rs b/crates/turtle/src/atuin_client/settings.rs
index e8ff98ee..d84e2eb0 100644
--- a/crates/turtle/src/atuin_client/settings.rs
+++ b/crates/turtle/src/atuin_client/settings.rs
@@ -1,8 +1,13 @@
-use std::{collections::HashMap, fmt, io::prelude::*, path::PathBuf, str::FromStr, sync::OnceLock};
+use crypto_secretbox::Key;
+use std::{
+ collections::HashMap, fmt, fs::read_to_string, io::prelude::Write, path::PathBuf, str::FromStr,
+ sync::OnceLock,
+};
use tokio::sync::OnceCell;
+use uuid::Uuid;
-use crate::atuin_common::record::HostId;
use crate::atuin_common::utils;
+use crate::{atuin_client::encryption::decode_key, atuin_common::record::HostId};
use clap::ValueEnum;
use config::{
Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat, builder::DefaultState,
@@ -217,16 +222,6 @@ pub(crate) enum KeymapMode {
Auto,
}
-impl KeymapMode {
- pub(crate) fn as_str(&self) -> &'static str {
- match self {
- KeymapMode::Emacs => "EMACS",
- KeymapMode::VimNormal => "VIMNORMAL",
- KeymapMode::VimInsert => "VIMINSERT",
- KeymapMode::Auto => "AUTO",
- }
- }
-}
// We want to translate the config to crossterm::cursor::SetCursorStyle, but
// the original type does not implement trait serde::Deserialize unfortunately.
@@ -257,19 +252,6 @@ pub(crate) enum CursorStyle {
SteadyBar,
}
-impl CursorStyle {
- pub(crate) fn as_str(&self) -> &'static str {
- match self {
- CursorStyle::DefaultUserShape => "DEFAULT",
- CursorStyle::BlinkingBlock => "BLINKBLOCK",
- CursorStyle::SteadyBlock => "STEADYBLOCK",
- CursorStyle::BlinkingUnderScore => "BLINKUNDERLINE",
- CursorStyle::SteadyUnderScore => "STEADYUNDERLINE",
- CursorStyle::BlinkingBar => "BLINKBAR",
- CursorStyle::SteadyBar => "STEADYBAR",
- }
- }
-}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct Stats {
@@ -330,36 +312,6 @@ impl Default for Stats {
}
}
-/// Resolved authentication state for sync operations.
-///
-/// Determined at runtime by examining which tokens are available and what
-/// server the client is configured to talk to. Operations use this to pick
-/// the right auth header and endpoint style.
-#[cfg(feature = "sync")]
-#[derive(Debug, Clone)]
-pub(crate) enum SyncAuth {
- /// Self-hosted Rust server. Uses `Authorization: Token <session>` and
- /// legacy endpoints.
- Legacy { token: String },
-
- /// Not authenticated at all. Contains an actionable user-facing message.
- NotLoggedIn { reason: String },
-}
-
-#[cfg(feature = "sync")]
-impl SyncAuth {
- /// Convert into the auth token type used by the API client.
- ///
- /// Returns an error with an actionable message for `NotLoggedIn`.
- pub(crate) fn into_auth_token(self) -> Result<crate::atuin_client::api_client::AuthToken> {
- use crate::atuin_client::api_client::AuthToken;
- match self {
- SyncAuth::Legacy { token } => Ok(AuthToken(token)),
- SyncAuth::NotLoggedIn { reason } => Err(eyre!(reason)),
- }
- }
-}
-
#[derive(Clone, Debug, Deserialize, Default, Serialize)]
#[expect(clippy::struct_excessive_bools)]
pub(crate) struct Keys {
@@ -781,14 +733,6 @@ impl UiColumn {
column_type,
}
}
-
- pub(crate) fn with_width(column_type: UiColumnType, width: u16) -> Self {
- Self {
- column_type,
- width,
- expand: column_type == UiColumnType::Command,
- }
- }
}
// Custom deserialize to handle both string and object formats:
@@ -902,6 +846,82 @@ impl Default for Ui {
}
}
+/// Sync-specific settings.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub(crate) struct Sync {
+ /// The sync address for atuin.
+ pub(crate) address: String,
+
+ #[serde(default)]
+ pub(crate) frequency: String,
+
+ #[serde(default)]
+ pub(crate) auto: bool,
+
+ #[serde(default)]
+ pub(crate) user_id_path: Option<PathBuf>,
+
+ #[serde(default)]
+ pub(crate) encryption_key_path: Option<PathBuf>,
+}
+
+impl Sync {
+ fn try_read_file(file: Option<&PathBuf>) -> Result<Option<String>> {
+ if let Some(path) = file {
+ if path.try_exists()? {
+ let user = read_to_string(path)?;
+
+ if user.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(user))
+ }
+ } else {
+ // It's okay that the file doesn't exist.
+ // The important part is to error out if we can't access it (e.g. Because of missing
+ // permissions).
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub(crate) fn have_sync_user(&self) -> Result<bool> {
+ let sa = self.user_id()?;
+ Ok(sa.is_some())
+ }
+
+ pub(crate) fn user_id(&self) -> Result<Option<Uuid>> {
+ Self::try_read_file(self.user_id_path.as_ref())?
+ .map(|file| Uuid::try_parse(&file).map_err(Into::into))
+ .transpose()
+ }
+ pub(crate) fn encryption_key(&self) -> Result<Option<Key>> {
+ Self::try_read_file(self.encryption_key_path.as_ref())?
+ .map(decode_key)
+ .transpose()
+ }
+
+ pub(crate) async fn should_sync(&self) -> Result<bool> {
+ if !self.auto || !self.have_sync_user()? {
+ return Ok(false);
+ }
+
+ if self.frequency == "0" || self.frequency.is_empty() {
+ return Ok(true);
+ }
+
+ match parse_duration(self.frequency.as_str()) {
+ Ok(d) => {
+ let d = time::Duration::try_from(d)?;
+ Ok(OffsetDateTime::now_utc() - Settings::last_sync().await? >= d)
+ }
+ Err(e) => Err(eyre!("failed to check sync: {}", e)),
+ }
+ }
+}
+
#[derive(Clone, Debug, Deserialize, Serialize)]
#[expect(clippy::struct_excessive_bools)]
pub(crate) struct Settings {
@@ -909,15 +929,9 @@ pub(crate) struct Settings {
pub(crate) dialect: Dialect,
pub(crate) timezone: Timezone,
pub(crate) style: Style,
- pub(crate) auto_sync: bool,
-
- /// The sync address for atuin.
- pub(crate) sync_address: String,
- pub(crate) sync_frequency: String,
pub(crate) db_path: String,
pub(crate) record_store_path: String,
- pub(crate) key_path: String,
pub(crate) search_mode: SearchMode,
pub(crate) filter_mode: Option<FilterMode>,
pub(crate) filter_mode_shell_up_key_binding: Option<FilterMode>,
@@ -963,6 +977,9 @@ pub(crate) struct Settings {
pub(crate) command_chaining: bool,
#[serde(default)]
+ pub(crate) sync: Sync,
+
+ #[serde(default)]
pub(crate) stats: Stats,
#[serde(default)]
@@ -994,24 +1011,6 @@ pub(crate) struct Settings {
}
impl Settings {
- pub(crate) fn utc() -> Self {
- Self::builder()
- .expect("Could not build default")
- .set_override("timezone", "0")
- .expect("failed to override timezone with UTC")
- .build()
- .expect("Could not build config")
- .try_deserialize()
- .expect("Could not deserialize config")
- }
-
- pub(crate) fn effective_data_dir() -> PathBuf {
- DATA_DIR
- .get()
- .cloned()
- .unwrap_or_else(crate::atuin_common::utils::data_dir)
- }
-
// -- Meta store: lazily initialized on first access --
pub(crate) async fn meta_store() -> Result<&'static crate::atuin_client::meta::MetaStore> {
@@ -1037,34 +1036,6 @@ impl Settings {
Self::meta_store().await?.save_sync_time().await
}
- pub(crate) async fn should_sync(&self) -> Result<bool> {
- if !self.auto_sync || !self.have_sync_key().await? {
- return Ok(false);
- }
-
- if self.sync_frequency == "0" {
- return Ok(true);
- }
-
- match parse_duration(self.sync_frequency.as_str()) {
- Ok(d) => {
- let d = time::Duration::try_from(d)?;
- Ok(OffsetDateTime::now_utc() - Settings::last_sync().await? >= d)
- }
- Err(e) => Err(eyre!("failed to check sync: {}", e)),
- }
- }
-
- pub(crate) async fn have_sync_key(&self) -> Result<bool> {
- let sa = self.sync_auth().await?;
- Ok(matches!(sa, SyncAuth::Legacy { .. }))
- }
-
- pub(crate) async fn sync_auth(&self) -> Result<SyncAuth> {
- // TODO(@bpeetz): Add this <2026-06-11>
- todo!()
- }
-
pub(crate) fn default_filter_mode(&self, git_root: bool) -> FilterMode {
self.filter_mode
.filter(|x| self.search.filters.contains(x))
@@ -1213,7 +1184,7 @@ impl Settings {
let config_dir = crate::atuin_common::utils::config_dir();
create_dir_all(&config_dir)
- .wrap_err_with(|| format!("could not create dir {config_dir:?}"))?;
+ .wrap_err_with(|| format!("could not create dir {}", config_dir.display()))?;
let mut config_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
PathBuf::from(p)
@@ -1414,12 +1385,8 @@ impl Settings {
}
pub(crate) fn paths_ok(&self) -> bool {
- let paths = [
- &self.db_path,
- &self.record_store_path,
- &self.key_path,
- &self.meta.db_path,
- ];
+ // TODO(@bpeetz): Add the `sync.*` paths <2026-06-11>
+ let paths = [&self.db_path, &self.record_store_path, &self.meta.db_path];
paths.iter().all(|p| !utils::broken_symlink(p))
}
}
@@ -1437,20 +1404,6 @@ impl Default for Settings {
}
}
-/// Initialize the meta store configuration for testing.
-///
-/// This should only be used in tests. It allows tests to bypass the normal
-/// Settings::new() flow while still being able to use Settings::host_id()
-/// and other meta store dependent functions.
-///
-/// # Safety
-/// This function is not thread-safe with concurrent calls to Settings::new()
-/// or other meta store initialization. Only call from tests.
-#[doc(hidden)]
-pub(crate) fn init_meta_config_for_testing(meta_db_path: impl Into<String>, local_timeout: f64) {
- META_CONFIG.set((meta_db_path.into(), local_timeout)).ok();
-}
-
#[cfg(test)]
pub(crate) fn test_local_timeout() -> f64 {
std::env::var("ATUIN_TEST_LOCAL_TIMEOUT")