diff --git a/Cargo.lock b/Cargo.lock index 859b12d71..2ca2764ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,19 +393,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link 0.2.1", -] - [[package]] name = "clap" version = "4.6.1" @@ -538,12 +525,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1892,30 +1873,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -3380,7 +3337,6 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" name = "starship" version = "1.25.1" dependencies = [ - "chrono", "clap", "clap_complete", "clap_complete_nushell", @@ -3391,6 +3347,7 @@ dependencies = [ "guess_host_triple", "home", "indexmap", + "jiff", "jsonc-parser", "log", "mockall", diff --git a/Cargo.toml b/Cargo.toml index 680f4306d..77d06c0b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ config-schema = ["schemars"] notify = ["notify-rust"] [dependencies] -chrono = { version = "0.4.44", default-features = false, features = ["clock", "std", "wasmbind"] } clap = { version = "4.6.1", features = ["derive", "cargo", "unicode"] } clap_complete = "4.6.5" clap_complete_nushell = "4.6.0" @@ -44,6 +43,7 @@ dunce = "1.0.5" # default feature restriction addresses https://github.com/starship/starship/issues/4251 gix = { version = "0.84.0", default-features = false, features = ["max-performance-safe", "revision", "zlib-rs", "status", "sha1"] } indexmap = { version = "2.14.0", features = ["serde"] } +jiff = { version = "0.2.24", features = ["serde"] } jsonc-parser = { version = "0.32.4", features = ["serde"] } log = { version = "0.4.30", features = ["std"] } # notify-rust is optional (on by default) because the crate doesn't currently build for darwin with nix diff --git a/docs/config/README.md b/docs/config/README.md index 11f19f04e..f5e46fb31 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -4814,7 +4814,7 @@ format = 'via [$symbol$workspace]($style) ' ## Time The `time` module shows the current **local** time. -The `format` configuration value is used by the [`chrono`](https://crates.io/crates/chrono) crate to control how the time is displayed. Take a look [at the chrono strftime docs](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) to see what options are available. +The `format` configuration value is used by the [`jiff`](https://crates.io/crates/jiff) crate to control how the time is displayed. Take a look [at the jiff strftime docs](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html) to see what options are available. > [!TIP] > This module is disabled by default. @@ -4822,15 +4822,15 @@ The `format` configuration value is used by the [`chrono`](https://crates.io/cra ### Options -| Option | Default | Description | -| ----------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `format` | `'at [$time]($style) '` | The format string for the module. | -| `use_12hr` | `false` | Enables 12 hour formatting | -| `time_format` | see below | The [chrono format string](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) used to format the time. | -| `style` | `'bold yellow'` | The style for the module time | -| `utc_time_offset` | `'local'` | Sets the UTC offset to use. Range from -24 < x < 24. Allows floats to accommodate 30/45 minute timezone offsets. | -| `disabled` | `true` | Disables the `time` module. | -| `time_range` | `'-'` | Sets the time range during which the module will be shown. Times must be specified in 24-hours format | +| Option | Default | Description | +| ----------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `format` | `'at [$time]($style) '` | The format string for the module. | +| `use_12hr` | `false` | Enables 12 hour formatting | +| `time_format` | see below | The [jiff format string](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html) used to format the time. | +| `style` | `'bold yellow'` | The style for the module time | +| `utc_time_offset` | `'local'` | Sets the UTC offset to use. Either an IANA time zone name or a range from -24 < x < 24. Allows floats to accommodate 30/45 minute timezone offsets. | +| `disabled` | `true` | Disables the `time` module. | +| `time_range` | `'-'` | Sets the time range during which the module will be shown. Times must be specified in 24-hours format | If `use_12hr` is `true`, then `time_format` defaults to `'%r'`. Otherwise, it defaults to `'%T'`. Manually setting `time_format` will override the `use_12hr` setting. @@ -4846,6 +4846,8 @@ Manually setting `time_format` will override the `use_12hr` setting. ### Example +#### With UTC offset + ```toml # ~/.config/starship.toml @@ -4857,6 +4859,17 @@ utc_time_offset = '-5' time_range = '10:00:00-14:00:00' ``` +#### With Timezone name + +```toml +# ~/.config/starship.toml + +[time] +disabled = false +time_format = '%T' +utc_time_offset = 'Europe/Berlin' +``` + ## Typst The `typst` module shows the current installed version of Typst used in a project. diff --git a/src/configs/time.rs b/src/configs/time.rs index b4d9d706f..4a45bceb2 100644 --- a/src/configs/time.rs +++ b/src/configs/time.rs @@ -1,5 +1,13 @@ +use crate::config::Either; use serde::{Deserialize, Serialize}; +// Wrapper struct to enable serde serialization/deserialization for jiff::tz::TimeZone +#[derive(Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct TimezoneWrapper( + #[serde(with = "jiff::fmt::serde::tz::required")] pub jiff::tz::TimeZone, +); + #[derive(Clone, Deserialize, Serialize)] #[cfg_attr( feature = "config-schema", @@ -14,7 +22,8 @@ pub struct TimeConfig<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub time_format: Option<&'a str>, pub disabled: bool, - pub utc_time_offset: &'a str, + #[cfg_attr(feature = "config-schema", schemars(with = "String"))] + pub utc_time_offset: Either, pub time_range: &'a str, } @@ -26,7 +35,7 @@ impl Default for TimeConfig<'_> { use_12hr: false, time_format: None, disabled: true, - utc_time_offset: "local", + utc_time_offset: Either::Second("local"), time_range: "-", } } diff --git a/src/modules/aws.rs b/src/modules/aws.rs index 0fff267ea..8e8722fe2 100644 --- a/src/modules/aws.rs +++ b/src/modules/aws.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; -use chrono::DateTime; use ini::Ini; +use jiff::{Timestamp, Zoned}; use serde_json as json; use sha1::{Digest, Sha1}; @@ -149,7 +149,7 @@ fn get_credentials_duration( .find_map(|env_var| context.get_env(env_var)) { // get expiration from environment variables - chrono::DateTime::parse_from_rfc3339(&expiration_date).ok() + expiration_date.parse::().ok() } else if let Some(section) = get_creds(context, aws_creds).and_then(|creds| get_profile_creds(creds, aws_profile)) { @@ -158,7 +158,7 @@ fn get_credentials_duration( expiration_keys .iter() .find_map(|expiration_key| section.get(expiration_key)) - .and_then(|expiration| DateTime::parse_from_rfc3339(expiration).ok()) + .and_then(|expiration| expiration.parse::().ok()) } else { // get expiration from cached SSO credentials let config = get_config(context, aws_config)?; @@ -172,10 +172,10 @@ fn get_credentials_duration( let sso_cred_json: json::Value = json::from_str(&crate::utils::read_file(&sso_cred_path).ok()?).ok()?; let expires_at = sso_cred_json.get("expiresAt")?.as_str(); - DateTime::parse_from_rfc3339(expires_at?).ok() + expires_at?.parse::().ok() }?; - Some(expiration_date.timestamp() - chrono::Local::now().timestamp()) + Some(expiration_date.as_second() - Zoned::now().timestamp().as_second()) } fn alias_name(name: Option, aliases: &HashMap) -> Option { @@ -332,6 +332,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { #[cfg(test)] mod tests { + use super::*; use crate::test::ModuleRenderer; use nu_ansi_term::Color; use std::fs::{File, create_dir}; @@ -736,21 +737,16 @@ credential_process = /opt/bin/awscreds-retriever #[test] fn expiration_date_set() { - use chrono::{DateTime, SecondsFormat, Utc}; - let expiration_env_vars = ["AWS_SESSION_EXPIRATION", "AWS_CREDENTIAL_EXPIRATION"]; for env_var in expiration_env_vars { - let now_plus_half_hour: DateTime = - DateTime::from_timestamp(chrono::Local::now().timestamp() + 1800, 0).unwrap(); + let now_plus_half_hour = + Timestamp::from_second(Zoned::now().timestamp().as_second() + 1800).unwrap(); let actual = ModuleRenderer::new("aws") .env("AWS_PROFILE", "astronauts") .env("AWS_REGION", "ap-northeast-2") .env("AWS_ACCESS_KEY_ID", "dummy") - .env( - env_var, - now_plus_half_hour.to_rfc3339_opts(SecondsFormat::Secs, true), - ) + .env(env_var, now_plus_half_hour.to_string()) .collect(); let possible_values = [ @@ -772,16 +768,14 @@ credential_process = /opt/bin/awscreds-retriever #[test] fn expiration_date_set_from_file() -> io::Result<()> { - use chrono::{DateTime, Utc}; - let dir = tempfile::tempdir()?; let credentials_path = dir.path().join("credentials"); let mut file = File::create(&credentials_path)?; - let now_plus_half_hour: DateTime = - DateTime::from_timestamp(chrono::Local::now().timestamp() + 1800, 0).unwrap(); + let now_plus_half_hour = + Timestamp::from_second(Zoned::now().timestamp().as_second() + 1800).unwrap(); - let expiration_date = now_plus_half_hour.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + let expiration_date = now_plus_half_hour.to_string(); let expiration_keys = ["expiration", "x_security_token_expires"]; for key in expiration_keys { @@ -847,10 +841,7 @@ aws_secret_access_key=dummy #[test] fn expiration_date_set_expired() { - use chrono::{DateTime, SecondsFormat, Utc}; - - let now: DateTime = - DateTime::from_timestamp(chrono::Local::now().timestamp() - 1800, 0).unwrap(); + let now = Timestamp::from_second(Zoned::now().timestamp().as_second() - 1800).unwrap(); let symbol = "!!!"; @@ -862,10 +853,7 @@ aws_secret_access_key=dummy .env("AWS_PROFILE", "astronauts") .env("AWS_REGION", "ap-northeast-2") .env("AWS_ACCESS_KEY_ID", "dummy") - .env( - "AWS_SESSION_EXPIRATION", - now.to_rfc3339_opts(SecondsFormat::Secs, true), - ) + .env("AWS_SESSION_EXPIRATION", now.to_string()) .collect(); let expected = Some(format!( "on {}", @@ -1056,8 +1044,6 @@ credential_process = /opt/bin/awscreds-for-tests #[test] fn sso_legacy_set() -> io::Result<()> { - use chrono::{DateTime, SecondsFormat, Utc}; - let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?; std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?; @@ -1080,16 +1066,10 @@ sso_role_name = .join(".aws/sso/cache/a47a4e57aecc96b31b4f083543924bd6f828e65a.json"), )?; - let one_second_ago: DateTime = - DateTime::from_timestamp(chrono::Local::now().timestamp() - 1, 0).unwrap(); + let one_second_ago = + Timestamp::from_second(Zoned::now().timestamp().as_second() - 1).unwrap(); - file.write_all( - format!( - r#"{{"expiresAt": "{}"}}"#, - one_second_ago.to_rfc3339_opts(SecondsFormat::Secs, true) - ) - .as_bytes(), - )?; + file.write_all(format!(r#"{{"expiresAt": "{one_second_ago}"}}"#).as_bytes())?; file.sync_all()?; let actual = module_renderer.collect(); @@ -1104,8 +1084,6 @@ sso_role_name = #[test] fn sso_set() -> io::Result<()> { - use chrono::{DateTime, SecondsFormat, Utc}; - let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?; std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?; @@ -1134,16 +1112,10 @@ sso_registration_scopes = sso:account:access .join(".aws/sso/cache/7505d64a54e061b7acd54ccd58b49dc43500b635.json"), )?; - let one_second_ago: DateTime = - DateTime::from_timestamp(chrono::Local::now().timestamp() - 1, 0).unwrap(); + let one_second_ago = + Timestamp::from_second(Zoned::now().timestamp().as_second() - 1).unwrap(); - cache_file.write_all( - format!( - r#"{{"expiresAt": "{}"}}"#, - one_second_ago.to_rfc3339_opts(SecondsFormat::Secs, true) - ) - .as_bytes(), - )?; + cache_file.write_all(format!(r#"{{"expiresAt": "{one_second_ago}"}}"#).as_bytes())?; cache_file.sync_all()?; let actual = module_renderer diff --git a/src/modules/time.rs b/src/modules/time.rs index 7aae0398c..c19d2dcb6 100644 --- a/src/modules/time.rs +++ b/src/modules/time.rs @@ -1,7 +1,12 @@ -use chrono::{DateTime, FixedOffset, Local, NaiveTime, Utc}; +use jiff::{ + Timestamp, Zoned, + civil::Time, + tz::{Offset, TimeZone}, +}; use super::{Context, Module, ModuleConfig}; -use crate::configs::time::TimeConfig; +use crate::config::Either; +use crate::configs::time::{TimeConfig, TimezoneWrapper}; use crate::formatter::StringFormatter; /// Outputs the current time @@ -17,7 +22,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { // Hide prompt if current time is not inside time_range let (display_start, display_end) = parse_time_range(config.time_range); - let time_now = Local::now().time(); + let time_now = Zoned::now().time(); if !is_inside_time_range(time_now, display_start, display_end) { return None; } @@ -27,17 +32,26 @@ pub fn module<'a>(context: &'a Context) -> Option> { log::trace!("Timer module is enabled with format string: {time_format}"); - let formatted_time_string = if config.utc_time_offset != "local" { - create_offset_time_string(Utc::now(), config.utc_time_offset, time_format).unwrap_or_else( - |_| { - log::warn!( - "Invalid utc_time_offset configuration provided! Falling back to \"local\"." - ); - format_time(time_format, Local::now()) - }, - ) - } else { - format_time(time_format, Local::now()) + let formatted_time_string = match &config.utc_time_offset { + Either::First(TimezoneWrapper(tz)) => { + // Use IANA timezone name + let target_time = Timestamp::now().to_zoned(tz.clone()); + format_time_fixed_offset(time_format, target_time) + } + Either::Second("local") => { + // Use local timezone + format_time(time_format, Zoned::now()) + } + Either::Second(utc_time_offset) => { + // Use numeric offset + create_offset_time_string(Timestamp::now(), utc_time_offset, time_format) + .unwrap_or_else(|_| { + log::warn!( + "Invalid utc_time_offset configuration provided! Falling back to \"local\"." + ); + format_time(time_format, Zoned::now()) + }) + } }; let parsed = StringFormatter::new(config.format).and_then(|formatter| { @@ -65,10 +79,10 @@ pub fn module<'a>(context: &'a Context) -> Option> { } fn create_offset_time_string( - utc_time: DateTime, + utc_time: Timestamp, utc_time_offset_str: &str, time_format: &str, -) -> Result { +) -> Result { // Using floats to allow 30/45 minute offsets: https://www.timeanddate.com/time/time-zones-interesting.html let utc_time_offset_in_hours = utc_time_offset_str.parse::().unwrap_or( // Passing out of range value to force falling back to "local" @@ -76,38 +90,34 @@ fn create_offset_time_string( ); if utc_time_offset_in_hours < 24_f32 && utc_time_offset_in_hours > -24_f32 { let utc_offset_in_seconds: i32 = (utc_time_offset_in_hours * 3600_f32) as i32; - let Some(timezone_offset) = FixedOffset::east_opt(utc_offset_in_seconds) else { - return Err("Invalid offset"); - }; + let timezone_offset = Offset::from_seconds(utc_offset_in_seconds) + .map_err(|err| format!("Invalid timezone offset: {err:?}"))?; + let tz = TimeZone::fixed(timezone_offset); log::trace!("Target timezone offset is {timezone_offset}"); - let target_time = utc_time.with_timezone(&timezone_offset); + let target_time = utc_time.to_zoned(tz); log::trace!("Time in target timezone now is {target_time}"); Ok(format_time_fixed_offset(time_format, target_time)) } else { - Err("Invalid timezone offset.") + Err("Invalid timezone offset.".to_string()) } } /// Format a given time into the given string. This function should be referentially /// transparent, which makes it easy to test (unlike anything involving the actual time) -fn format_time(time_format: &str, local_time: DateTime) -> String { - local_time.format(time_format).to_string() +fn format_time(time_format: &str, local_time: Zoned) -> String { + local_time.strftime(time_format).to_string() } -fn format_time_fixed_offset(time_format: &str, utc_time: DateTime) -> String { - utc_time.format(time_format).to_string() +fn format_time_fixed_offset(time_format: &str, zoned_time: Zoned) -> String { + zoned_time.strftime(time_format).to_string() } /// Returns true if `time_now` is between `time_start` and `time_end`. /// If one of these values is not given, then it is ignored. /// It also handles cases where `time_start` and `time_end` have a midnight in between -fn is_inside_time_range( - time_now: NaiveTime, - time_start: Option, - time_end: Option, -) -> bool { +fn is_inside_time_range(time_now: Time, time_start: Option