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}