From f9a45140459c9f155a2be46c2c51d3c186bb3175 Mon Sep 17 00:00:00 2001 From: John Letey <30328854+johnletey@users.noreply.github.com> Date: Tue, 10 Sep 2019 18:54:40 +0100 Subject: [PATCH] feat: Implement the prompt module for time (#138) Add a module which displays the current time in a format requested by the user. Disabled by default. --- Cargo.lock | 1 + Cargo.toml | 1 + docs/config/README.md | 33 +++++++++++++ src/module.rs | 1 + src/modules/mod.rs | 2 + src/modules/time.rs | 105 ++++++++++++++++++++++++++++++++++++++++ src/print.rs | 2 +- tests/testsuite/main.rs | 1 + tests/testsuite/time.rs | 62 ++++++++++++++++++++++++ 9 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/modules/time.rs create mode 100644 tests/testsuite/time.rs diff --git a/Cargo.lock b/Cargo.lock index a8dfa9ed5..923dddebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,7 @@ version = "0.16.0" dependencies = [ "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "battery 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "gethostname 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 2e79d5eda..9e934e55d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ path-slash = "0.1.1" unicode-segmentation = "1.3.0" gethostname = "0.2.0" once_cell = "1.1.0" +chrono = "0.4" [dev-dependencies] tempfile = "3.1.0" diff --git a/docs/config/README.md b/docs/config/README.md index ac60f3758..3a5c47bcf 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -96,6 +96,7 @@ default_prompt_order = [ "cmd_duration", "line_break", "jobs", + "time", "battery", "character", ] @@ -603,6 +604,38 @@ The module will be shown if any of the following conditions are met: symbol = "⚙️ " ``` +## 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. + +::: tip +This module is disabled by default. +To enable it, set `disabled` to `false` in your configuration file. +::: + +### Options + +| Variable | Default | Description | +| ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | +| `12hr` | `false` | Enables 12 hour formatting | +| `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 | +| `disabled` | `true` | Disables the `time` module. | + +If `12hr` is `true`, then `format` defaults to `"%r"`. Otherwise, it defaults to `"%T"`. +Manually setting `format` will override the `12hr` setting. + +### Example + +```toml +# ~/.config/starship.toml + +[time] +disabled = false +format = "🕙[ %T ]" +``` + ## Username The `username` module shows active user's username. diff --git a/src/module.rs b/src/module.rs index 80f7bc8d4..04233578a 100644 --- a/src/module.rs +++ b/src/module.rs @@ -24,6 +24,7 @@ pub const ALL_MODULES: &[&str] = &[ "python", "ruby", "rust", + "time", "username", ]; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 83a7d1c40..34f1cc12b 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -15,6 +15,7 @@ mod package; mod python; mod ruby; mod rust; +mod time; mod username; #[cfg(feature = "battery")] @@ -44,6 +45,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "jobs" => jobs::module(context), "nix_shell" => nix_shell::module(context), "hostname" => hostname::module(context), + "time" => time::module(context), _ => { eprintln!("Error: Unknown module {}. Use starship module --list to list out all supported modules.", module); diff --git a/src/modules/time.rs b/src/modules/time.rs new file mode 100644 index 000000000..d76a5c1e1 --- /dev/null +++ b/src/modules/time.rs @@ -0,0 +1,105 @@ +use ansi_term::Color; +use chrono::offset::TimeZone; +use chrono::{DateTime, Local}; + +use super::{Context, Module}; + +/// Outputs the current time +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("time"); + + // Remove when logic for disabled by default exists + if module.config_value_bool("disabled").unwrap_or(true) { + return None; + } + + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Yellow.bold()); + module.set_style(module_style); + + // Load module settings + let is_12hr = module.config_value_bool("12hr").unwrap_or(false); + + let default_format = if is_12hr { "%r" } else { "%T" }; + let time_format = module + .config_value_str("format") + .unwrap_or(default_format) + .to_owned(); + + log::trace!( + "Timer module is enabled with format string: {}", + time_format + ); + + let local: DateTime = Local::now(); + let formatted_time_string = format_time(&time_format, local); + module.new_segment("time", &formatted_time_string); + module.get_prefix().set_value("at "); + + Some(module) +} + +/// 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, localtime: DateTime) -> String { + localtime.format(time_format).to_string() +} + +/* Because we cannot do integration tests on the time module, these unit +tests become extra important */ +#[cfg(test)] +mod tests { + use super::*; + + const FMT_12: &str = "%r"; + const FMT_24: &str = "%T"; + + #[test] + fn test_midnight_12hr() { + let time = Local.ymd(2014, 7, 8).and_hms(0, 0, 0); + let formatted = format_time(FMT_12, time); + assert_eq!(formatted, "12:00:00 AM"); + } + + #[test] + fn test_midnight_24hr() { + let time = Local.ymd(2014, 7, 8).and_hms(0, 0, 0); + let formatted = format_time(FMT_24, time); + assert_eq!(formatted, "00:00:00"); + } + + #[test] + fn test_noon_12hr() { + let time = Local.ymd(2014, 7, 8).and_hms(12, 0, 0); + let formatted = format_time(FMT_12, time); + assert_eq!(formatted, "12:00:00 PM"); + } + + #[test] + fn test_noon_24hr() { + let time = Local.ymd(2014, 7, 8).and_hms(12, 0, 0); + let formatted = format_time(FMT_24, time); + assert_eq!(formatted, "12:00:00"); + } + + #[test] + fn test_arbtime_12hr() { + let time = Local.ymd(2014, 7, 8).and_hms(15, 36, 47); + let formatted = format_time(FMT_12, time); + assert_eq!(formatted, "03:36:47 PM"); + } + + #[test] + fn test_arbtime_24hr() { + let time = Local.ymd(2014, 7, 8).and_hms(15, 36, 47); + let formatted = format_time(FMT_24, time); + assert_eq!(formatted, "15:36:47"); + } + + fn test_format_with_paren() { + let time = Local.ymd(2014, 7, 8).and_hms(15, 36, 47); + let formatted = format_time("[%T]", time); + assert_eq!(formatted, "[15:36:47]"); + } +} diff --git a/src/print.rs b/src/print.rs index 2b730e259..f2301ca25 100644 --- a/src/print.rs +++ b/src/print.rs @@ -29,7 +29,7 @@ const DEFAULT_PROMPT_ORDER: &[&str] = &[ "line_break", "jobs", #[cfg(feature = "battery")] - "battery", + "time", "character", ]; diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index bf64f3099..f92a702cf 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -15,4 +15,5 @@ mod nix_shell; mod nodejs; mod python; mod ruby; +mod time; mod username; diff --git a/tests/testsuite/time.rs b/tests/testsuite/time.rs new file mode 100644 index 000000000..c73545b26 --- /dev/null +++ b/tests/testsuite/time.rs @@ -0,0 +1,62 @@ +use ansi_term::Color; +use std::fs; +use std::io; +use std::path::Path; +use tempfile::TempDir; + +use crate::common::{self, TestCommand}; + +/* Note: tests in this crate cannot rely on the actual time displayed by +the module, since that is dependent on the time inside the test environment, +which we cannot control. + +However, we *can* test certain things here, such as the fact that the module +should not display when disabled, should display *something* when enabled, +and should have the correct prefixes and suffixes in a given config */ + +#[test] +fn config_enabled() -> io::Result<()> { + let output = common::render_module("time") + .use_config(toml::toml! { + [time] + disabled = false + }) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + // We can't test what it actually is...but we can assert it's not blank + assert!(!actual.is_empty()); + Ok(()) +} + +#[test] +fn config_blank() -> io::Result<()> { + let output = common::render_module("time").output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = ""; + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +fn config_check_prefix_and_suffix() -> io::Result<()> { + let output = common::render_module("time") + .use_config(toml::toml! { + [time] + disabled = false + format = "[%T]" + }) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + // This is the prefix with "at ", the color code, then the prefix char [ + let col_prefix = format!("at {}{}[", '\u{1b}', "[1;33m"); + + // This is the suffix with suffix char ']', then color codes, then a space + let col_suffix = format!("]{}{} ", '\u{1b}', "[0m"); + + assert!(actual.starts_with(&col_prefix)); + assert!(actual.ends_with(&col_suffix)); + Ok(()) +}