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>;