Skip to content

Commit

Permalink
Timezone configuration support
Browse files Browse the repository at this point in the history
  • Loading branch information
cyqsimon committed Jan 26, 2024
1 parent 9f1d917 commit 6600546
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 110 deletions.
133 changes: 133 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion atuin-client/Cargo.toml
Expand Up @@ -21,7 +21,7 @@ atuin-common = { path = "../atuin-common", version = "17.2.1" }

log = { workspace = true }
base64 = { workspace = true }
time = { workspace = true }
time = { workspace = true, features = ["macros", "formatting"] }
clap = { workspace = true }
eyre = { workspace = true }
directories = { workspace = true }
Expand Down Expand Up @@ -52,6 +52,7 @@ thiserror = { workspace = true }
futures = "0.3"
crypto_secretbox = "0.1.1"
generic-array = { version = "0.14", features = ["serde"] }
serde_with = "3.5.1"

# encryption
rusty_paseto = { version = "0.6.0", default-features = false }
Expand Down
6 changes: 6 additions & 0 deletions atuin-client/config.toml
Expand Up @@ -16,6 +16,12 @@
## date format used, either "us" or "uk"
# dialect = "us"

## default timezone to use when displaying time
## either "l", "local" to use the system's current local timezone, or an offset
## from UTC in the format of "<+|->H[H][:M[M][:S[S]]]"
## for example: "+9", "-05", "+03:30", "-01:23:45", etc.
# timezone = "+0"

## enable or disable automatic sync
# auto_sync = true

Expand Down
98 changes: 89 additions & 9 deletions atuin-client/src/settings.rs
@@ -1,5 +1,6 @@
use std::{
convert::TryFrom,
fmt,
io::prelude::*,
path::{Path, PathBuf},
str::FromStr,
Expand All @@ -10,13 +11,18 @@ use clap::ValueEnum;
use config::{
builder::DefaultState, Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat,
};
use eyre::{eyre, Context, Result};
use eyre::{bail, eyre, Context, Error, Result};
use fs_err::{create_dir_all, File};
use parse_duration::parse;
use regex::RegexSet;
use semver::Version;
use serde::Deserialize;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use serde_with::DeserializeFromStr;
use time::{
format_description::{well_known::Rfc3339, FormatItem},
macros::format_description,
OffsetDateTime, UtcOffset,
};
use uuid::Uuid;

pub const HISTORY_PAGE_SIZE: i64 = 100;
Expand Down Expand Up @@ -122,6 +128,46 @@ impl From<Dialect> for interim::Dialect {
}
}

/// Type wrapper around `time::UtcOffset` to support a wider variety of timezone formats.
///
/// Note that the parsing of this struct needs to be done before starting any
/// multithreaded runtime, otherwise it will fail on most Unix systems.
///
/// See: https://github.com/atuinsh/atuin/pull/1517#discussion_r1447516426
#[derive(Clone, Copy, Debug, Eq, PartialEq, DeserializeFromStr)]
pub struct Timezone(pub UtcOffset);
impl fmt::Display for Timezone {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// format: <+|-><hour>[:<minute>[:<second>]]
static OFFSET_FMT: &[FormatItem<'_>] =
format_description!("[offset_hour sign:mandatory padding:none][optional [:[offset_minute padding:none][optional [:[offset_second padding:none]]]]]");
impl FromStr for Timezone {
type Err = Error;

fn from_str(s: &str) -> Result<Self> {
// local timezone
if matches!(s.to_lowercase().as_str(), "l" | "local") {
let offset = UtcOffset::current_local_offset()?;
return Ok(Self(offset));
}

// offset from UTC
if let Ok(offset) = UtcOffset::parse(s, OFFSET_FMT) {
return Ok(Self(offset));
}

// IDEA: Currently named timezones are not supported, because the well-known crate
// for this is `chrono_tz`, which is not really interoperable with the datetime crate
// that we currently use - `time`. If ever we migrate to using `chrono`, this would
// be a good feature to add.

bail!(r#""{s}" is not a valid timezone spec"#)
}
}

#[derive(Clone, Debug, Deserialize, Copy)]
pub enum Style {
#[serde(rename = "auto")]
Expand Down Expand Up @@ -181,6 +227,7 @@ pub struct Sync {
#[derive(Clone, Debug, Deserialize)]
pub struct Settings {
pub dialect: Dialect,
pub timezone: Timezone,
pub style: Style,
pub auto_sync: bool,
pub update_check: bool,
Expand Down Expand Up @@ -229,11 +276,6 @@ pub struct Settings {
// config! Keep secrets and settings apart.
#[serde(skip)]
pub session_token: String,

// This is determined at startup and cached.
// This is due to non-threadsafe get-env limitations.
#[serde(skip)]
pub local_tz: Option<time::UtcOffset>,
}

impl Settings {
Expand Down Expand Up @@ -403,6 +445,7 @@ impl Settings {
.set_default("key_path", key_path.to_str())?
.set_default("session_path", session_path.to_str())?
.set_default("dialect", "us")?
.set_default("timezone", "+0")?
.set_default("auto_sync", true)?
.set_default("update_check", true)?
.set_default("sync_address", "https://api.atuin.sh")?
Expand Down Expand Up @@ -503,8 +546,6 @@ impl Settings {
settings.session_token = String::from("not logged in");
}

settings.local_tz = time::UtcOffset::current_local_offset().ok();

Ok(settings)
}

Expand All @@ -525,3 +566,42 @@ impl Default for Settings {
.expect("Could not deserialize config")
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use eyre::Result;

use super::Timezone;

#[test]
fn can_parse_offset_timezone_spec() -> Result<()> {
assert_eq!(Timezone::from_str("+02")?.0.as_hms(), (2, 0, 0));
assert_eq!(Timezone::from_str("-04")?.0.as_hms(), (-4, 0, 0));
assert_eq!(Timezone::from_str("+05:30")?.0.as_hms(), (5, 30, 0));
assert_eq!(Timezone::from_str("-09:30")?.0.as_hms(), (-9, -30, 0));

// single digit hours are allowed
assert_eq!(Timezone::from_str("+2")?.0.as_hms(), (2, 0, 0));
assert_eq!(Timezone::from_str("-4")?.0.as_hms(), (-4, 0, 0));
assert_eq!(Timezone::from_str("+5:30")?.0.as_hms(), (5, 30, 0));
assert_eq!(Timezone::from_str("-9:30")?.0.as_hms(), (-9, -30, 0));

// fully qualified form
assert_eq!(Timezone::from_str("+09:30:00")?.0.as_hms(), (9, 30, 0));
assert_eq!(Timezone::from_str("-09:30:00")?.0.as_hms(), (-9, -30, 0));

// these offsets don't really exist but are supported anyway
assert_eq!(Timezone::from_str("+0:5")?.0.as_hms(), (0, 5, 0));
assert_eq!(Timezone::from_str("-0:5")?.0.as_hms(), (0, -5, 0));
assert_eq!(Timezone::from_str("+01:23:45")?.0.as_hms(), (1, 23, 45));
assert_eq!(Timezone::from_str("-01:23:45")?.0.as_hms(), (-1, -23, -45));

// require a leading sign for clarity
assert!(Timezone::from_str("5").is_err());
assert!(Timezone::from_str("10:30").is_err());

Ok(())
}
}

0 comments on commit 6600546

Please sign in to comment.