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}