Skip to main content

i3status_rs/
formatting.rs

1//! # Formatting system
2//! Many blocks have a `format` configuration option, which allows to heavily customize the block's
3//! appearance. In short, each block with `format` option provides a set of values, which are
4//! displayed according to `format`. `format`'s value is just a text with embedded variables.
5//! Similarly to PHP and shell, variable name must start with a `$`:
6//! `this is a variable: -> $var <-`.
7//!
8//! Also, format strings can embed icons. For example, `^icon_ping` in `" ^icon_ping $ping "` gets
9//! substituted with a "ping" icon from your icon set. For a complete list of icons, see
10//! [this](https://github.com/greshake/i3status-rust/blob/master/doc/themes.md#available-icon-overrides).
11//!
12//! # Types
13//!
14//! The allowed types of variables are:
15//!
16//! Type                      | Default formatter
17//! --------------------------|------------------
18//! Text                      | `str`
19//! Number                    | `eng`
20//! Datetime                  | `datetime`
21//! Duration                  | `duration`
22//! [Flag](#how-to-use-flags) | N/A
23//!
24//! # Formatters
25//!
26//! A formatter is something that converts a value into a text. Because there are many ways to do
27//! this, a number of formatters is available. Formatter can be specified using the syntax similar
28//! to method calls in many programming languages: `<variable>.<formatter>(<args>)`. For example:
29//! `$title.str(min_w:10, max_w:20)`.
30//!
31//! Note: for arguments that accept a boolean value, just specifying the argument will be treated as `arg:true`.
32//!
33//! ## `str` - Format text
34//!
35//! Argument               | Description                                       |Default value
36//! -----------------------|---------------------------------------------------|-------------
37//! `min_width` or `min_w` | if text is shorter it will be padded using spaces | `0`
38//! `max_width` or `max_w` | if text is longer it will be truncated            | Infinity
39//! `width` or `w`         | Text will be exactly this length by padding or truncating as needed | N/A
40//! `rot_interval`         | if text is longer than `max_width` it will be rotated every `rot_interval` seconds, if set | None
41//! `rot_separator`        | if text is longer than `max_width` it will be rotated with this seporator | <code>\"\|\"</code>
42//!
43//! Note: width just changes the values of both min_width and max_width to be the same. Use width
44//! if you want the values to be the same, or the other two otherwise. Don't mix width with
45//! min_width or max_width.
46//!
47//! ## `eng` - Format numbers using engineering notation
48//!
49//! Argument        | Description                                                                                      |Default value
50//! ----------------|--------------------------------------------------------------------------------------------------|-------------
51//! `width` or `w`  | the resulting text will be at least `width` characters long                                      | `2`
52//! `unit` or `u`   | some values have a [unit](unit::Unit), and it is possible to convert them by setting this option | N/A
53//! `hide_unit`     | hide the unit symbol                                                                             | `false`
54//! `unit_space`    | have a whitespace before unit symbol                                                             | `false`
55//! `prefix` or `p` | specify this argument if you want to set the minimal [SI prefix](prefix::Prefix)                 | N/A
56//! `hide_prefix`   | hide the prefix symbol                                                                           | `false`
57//! `prefix_space`  | have a whitespace before prefix symbol                                                           | `false`
58//! `force_prefix`  | force the prefix value instead of setting a "minimal prefix"                                     | `false`
59//! `pad_with`      | the character that is used to pad the number to be `width` long                                  | ` ` (a space)
60//! `range`         | a range of allowed values, in the format `<start>..<end>`, inclusive. Both start and end are optional. Can be used to, for example, hide the block when the value is not in a given range. | `..`
61//! `show`          | show this value. Can be used with `range` for conditional formatting                             | `true`
62//!
63//! ## `bar` - Display numbers as progress bars
64//!
65//! Argument               | Description                                                                     |Default value
66//! -----------------------|---------------------------------------------------------------------------------|-------------------------
67//! `width` or `w`         | the width of the bar (in characters)                                            | `5` (`1` for `vertical`)
68//! `max_value`            | which value is treated as "full". For example, for battery level `100` is full. | `100`
69//! `vertical` or `v`      | whether to render the bar vertically or not                                     | `false`
70//!
71//! ## `tally` - Display numbers as tally marks
72//!
73//! Argument       | Description                                                                                                                                                                     |Default value
74//! ---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------
75//! `style` or `s` | One of [`chinese_counting_rods`/`ccr`](https://en.wikipedia.org/wiki/Counting_rods), [`chinese_tally`/`ct`, `western_tally`/`wt`, `western_tally_ungrouped`/`wtu`](https://en.wikipedia.org/wiki/Tally_marks) | western_tally
76//!
77//! ## `pango-str` - Just display the text without pango markup escaping
78//!
79//! No arguments.
80//!
81//! ## `datetime` - Display datetime
82//! Argument               | Description                                                                                                                                                                           |Default value
83//! -----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------
84//! `format` or `f`        | [chrono docs](https://docs.rs/chrono/0.3.0/chrono/format/strftime/index.html#specifiers) for all options, or `short`, `medium`, `long`, or `full` for icu datetimes                   | `'%a %d/%m %R'`
85//! `locale` or `l`        | Locale to apply when formatting the time                                                                                                                                              | System locale
86//! `precision` or `p`     | Precision to apply when formatting an icu datetime (`hours`/`hour`/`h`, `minutes`/`minute`/`m`, or `seconds`/`second`/`s`), if none is specified then only the date will be displayed | None
87//!
88//!
89//! ## `duration`/`dur` - Format durations
90//!
91//! Argument                     | Description                                                                                      |Default value
92//! -----------------|--------------------------------------------------------------------------------------------------|------------------------------------------------------
93//! `hms`            | Should the format be hours:minutes:seconds.milliseconds                                          | `false`
94//! `max_unit`       | The largest unit to display the duration with (see below for the list of all possible units)     | hms ? `h` : `y`
95//! `min_unit`       | The smallest unit to display the duration with (see below for the list of all possible units)    | `s`
96//! `units`          | The number of units to display                                                                   | min(# of units between `max_unit` and `min_unit``, 2)
97//! `round_up`       | Round up to the nearest minimum displayed unit                                                   | `true`
98//! `unit_space`     | Should there be a space between the value and unit symbol (not allowed when `hms:true`)          | `false`
99//! `pad_with`       | The character that is used to pad the numbers                                                    | hms ? `0` : ` ` (a space)
100//! `leading_zeroes` | If fewer than `units` are non-zero should leading numbers that have a value of zero be shown     | `true`
101//!
102//! Unit | Description
103//! -----|------------
104//! y    | years
105//! w    | weeks
106//! d    | days
107//! h    | hours
108//! m    | minutes
109//! s    | seconds
110//! ms   | milliseconds
111//!
112//! # Handling missing placeholders and incorrect types
113//!
114//! Some blocks allow missing placeholders, for example [bluetooth](crate::blocks::bluetooth)'s
115//! "percentage" may be absent if the device is not supported. To handle such cases it is possible
116//! to queue multiple formats together by using `|` symbol: `<something that can fail>|<otherwise
117//! try this>|<or this>`.
118//!
119//! In addition, formats can be recursive. To set a format inside of another format, place it
120//! inside of `{}`. For example, in `Percentage: {$percentage|N/A}` the text "Percentage: " will be
121//! always displayed, followed by the actual percentage or "N/A" in case percentage is not
122//! available. This example does exactly the same thing as `Percentage: $percentage|Percentage: N/A`
123//!
124//! # How to use flags
125//!
126//! Some blocks provide flags, which can be used to change the format based on some criteria. For
127//! example, [taskwarrior](crate::blocks::taskwarrior) defines `done` if the count is zero. In
128//! general, flags are used in this way:
129//!
130//! ```text
131//! $a{a is set}|$b$c{b and c are set}|${b|c}{b or c is set}|neither flag is set
132//! ```
133
134pub mod config;
135pub mod formatter;
136pub mod parse;
137pub mod prefix;
138pub mod scheduling;
139pub mod template;
140pub mod unit;
141pub mod value;
142
143use std::borrow::Cow;
144use std::collections::HashMap;
145use std::str::FromStr;
146
147use crate::config::SharedConfig;
148use crate::errors::*;
149use template::FormatTemplate;
150use value::Value;
151
152pub type Values = HashMap<Cow<'static, str>, Value>;
153
154#[derive(Debug, thiserror::Error)]
155pub enum FormatError {
156    #[error("Placeholder '{0}' not found")]
157    PlaceholderNotFound(String),
158    #[error("{} cannot be formatted with '{}' formatter", .ty, .fmt)]
159    IncompatibleFormatter { ty: &'static str, fmt: &'static str },
160    #[error("Number {0} is out of range")]
161    NumberOutOfRange(f64),
162    #[error(transparent)]
163    Other(#[from] Error),
164}
165
166#[derive(Debug)]
167pub struct MultiFormat {
168    // The length is always at least one, so we don't need to worry about
169    // dividing by zero when doing the modulo math to calculate the
170    // previous and next formats.
171    formats: Vec<Format>,
172    index: usize,
173}
174
175impl MultiFormat {
176    pub fn new(formats: Vec<Format>) -> Self {
177        Self { formats, index: 0 }
178    }
179
180    pub fn iter(&self) -> std::slice::Iter<'_, Format> {
181        self.formats.iter()
182    }
183
184    pub fn get_format(&self) -> Format {
185        self.formats[self.index].clone()
186    }
187
188    pub fn next_format(&mut self) {
189        self.index = (self.index + 1) % self.formats.len();
190    }
191
192    pub fn prev_format(&mut self) {
193        self.index = (self.index + (self.formats.len() - 1)) % self.formats.len();
194    }
195}
196
197#[derive(Debug, Clone)]
198pub struct Format {
199    full: FormatTemplate,
200    short: FormatTemplate,
201    intervals: Vec<u64>,
202}
203
204impl Format {
205    pub fn new(full: FormatTemplate, short: FormatTemplate) -> Self {
206        let mut intervals = Vec::new();
207        full.init_intervals(&mut intervals);
208        short.init_intervals(&mut intervals);
209
210        Self {
211            full,
212            short,
213            intervals,
214        }
215    }
216
217    pub fn contains_key(&self, key: &str) -> bool {
218        self.full.contains_key(key) || self.short.contains_key(key)
219    }
220
221    pub fn intervals(&self) -> Vec<u64> {
222        self.intervals.clone()
223    }
224
225    pub fn render(
226        &self,
227        values: &Values,
228        config: &SharedConfig,
229    ) -> Result<(Vec<Fragment>, Vec<Fragment>)> {
230        let full = self
231            .full
232            .render(values, config)
233            .error("Failed to render full text")?;
234        let short = self
235            .short
236            .render(values, config)
237            .error("Failed to render short text")?;
238        Ok((full, short))
239    }
240}
241
242impl FromStr for Format {
243    type Err = Error;
244
245    fn from_str(s: &str) -> Result<Self> {
246        Ok(Self::new(s.parse()?, FormatTemplate::default()))
247    }
248}
249
250#[derive(Debug, Default, Clone)]
251pub struct Fragment {
252    pub text: String,
253    pub metadata: Metadata,
254}
255
256impl From<String> for Fragment {
257    fn from(text: String) -> Self {
258        Self {
259            text,
260            metadata: Default::default(),
261        }
262    }
263}
264
265impl Fragment {
266    pub fn formatted_text(&self) -> String {
267        match (self.metadata.italic, self.metadata.underline) {
268            (true, true) => format!("<i><u>{}</u></i>", self.text),
269            (false, true) => format!("<u>{}</u>", self.text),
270            (true, false) => format!("<i>{}</i>", self.text),
271            (false, false) => self.text.clone(),
272        }
273    }
274}
275
276#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
277pub struct Metadata {
278    pub instance: Option<&'static str>,
279    pub underline: bool,
280    pub italic: bool,
281}
282
283impl Metadata {
284    pub fn is_default(&self) -> bool {
285        *self == Default::default()
286    }
287}