duat_base/widgets/status_line/
mod.rs

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