duat_utils/widgets/status_line/
mod.rs

1//! A widget that shows general information, usually about a [`File`]
2//!
3//! The [`StatusLine`] is a very convenient widget when the user
4//! simply wants to show some informatioon. The information, when
5//! relevant, can automatically be tied to the active file, saving
6//! some keystrokes for the user's configuration.
7//!
8//! There is also the [`status!`] macro, which is an extremely
9//! convenient way to modify the text of the status line, letting you
10//! place form, in the same way that [`text!`] does, and
11//! automatically recognizing a ton of different types of functions,
12//! that can read from the file, from other places, from [data] types,
13//! etc.
14//!
15//! [data]: crate::data
16mod state;
17
18use duat_core::{context::DynFile, prelude::*, text::Builder, ui::Side};
19
20pub use self::{macros::status, state::State};
21use crate::state::{name_txt, main_txt, mode_txt, sels_txt};
22
23/// A widget to show information, usually about a [`File`]
24///
25/// This widget is updated whenever any of its parts needs to be
26/// updated, and it also automatically adjusts to where it was pushed.
27/// For example, if you push it to a file (via `hook::add::<File>`,
28/// for example), it's information will point to the [`File`] to which
29/// it was pushed. However, if you push it with [`WindowCreated`], it
30/// will always point to the currently active [`File`]:
31///
32/// ```rust
33/// # use duat_core::doc_duat as duat;
34/// # use duat_utils::widgets::{FooterWidgets, status};
35/// setup_duat!(setup);
36/// use duat::prelude::*;
37///
38/// fn setup() {
39///     hook::add::<File>(|_, (cfg, builder)| {
40///         builder.push(status!("{name_txt}").above());
41///         cfg
42///     });
43///
44///     hook::remove("WindowWidgets");
45///     hook::add::<WindowCreated>(|pa, builder| {
46///         builder.push(FooterWidgets::new(status!(
47///             "{} {sels_txt} {main_txt}",
48///             mode_txt(pa)
49///         )));
50///     });
51/// }
52/// ```
53///
54/// In the above example, each file would have a status line with the
55/// name of the file, and by pushing [`FooterWidgets`], you will push
56/// a [`StatusLine`], [`PromptLine`] and [`Notifications`] combo to
57/// each window. This [`StatusLine`] will point to the currently
58/// active [`File`], instead of a specific one.
59///
60/// You will usually want to create [`StatusLine`]s via the
61/// [`status!`] macro, since that is how you can customize it.
62/// Although, if you want the regular status line, you can just:
63///
64/// ```rust
65/// # use duat_core::doc_duat as duat;
66/// # use duat_utils::widgets::{LineNumbers, StatusLine};
67/// setup_duat!(setup);
68/// use duat::prelude::*;
69///
70/// fn setup() {
71///     hook::remove("FileWidgets");
72///     hook::add::<File>(|_, (cfg, builder)| {
73///         builder.push(LineNumbers::cfg());
74///         builder.push(StatusLine::cfg().above());
75///         cfg
76///     });
77/// }
78/// ```
79///
80/// [`File`]: duat_core::file::File
81/// [`WindowCreated`]: duat_core::hook::WindowCreated
82/// [`PromptLine`]: super::PromptLine
83/// [`Notifications`]: super::Notifications
84/// [`FooterWidgets`]: super::FooterWidgets
85pub struct StatusLine<U: Ui> {
86    file_handle: FileHandle<U>,
87    text_fn: TextFn<U>,
88    text: Text,
89    checker: Box<dyn Fn(&Pass) -> bool + Send>,
90}
91
92impl<U: Ui> Widget<U> for StatusLine<U> {
93    type Cfg = StatusLineCfg<U>;
94
95    fn update(pa: &mut Pass, handle: &Handle<Self, U>) {
96        if let FileHandle::Dynamic(dyn_file) = &mut handle.write(pa).file_handle {
97            dyn_file.swap_to_current();
98        }
99
100        let sl = handle.read(pa);
101
102        handle.write(pa).text = match &sl.file_handle {
103            FileHandle::Fixed(file) => (sl.text_fn)(pa, file),
104            FileHandle::Dynamic(dyn_file) => (sl.text_fn)(pa, dyn_file.handle()),
105        };
106    }
107
108    fn needs_update(&self, pa: &Pass) -> bool {
109        let file_changed = match &self.file_handle {
110            FileHandle::Fixed(handle) => handle.has_changed(),
111            FileHandle::Dynamic(dyn_file) => dyn_file.has_changed(pa),
112        };
113
114        file_changed || (self.checker)(pa)
115    }
116
117    fn cfg() -> Self::Cfg {
118        StatusLineCfg {
119            fns: None,
120            specs: PushSpecs::below().ver_len(1.0),
121        }
122    }
123
124    fn text(&self) -> &Text {
125        &self.text
126    }
127
128    fn text_mut(&mut self) -> &mut Text {
129        &mut self.text
130    }
131
132    fn once() -> Result<(), Text> {
133        form::set_weak("file", Form::yellow().italic());
134        form::set_weak("selections", Form::dark_blue());
135        form::set_weak("coord", Form::dark_yellow());
136        form::set_weak("separator", Form::cyan());
137        form::set_weak("mode", Form::green());
138        Ok(())
139    }
140}
141
142/// The [`WidgetCfg`] for a [`StatusLine`]
143pub struct StatusLineCfg<U: Ui> {
144    fns: Option<(BuilderFn<U>, CheckerFn)>,
145    specs: PushSpecs,
146}
147
148impl<U: Ui> StatusLineCfg<U> {
149    #[doc(hidden)]
150    pub fn new_with(fns: (BuilderFn<U>, CheckerFn), specs: PushSpecs) -> Self {
151        Self { fns: Some(fns), specs }
152    }
153
154    /// Replaces the previous formatting with a new one
155    pub fn fmt(self, new: Self) -> Self {
156        Self { specs: self.specs, ..new }
157    }
158
159    /// Puts the [`StatusLine`] above, as opposed to below
160    pub fn above(self) -> Self {
161        Self {
162            specs: PushSpecs::above().ver_len(1.0),
163            ..self
164        }
165    }
166
167    /// Puts the [`StatusLine`] below, this is the default
168    pub fn below(self) -> Self {
169        Self {
170            specs: PushSpecs::below().ver_len(1.0),
171            ..self
172        }
173    }
174
175    /// Puts the [`StatusLine`] on the right, instead of below
176    ///
177    /// use this if you want a single line [`StatusLine`],
178    /// [`PromptLine`]/[`Notifications`] combo.
179    ///
180    /// [`PromptLine`]: super::PromptLine
181    /// [`Notifications`]: super::Notifications
182    pub fn right_ratioed(self, den: u16, div: u16) -> Self {
183        Self {
184            specs: self.specs.to_right().hor_ratio(den, div),
185            ..self
186        }
187    }
188
189    /// The [`PushSpecs`] in use
190    pub fn specs(&self) -> PushSpecs {
191        self.specs
192    }
193}
194
195impl<U: Ui> WidgetCfg<U> for StatusLineCfg<U> {
196    type Widget = StatusLine<U>;
197
198    fn build(self, pa: &mut Pass, info: BuildInfo<U>) -> (Self::Widget, PushSpecs) {
199        let (builder_fn, checker_fn) = if let Some((builder, checker)) = self.fns {
200            (builder, checker)
201        } else {
202            let mode_txt = mode_txt(pa);
203            let cfg = match self.specs.side() {
204                Side::Above | Side::Below => {
205                    macros::status!("{mode_txt}{Spacer}{name_txt} {sels_txt} {main_txt}")
206                }
207                Side::Right => {
208                    macros::status!("{AlignRight}{name_txt} {mode_txt} {sels_txt} {main_txt}",)
209                }
210                Side::Left => unreachable!(),
211            };
212
213            cfg.fns.unwrap()
214        };
215
216        let widget = StatusLine {
217            file_handle: match info.file() {
218                Some(handle) => FileHandle::Fixed(handle),
219                None => FileHandle::Dynamic(context::dyn_file(pa).unwrap()),
220            },
221            text_fn: Box::new(move |pa, fh| {
222                let builder = Text::builder();
223                builder_fn(pa, builder, fh)
224            }),
225            text: Text::default(),
226            checker: Box::new(checker_fn),
227        };
228        (widget, self.specs)
229    }
230}
231
232impl<U: Ui> Default for StatusLineCfg<U> {
233    fn default() -> Self {
234        StatusLine::cfg()
235    }
236}
237
238mod macros {
239    /// The macro that creates a [`StatusLine`]
240    ///
241    /// This macro works like the [`txt!`] macro, in  that [`Form`]s
242    /// are pushed with `[{FormName}]`. However, [`txt!`]  is
243    /// evaluated immediately, while [`status!`] is evaluated when
244    /// updates occur.
245    ///
246    /// The macro will mostly read from the [`File`] widget and its
247    /// related structs. In order to do that, it will accept functions
248    /// as arguments. These functions take the following parameters:
249    ///
250    /// * The [`&File`] widget;
251    /// * A specific [`&impl Widget`], which is glued to the [`File`];
252    ///
253    /// Both of these can also have a second argument of type
254    /// [`&Area`]. This will include the [`Widget`]'s [`Area`] when
255    /// creating the status part. Additionally, you may include a
256    /// first argument of type [`&Pass`] (e.g. `fn(&Pass, &File)`,
257    /// `fn(&Pass, &Widget, &Area), etc.), giving you _non mutating_
258    /// access to global state.
259    ///
260    /// Here's some examples:
261    ///
262    /// ```rust
263    /// # use duat_core::doc_duat as duat;
264    /// # use duat_utils::widgets::status;
265    /// setup_duat!(setup);
266    /// use duat::prelude::*;
267    ///
268    /// fn name_but_funky(file: &File) -> String {
269    ///     file.name()
270    ///         .chars()
271    ///         .enumerate()
272    ///         .map(|(i, char)| {
273    ///             if i % 2 == 1 {
274    ///                 char.to_uppercase().to_string()
275    ///             } else {
276    ///                 char.to_lowercase().to_string()
277    ///             }
278    ///         })
279    ///         .collect()
280    /// }
281    ///
282    /// fn powerline_main_txt(file: &File, area: &Area) -> Text {
283    ///     let selections = file.selections();
284    ///     let cfg = file.print_cfg();
285    ///     let v_caret = selections
286    ///         .get_main()
287    ///         .unwrap()
288    ///         .v_caret(file.text(), area, cfg);
289    ///
290    ///     txt!(
291    ///         "[separator][coord]{}[separator][coord]{}[separator][coord]{}",
292    ///         v_caret.visual_col(),
293    ///         v_caret.line(),
294    ///         file.len_lines()
295    ///     )
296    ///     .build()
297    /// }
298    ///
299    /// fn setup() {
300    ///     hook::add::<WindowCreated>(|_, builder| {
301    ///         builder.push(status!("[file]{name_but_funky}[] {powerline_main_txt}"));
302    ///     });
303    /// }
304    /// ```
305    ///
306    /// Now, there are other types of arguments that you can also
307    /// pass. They update differently from the previous ones. The
308    /// previous arguments update when the [`File`] updates. The
309    /// following types of arguments update independently or not
310    /// at all:
311    ///
312    /// - A [`Text`] argument, which can be formatted in a similar way
313    ///   throught the [`txt!`] macro;
314    /// - Any [`impl Display`], such as numbers, strings, chars, etc.
315    ///   [`impl Debug`] types also work, when including the usual
316    ///   `":?"` and derived suffixes;
317    /// - [`RwData`] or [`DataMap`]s of the previous two types. These
318    ///   will update whenever the data inside is changed;
319    /// - An [`(Fn(&Pass) -> Text/Display/Debug, Fn(&Pass) -> bool)`]
320    ///   tuple. The first function returns what will be shown, while
321    ///   the second function checks for updates, which will call the
322    ///   first function again;
323    ///
324    /// Here's an examples:
325    ///
326    /// ```rust
327    /// # use duat_core::doc_duat as duat;
328    /// # use duat_utils::widgets::status;
329    /// setup_duat!(setup);
330    /// use std::sync::atomic::{AtomicUsize, Ordering};
331    ///
332    /// use duat::prelude::*;
333    ///
334    /// fn setup() {
335    ///     let changing_str = RwData::new("Initial text".to_string());
336    ///
337    ///     fn counter(update: bool) -> usize {
338    ///         static COUNT: AtomicUsize = AtomicUsize::new(0);
339    ///         if update {
340    ///             COUNT.fetch_add(1, Ordering::Relaxed) + 1
341    ///         } else {
342    ///             COUNT.load(Ordering::Relaxed)
343    ///         }
344    ///     }
345    ///
346    ///     hook::add::<WindowCreated>({
347    ///         let changing_str = changing_str.clone();
348    ///         move |_, builder| {
349    ///             let changing_str = changing_str.clone();
350    ///             let checker = changing_str.checker();
351    ///
352    ///             let text = txt!("Static text").build();
353    ///
354    ///             let counter = move |_: &File| counter(checker());
355    ///
356    ///             builder.push(status!("{changing_str} [counter]{counter}[] {text}",));
357    ///         }
358    ///     });
359    ///
360    ///     cmd::add!("set-text", |pa, new: &str| {
361    ///         *changing_str.write(pa) = new.to_string();
362    ///         Ok(None)
363    ///     })
364    /// }
365    /// ```
366    ///
367    /// In the above example, I added some dynamic [`Text`], through
368    /// the usage of an [`RwData<Text>`], I added some static
369    /// [`Text`], some [`Form`]s (`"counter"` and `"default"`) and
370    /// even a counter,.
371    ///
372    /// [`StatusLine`]: super::StatusLine
373    /// [`txt!`]: duat_core::text::txt
374    /// [`File`]: duat_core::file::File
375    /// [`&File`]: duat_core::file::File
376    /// [`&Selections`]: duat_core::mode::Selections
377    /// [`&impl Widget`]: duat_core::ui::Widget
378    /// [`impl Display`]: std::fmt::Display
379    /// [`impl Debug`]: std::fmt::Debug
380    /// [`Text`]: duat_core::text::Text
381    /// [`RwData`]: duat_core::data::RwData
382    /// [`DataMap`]: duat_core::data::DataMap
383    /// [`FnOnce(&Pass) -> RwData/DataMap`]: FnOnce
384    /// [`(Fn(&Pass) -> Text/Display/Debug, Fn(&Pass) -> bool)`]: Fn
385    /// [`RwData<Text>`]: duat_core::data::RwData
386    /// [`Form`]: duat_core::form::Form
387    /// [`&Area`]: duat_core::ui::Area
388    /// [`Area`]: duat_core::ui::Area
389    /// [`Widget`]: duat_core::ui::Widget
390    /// [`&Pass`]: duat_core::data::Pass
391    pub macro status($($parts:tt)*) {{
392        #[allow(unused_imports)]
393        use $crate::{
394            private_exports::{
395                duat_core::{context::Handle, data::Pass, file::File, ui::PushSpecs, text::Builder},
396                format_like, parse_form, parse_status_part, parse_str
397            },
398            widgets::StatusLineCfg,
399        };
400
401        let text_fn = |_: &Pass, _: &mut Builder, _: &Handle<File<_>, _>| {};
402        let checker = |_: &Pass| false;
403
404        let (text_fn, checker) = format_like!(
405            parse_str,
406            [('{', parse_status_part, false), ('[', parse_form, true)],
407            (text_fn, checker),
408            $($parts)*
409        );
410
411        StatusLineCfg::new_with(
412            (
413                Box::new(move |pa: &Pass, mut builder: Builder, handle: &Handle<File<_>, _>| {
414                    text_fn(pa, &mut builder, &handle);
415                    builder.build()
416                }),
417                Box::new(checker)
418            ),
419            PushSpecs::below().ver_len(1.0),
420        )
421    }}
422}
423
424type TextFn<U> = Box<dyn Fn(&Pass, &Handle<File<U>, U>) -> Text + Send>;
425type BuilderFn<U> = Box<dyn Fn(&Pass, Builder, &Handle<File<U>, U>) -> Text + Send>;
426type CheckerFn = Box<dyn Fn(&Pass) -> bool + Send>;
427
428enum FileHandle<U: Ui> {
429    Fixed(Handle<File<U>, U>),
430    Dynamic(DynFile<U>),
431}