Skip to main content

i3status_rs/
widget.rs

1use crate::block_plan::{CONTRACT_BUG, OutputHandle, OutputKind};
2use crate::config::SharedConfig;
3use crate::errors::*;
4use crate::formatting::{Format, Fragment, Values};
5use crate::protocol::i3bar_block::I3BarBlock;
6use serde::Deserialize;
7use smart_default::SmartDefault;
8
9#[derive(Debug, Clone, Default)]
10pub struct Widget {
11    pub state: State,
12    source: Source,
13    values: Values,
14    /// The declared output variant this widget renders, when the block has
15    /// been migrated to a prepared [`crate::block_plan::BlockPlan`].
16    contract: Option<OutputHandle>,
17}
18
19impl Widget {
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /*
25     * Builders
26     */
27
28    pub fn with_text(mut self, text: String) -> Self {
29        self.set_text(text);
30        self
31    }
32
33    pub fn with_state(mut self, state: State) -> Self {
34        self.state = state;
35        self
36    }
37
38    pub fn with_format(mut self, format: Format) -> Self {
39        self.set_format(format);
40        self
41    }
42
43    /*
44     * Setters
45     */
46
47    pub fn set_text(&mut self, text: String) {
48        if text.is_empty() {
49            self.source = Source::None;
50        } else {
51            self.source = Source::Text(text);
52        }
53        // Same reasoning as `set_format`: raw text is not what the output
54        // handle promised, so the contract no longer describes this widget.
55        // Text output is declared and rendered via `new_text_widget`.
56        self.contract = None;
57    }
58
59    pub fn set_format(&mut self, format: Format) {
60        self.source = Source::Format(format);
61        // A raw format replacement invalidates whatever output contract the
62        // widget carried: the plan no longer describes what will render.
63        // Contracted widgets must switch outputs via `set_output` instead;
64        // publishing this widget now fails the contract check.
65        self.contract = None;
66    }
67
68    pub fn set_values(&mut self, new_values: Values) {
69        self.values = new_values;
70    }
71
72    /// A widget rendering `output`'s declared format. Source and contract are
73    /// set together and only here: there is deliberately no way to attach a
74    /// handle to a widget whose source did not come from it, which would let
75    /// an arbitrary format pass validation under a valid output's name.
76    pub(crate) fn from_output(output: &OutputHandle) -> Self {
77        let mut widget = Self::new();
78        widget.set_output(output);
79        widget
80    }
81
82    /// A widget rendering `text` as `output`, which must be a text output.
83    /// See [`Self::from_output`] for why this is a constructor.
84    pub(crate) fn text_from_output(output: &OutputHandle, text: String) -> Self {
85        let mut widget = Self::new();
86        widget.set_text(text);
87        widget.contract = Some(output.clone());
88        widget
89    }
90
91    /// Switch this widget to another declared output: installs that output's
92    /// effective format and contract together.
93    pub(crate) fn set_output(&mut self, output: &OutputHandle) {
94        self.set_format(output.format().clone());
95        self.contract = Some(output.clone());
96    }
97
98    #[cfg(test)]
99    pub(crate) fn contract(&self) -> Option<&OutputHandle> {
100        self.contract.as_ref()
101    }
102
103    /// Enforce the prepared contract at the point a block publishes this
104    /// widget: everything that renders — a format or plain text alike — must
105    /// come from an output handle of the matching kind, and every icon value
106    /// must be declared. A failure here is an i3status-rs bug (the bar shows
107    /// it as a block error), never a configuration problem.
108    pub(crate) fn check_contract(&self) -> Result<()> {
109        // A widget with no source renders nothing at all, so there is no
110        // output to hold it to.
111        if matches!(self.source, Source::None) {
112            return Ok(());
113        }
114        let Some(handle) = &self.contract else {
115            let kind = match self.source {
116                Source::Format(_) => "format",
117                _ => "text",
118            };
119            return Err(Error::new(format!(
120                "{CONTRACT_BUG}: a {kind} widget was published without a \
121                 prepared output handle"
122            )));
123        };
124        let declared = handle.output().kind();
125        let matches_source = match self.source {
126            Source::Format(_) => declared == OutputKind::Format,
127            Source::Text(_) => declared == OutputKind::Text,
128            Source::None => true,
129        };
130        if !matches_source {
131            return Err(Error::new(format!(
132                "{CONTRACT_BUG}: output '{}' is declared as {declared:?} but \
133                 the published widget renders {}",
134                handle.id(),
135                match self.source {
136                    Source::Format(_) => "a format",
137                    _ => "text",
138                }
139            )));
140        }
141        if let Some(violation) = self.contract_violations().into_iter().next() {
142            return Err(Error::new(violation));
143        }
144        Ok(())
145    }
146
147    /// Icon values not declared by this widget's output contract.
148    pub(crate) fn contract_violations(&self) -> Vec<String> {
149        match &self.contract {
150            Some(handle) => handle.icon_violations(&self.values),
151            None => Vec::new(),
152        }
153    }
154
155    pub fn intervals(&self) -> Vec<u64> {
156        match &self.source {
157            Source::Format(f) => f.intervals(),
158            _ => Vec::new(),
159        }
160    }
161
162    /// Construct `I3BarBlock` from this widget
163    pub fn get_data(&self, shared_config: &SharedConfig, id: usize) -> Result<Vec<I3BarBlock>> {
164        // Create a "template" block
165        let (key_bg, key_fg) = shared_config.theme.get_colors(self.state);
166        let (full, short) = self.source.render(shared_config, &self.values)?;
167        let mut template = I3BarBlock {
168            instance: format!("{id}:"),
169            background: key_bg,
170            color: key_fg,
171            ..I3BarBlock::default()
172        };
173
174        // Collect all the pieces into "parts"
175        let mut parts = Vec::new();
176
177        if full.is_empty() {
178            return Ok(parts);
179        }
180
181        // If short text is available, it's necessary to hide all full blocks. `swaybar`/`i3bar`
182        // will switch a block to "short mode" only if it's "short_text" is set to a non-empty
183        // string "<span/>" is a non-empty string and it doesn't display anything. It's kinda hacky,
184        // but it works.
185        if !short.is_empty() {
186            template.short_text = "<span/>".into();
187        }
188
189        parts.extend(full.into_iter().map(|w| {
190            let mut data = template.clone();
191            data.full_text = w.formatted_text();
192            if let Some(i) = &w.metadata.instance {
193                data.instance.push_str(i);
194            }
195            data
196        }));
197
198        template.full_text = "<span/>".into();
199        parts.extend(short.into_iter().map(|w| {
200            let mut data = template.clone();
201            data.short_text = w.formatted_text();
202            if let Some(i) = &w.metadata.instance {
203                data.instance.push_str(i);
204            }
205            data
206        }));
207
208        Ok(parts)
209    }
210}
211
212/// State of the widget. Affects the theming.
213#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, SmartDefault)]
214pub enum State {
215    #[default]
216    #[serde(alias = "idle")]
217    Idle,
218    #[serde(alias = "info")]
219    Info,
220    #[serde(alias = "good")]
221    Good,
222    #[serde(alias = "warning")]
223    Warning,
224    #[serde(alias = "critical")]
225    Critical,
226}
227
228/// The source of text for widget
229#[derive(Debug, Clone, SmartDefault)]
230enum Source {
231    /// Collapsed widget (only icon will be displayed)
232    #[default]
233    None,
234    /// Simple text
235    Text(String),
236    /// A format template
237    Format(Format),
238}
239
240impl Source {
241    fn render(
242        &self,
243        config: &SharedConfig,
244        values: &Values,
245    ) -> Result<(Vec<Fragment>, Vec<Fragment>)> {
246        match self {
247            Self::Text(text) => Ok((vec![text.clone().into()], vec![])),
248            Self::Format(format) => format.render(values, config),
249            Self::None => Ok((vec![], vec![])),
250        }
251    }
252}