Skip to main content

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 std::sync::{Arc, LazyLock, mpsc};
19
20use duat_core::{
21    context::{self, DynBuffer, Handle},
22    data::Pass,
23    hook::{self, BufferClosed, BufferUpdated},
24    text::{Builder, Spacer, Text, TextMut},
25    ui::{PushSpecs, PushTarget, Side, Widget},
26};
27
28pub use self::state::State;
29use crate::state::{main_txt, mode_txt, name_txt, sels_txt};
30
31mod state;
32#[macro_use]
33mod macros;
34#[doc(inline)]
35pub use crate::__status__ as status;
36
37/// A widget to show information, usually about a [`Buffer`]
38///
39/// This widget is updated whenever any of its parts needs to be
40/// updated, and it also automatically adjusts to where it was pushed.
41/// For example, if you push it to a buffer (via
42/// `hook::add::<BufferOpened>`, for example), it's information will
43/// point to the [`Buffer`] to which it was pushed. However, if you
44/// push it with [`WindowOpened`], it will always point to the
45/// currently active [`Buffer`]:
46///
47/// ```rust
48/// # duat_core::doc_duat!(duat);
49/// # use duat_base::widgets::status;
50/// setup_duat!(setup);
51/// use duat::prelude::*;
52///
53/// fn setup() {
54///     opts::set(|opts| opts.one_line_footer = true);
55///     opts::fmt_status(|pa| status!("{Spacer}{} {sels_txt} {main_txt}", mode_txt()));
56///
57///     hook::add::<BufferOpened>(|pa, handle| {
58///         status!("{Spacer}{name_txt}{Spacer}")
59///             .above()
60///             .push_on(pa, handle);
61///     });
62/// }
63/// ```
64///
65/// In the code above, I'm modifying the "global" `StatusLine` through
66/// [`opts::set_status`] (this can be done with [hooks] as well, but
67/// this method is added for convenience's sake). This is in
68/// conjunction with [`opts::one_line_footer`], which will place
69/// the [`PromptLine`] and `StatusLine` on the same line.
70///
71/// After that, I'm _also_ pushing a new `StatusLine` above every
72/// opened [`Buffer`], showing that `Buffer`]'s name, centered.
73///
74/// You will usually want to create `StatusLine`s via the
75/// [`status!`] macro, since that is how you can customize it.
76/// Although, if you want the regular status line, you can call
77/// [`StatusLine::builder`]:
78///
79/// ```rust
80/// # duat_core::doc_duat!(duat);
81/// # use duat_base::widgets::StatusLine;
82/// setup_duat!(setup);
83/// use duat::prelude::*;
84///
85/// fn setup() {
86///     hook::add::<BufferOpened>(|pa, handle| {
87///         StatusLine::builder().above().push_on(pa, handle);
88///     });
89/// }
90/// ```
91///
92/// [`Buffer`]: duat_core::buffer::Buffer
93/// [`WindowOpened`]: duat_core::hook::WindowOpened
94/// [`PromptLine`]: super::PromptLine
95/// [`Notifications`]: super::Notifications
96/// [`FooterWidgets`]: super::FooterWidgets
97/// [`opts::set_status`]: https://docs.rs/duat/latest/duat/opts/fn.set_status.html
98/// [`opts::one_line_footer`]: https://docs.rs/duat/latest/duat/opts/fn.one_line_footer.html
99/// [hooks]: duat_core::hook
100pub struct StatusLine {
101    buffer_handle: BufferHandle,
102    text_fn: TextFn,
103    text: Text,
104    checker: CheckerFn,
105}
106
107impl StatusLine {
108    fn new(builder: StatusLineFmt, buffer_handle: BufferHandle) -> Self {
109        let (builder_fn, checker) = if let Some((builder, checker)) = builder.fns {
110            (builder, checker)
111        } else {
112            let mode_txt = mode_txt();
113
114            let opts = match builder.specs.side {
115                Side::Above | Side::Below => {
116                    status!("{mode_txt}{Spacer}{name_txt} {sels_txt} {main_txt}")
117                }
118                Side::Right => {
119                    status!("{Spacer}{name_txt} {mode_txt} {sels_txt} {main_txt}",)
120                }
121                Side::Left => unreachable!(),
122            };
123
124            opts.fns.unwrap()
125        };
126
127        Self {
128            buffer_handle,
129            text_fn: Box::new(move |pa, fh| {
130                let builder = Text::builder();
131                builder_fn(pa, builder, fh)
132            }),
133            text: Text::new(),
134            checker,
135        }
136    }
137
138    /// Replaces this `StatusLine` with a new one
139    pub fn fmt(&mut self, new: StatusLineFmt) {
140        let handle = self.buffer_handle.clone();
141        *self = StatusLine::new(new, handle);
142    }
143
144    /// Returns a [`StatusLineFmt`], which can be used to push
145    /// around `StatusLine`s
146    ///
147    /// The same can be done more conveniently with the [`status!`]
148    /// macro, which is imported by default in the configuration
149    /// crate.
150    pub fn builder() -> StatusLineFmt {
151        StatusLineFmt { fns: None, ..Default::default() }
152    }
153
154    fn update(pa: &mut Pass, handle: &Handle<Self>) {
155        if let BufferHandle::Dynamic(dyn_file) = &mut handle.write(pa).buffer_handle {
156            dyn_file.swap_to_current();
157        }
158
159        let sl = handle.read(pa);
160
161        handle.write(pa).text = match &sl.buffer_handle {
162            BufferHandle::Fixed(buffer) => (sl.text_fn)(pa, buffer),
163            BufferHandle::Dynamic(dyn_file) => (sl.text_fn)(pa, dyn_file.handle()),
164        };
165
166        // Do this in case the Buffer is never read during Text construction
167        match &handle.read(pa).buffer_handle {
168            BufferHandle::Fixed(handle) => handle.declare_as_read(),
169            BufferHandle::Dynamic(dyn_file) => dyn_file.declare_as_read(),
170        }
171    }
172}
173
174impl Widget for StatusLine {
175    fn text(&self) -> &Text {
176        &self.text
177    }
178
179    fn text_mut(&mut self) -> TextMut<'_> {
180        self.text.as_mut()
181    }
182}
183
184/// A builder for [`StatusLine`]s
185///
186/// This struct is created by the [`status!`] macro, and its purpose
187/// is mainly to allow formatting of the `StatusLine`.
188///
189/// There is also the [`StatusLineFmt::above`] method, which places
190/// the `StatusLine` above, rather than below.
191pub struct StatusLineFmt {
192    fns: Option<(BuilderFn, CheckerFn)>,
193    specs: PushSpecs,
194}
195
196impl StatusLineFmt {
197    /// Push the [`StatusLine`]
198    ///
199    /// If the handle's [`Widget`] is a [`Buffer`], then this
200    /// `StatusLine` will refer to it when printing information about
201    /// `Buffer`s. Otherwise, the `StatusLine` will print information
202    /// about the currently active `Buffer`.
203    ///
204    /// [`Buffer`]: duat_core::buffer::Buffer
205    pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<StatusLine> {
206        static SENDER: LazyLock<mpsc::Sender<StatusLineEvent>> = LazyLock::new(|| {
207            hook::add::<BufferUpdated>(|pa, buffer| {
208                let handles = buffer.get_related::<StatusLine>(pa);
209                for (statusline, _) in &handles {
210                    StatusLine::update(pa, statusline);
211                }
212
213                let handles: Vec<Handle<StatusLine>> = context::current_window(pa)
214                    .handles(pa)
215                    .filter_map(Handle::try_downcast)
216                    .filter(|statusline| !handles.iter().any(|(other, _)| other == statusline))
217                    .collect();
218
219                for statusline in handles {
220                    let sl = statusline.read(pa);
221                    let buffer_changed = match &sl.buffer_handle {
222                        BufferHandle::Fixed(handle) => handle.has_changed(pa),
223                        BufferHandle::Dynamic(dyn_buf) => dyn_buf.has_changed(pa),
224                    };
225
226                    if buffer_changed || (sl.checker)() {
227                        StatusLine::update(pa, &statusline);
228                    }
229                }
230            });
231
232            hook::add::<BufferClosed>(|pa, buffer| {
233                for (statusline, _) in buffer.get_related::<StatusLine>(pa) {
234                    SENDER.send(StatusLineEvent::Closed(statusline)).unwrap();
235                }
236            });
237
238            let (tx, rx) = mpsc::channel();
239
240            std::thread::spawn(move || {
241                let mut entries = Vec::new();
242                loop {
243                    match rx.recv_timeout(std::time::Duration::from_millis(50)) {
244                        Ok(StatusLineEvent::Opened(statusline, checker)) => {
245                            entries.push((statusline, checker));
246                        }
247                        Ok(StatusLineEvent::Closed(rm_statusline)) => {
248                            entries.retain(|(statusline, ..)| *statusline != rm_statusline);
249                        }
250                        Err(_) => {}
251                    }
252
253                    for (statusline, checker) in &entries {
254                        if checker() {
255                            let statusline = statusline.clone();
256                            context::queue(move |pa| StatusLine::update(pa, &statusline))
257                        }
258                    }
259                }
260            });
261
262            tx
263        });
264
265        let specs = self.specs;
266        let statusline = StatusLine::new(self, match push_target.try_downcast() {
267            Some(handle) => BufferHandle::Fixed(handle),
268            None => BufferHandle::Dynamic(context::dynamic_buffer(pa)),
269        });
270
271        let checker = statusline.checker.clone();
272        let statusline = push_target.push_outer(pa, statusline, specs);
273
274        SENDER
275            .send(StatusLineEvent::Opened(statusline.clone(), checker))
276            .unwrap();
277
278        statusline
279    }
280
281    /// Returns a new `StatusLineFmt`, meant to be called only be the
282    /// [`status!`] macro
283    #[doc(hidden)]
284    pub fn new_with(fns: (BuilderFn, CheckerFn)) -> Self {
285        Self { fns: Some(fns), ..Default::default() }
286    }
287
288    /// Puts the [`StatusLine`] above, as opposed to below
289    pub fn above(self) -> Self {
290        Self {
291            specs: PushSpecs { side: Side::Above, ..self.specs },
292            ..self
293        }
294    }
295
296    /// Puts the [`StatusLine`] below, this is the default
297    pub fn below(self) -> Self {
298        Self {
299            specs: PushSpecs { side: Side::Below, ..self.specs },
300            ..self
301        }
302    }
303
304    /// Puts the [`StatusLine`] on the right
305    pub(crate) fn right(self) -> Self {
306        Self {
307            specs: PushSpecs { side: Side::Right, ..self.specs },
308            ..self
309        }
310    }
311}
312
313impl Default for StatusLineFmt {
314    fn default() -> Self {
315        Self {
316            fns: None,
317            specs: PushSpecs {
318                side: Side::Below,
319                height: Some(1.0),
320                ..Default::default()
321            },
322        }
323    }
324}
325
326#[derive(Clone)]
327enum BufferHandle {
328    Fixed(Handle),
329    Dynamic(DynBuffer),
330}
331
332enum StatusLineEvent {
333    Opened(Handle<StatusLine>, CheckerFn),
334    Closed(Handle<StatusLine>),
335}
336
337type TextFn = Box<dyn Fn(&Pass, &Handle) -> Text + Send>;
338type BuilderFn = Box<dyn Fn(&Pass, Builder, &Handle) -> Text + Send>;
339type CheckerFn = Arc<dyn Fn() -> bool + Send + Sync>;