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 std::{cell::RefCell, rc::Rc};
19
20use duat_core::{prelude::*, text::Builder, ui::Side};
21
22pub use self::{macros::status, state::State};
23use crate::state::{file_fmt, main_fmt, mode_fmt, mode_name, sels_fmt};
24
25/// A widget to show information, usually about a [`File`]
26///
27/// This widget is updated whenever any of its parts needs to be
28/// updated, and it also automatically adjusts to where it was pushed.
29/// For example, if you push it with [`OnFileOpen`], it's information
30/// will point to the [`File`] to which it was pushed. However, if you
31/// push it with [`OnWindowOpen`], it will always point to the
32/// currently active [`File`]:
33///
34/// ```rust
35/// use duat_core::{
36/// hook::{OnFileOpen, OnWindowOpen},
37/// prelude::*,
38/// };
39/// use duat_utils::{state::*, widgets::*};
40///
41/// fn setup_generic_over_ui<U: Ui>() {
42/// hook::remove("FileWidgets");
43/// hook::add::<OnFileOpen<U>, U>(|pa, builder| {
44/// builder.push(pa, LineNumbers::cfg());
45/// builder.push(pa, status!("{file_fmt}").above());
46/// });
47///
48/// hook::remove("WindowWidgets");
49/// hook::add::<OnWindowOpen<U>, U>(|pa, builder| {
50/// let footer = FooterWidgets::new(status!("{mode_fmt} {sels_fmt} {main_fmt}"));
51/// builder.push(pa, footer);
52/// });
53/// }
54/// ```
55///
56/// In the above example, each file would have a status line with the
57/// name of the file, and by pushing [`FooterWidgets`], you will push
58/// a [`StatusLine`], [`PromptLine`] and [`Notifications`] combo to
59/// each window. This [`StatusLine`] will point to the currently
60/// active [`File`], instead of a specific one.
61///
62/// You will usually want to create [`StatusLine`]s via the
63/// [`status!`] macro, since that is how you can customize it.
64/// Although, if you want the regular status line, you can just:
65///
66/// ```rust
67/// use duat_core::{hook::OnFileOpen, prelude::*};
68/// use duat_utils::widgets::*;
69///
70/// fn setup_generic_over_ui<U: Ui>() {
71/// hook::remove("FileWidgets");
72/// hook::add::<OnFileOpen<U>, U>(|pa, builder| {
73/// builder.push(pa, LineNumbers::cfg());
74/// builder.push(pa, StatusLine::cfg().above());
75/// });
76/// }
77/// ```
78///
79/// [`File`]: duat_core::file::File
80/// [`OnFileOpen`]: duat_core::hook::OnFileOpen
81/// [`OnWindowOpen`]: duat_core::hook::OnWindowOpen
82/// [`PromptLine`]: super::PromptLine
83/// [`Notifications`]: super::Notifications
84/// [`FooterWidgets`]: super::FooterWidgets
85pub struct StatusLine<U: Ui> {
86 handle: FileHandle<U>,
87 text_fn: TextFn<U>,
88 text: Text,
89 checker: Box<dyn Fn() -> bool>,
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 let text = handle.read(pa, |wid, _| wid.text_fn.borrow_mut()(pa, &wid.handle));
97 handle.widget().replace_text(pa, text);
98 }
99
100 fn needs_update(&self) -> bool {
101 self.handle.has_changed() || (self.checker)()
102 }
103
104 fn cfg() -> Self::Cfg {
105 macros::status!("{file_fmt} {mode_fmt} {sels_fmt} {}", main_fmt)
106 }
107
108 fn text(&self) -> &Text {
109 &self.text
110 }
111
112 fn text_mut(&mut self) -> &mut Text {
113 &mut self.text
114 }
115
116 fn once() -> Result<(), Text> {
117 form::set_weak("file", Form::yellow().italic());
118 form::set_weak("selections", Form::dark_blue());
119 form::set_weak("coord", Form::dark_yellow());
120 form::set_weak("separator", Form::cyan());
121 form::set_weak("mode", Form::green());
122 Ok(())
123 }
124}
125
126/// The [`WidgetCfg`] for a [`StatusLine`]
127pub struct StatusLineCfg<U: Ui> {
128 builder: Option<BuilderFn<U>>,
129 checker: Option<Box<dyn Fn() -> bool>>,
130 specs: PushSpecs,
131}
132
133impl<U: Ui> StatusLineCfg<U> {
134 #[doc(hidden)]
135 pub fn new_with(
136 (builder, checker): (BuilderFn<U>, Box<dyn Fn() -> bool>),
137 specs: PushSpecs,
138 ) -> Self {
139 Self {
140 builder: Some(builder),
141 checker: Some(checker),
142 specs,
143 }
144 }
145
146 /// Replaces the previous formatting with a new one
147 pub fn replace(self, new: Self) -> Self {
148 Self { specs: self.specs, ..new }
149 }
150
151 /// Puts the [`StatusLine`] above, as opposed to below
152 pub fn above(self) -> Self {
153 Self {
154 specs: PushSpecs::above().with_ver_len(1.0),
155 ..self
156 }
157 }
158
159 /// Puts the [`StatusLine`] below, this is the default
160 pub fn below(self) -> Self {
161 Self {
162 specs: PushSpecs::below().with_ver_len(1.0),
163 ..self
164 }
165 }
166
167 /// Puts the [`StatusLine`] on the right, instead of below
168 ///
169 /// use this if you want a single line [`StatusLine`],
170 /// [`PromptLine`]/[`Notifications`] combo.
171 ///
172 /// [`PromptLine`]: super::PromptLine
173 /// [`Notifications`]: super::Notifications
174 pub fn right_ratioed(self, den: u16, div: u16) -> Self {
175 Self {
176 specs: self.specs.to_right().with_hor_ratio(den, div),
177 ..self
178 }
179 }
180
181 /// The [`PushSpecs`] in use
182 pub fn specs(&self) -> PushSpecs {
183 self.specs
184 }
185}
186
187impl<U: Ui> WidgetCfg<U> for StatusLineCfg<U> {
188 type Widget = StatusLine<U>;
189
190 fn build(self, pa: &mut Pass, handle: Option<FileHandle<U>>) -> (Self::Widget, PushSpecs) {
191 let handle = match handle {
192 Some(handle) => handle,
193 None => context::dyn_file(pa).unwrap(),
194 };
195
196 let checker = match self.checker {
197 Some(checker) => checker,
198 // mode checker because mode_name is used in the default
199 None => Box::new(crate::state::mode_name().checker()),
200 };
201
202 let text_fn: TextFn<U> = match self.builder {
203 Some(mut text_fn) => Rc::new(RefCell::new(
204 for<'a, 'b> move |pa: &'a Pass, handle: &'b FileHandle<U>| -> Text {
205 text_fn(pa, Text::builder(), handle)
206 },
207 )),
208 None => {
209 let cfg = match self.specs.side() {
210 Side::Above | Side::Below => {
211 let mode_upper_fmt = mode_name().map(|mode| {
212 let mode = match mode.split_once('<') {
213 Some((mode, _)) => mode,
214 None => mode,
215 };
216 txt!("[mode]{}", mode.to_uppercase()).build()
217 });
218 macros::status!("{mode_upper_fmt}{Spacer}{file_fmt} {sels_fmt} {main_fmt}")
219 }
220 Side::Right => {
221 macros::status!("{AlignRight}{file_fmt} {mode_fmt} {sels_fmt} {main_fmt}")
222 }
223 Side::Left => unreachable!(),
224 };
225
226 let mut text_fn = cfg.builder.unwrap();
227 Rc::new(RefCell::new(
228 for<'a, 'b> move |pa: &'a Pass, handle: &'b FileHandle<U>| -> Text {
229 text_fn(pa, Text::builder(), handle)
230 },
231 ))
232 }
233 };
234
235 let widget = StatusLine {
236 handle,
237 text_fn,
238 text: Text::default(),
239 checker: Box::new(checker),
240 };
241 (widget, self.specs)
242 }
243}
244
245impl<U: Ui> Default for StatusLineCfg<U> {
246 fn default() -> Self {
247 StatusLine::cfg()
248 }
249}
250
251mod macros {
252 /// The macro that creates a [`StatusLine`]
253 ///
254 /// This macro works like the [`txt!`] macro, in that [`Form`]s
255 /// are pushed with `[{FormName}]`. However, [`txt!`] is
256 /// evaluated immediately, while [`status!`] is evaluated when
257 /// updates occur.
258 ///
259 /// The macro will mostly read from the [`File`] widget and its
260 /// related structs. In order to do that, it will accept functions
261 /// as arguments. These functions take the following
262 /// parameters:
263 ///
264 /// * The [`&File`] widget;
265 /// * The [`&Selections`] of the [`File`]
266 /// * A specific [`&impl Widget`], which is glued to the [`File`];
267 ///
268 /// Here's some examples:
269 ///
270 /// ```rust
271 /// use duat_core::{hook::OnWindowOpen, prelude::*};
272 /// use duat_utils::widgets::status;
273 ///
274 /// fn name_but_funky<U: Ui>(file: &File<U>) -> String {
275 /// let mut name = String::new();
276 ///
277 /// for byte in unsafe { name.as_bytes_mut().iter_mut().step_by(2) } {
278 /// *byte = byte.to_ascii_uppercase();
279 /// }
280 ///
281 /// name
282 /// }
283 ///
284 /// fn powerline_main_fmt<U: Ui>(file: &File<U>, area: &U::Area) -> Text {
285 /// let selections = file.selections();
286 /// let cfg = file.print_cfg();
287 /// let v_caret = selections
288 /// .get_main()
289 /// .unwrap()
290 /// .v_caret(file.text(), area, cfg);
291 ///
292 /// txt!(
293 /// "[separator][coord]{}[separator][coord]{}[separator][coord]{}",
294 /// v_caret.visual_col(),
295 /// v_caret.line(),
296 /// file.len_lines()
297 /// )
298 /// .build()
299 /// }
300 ///
301 /// fn setup_generic_over_ui<U: Ui>() {
302 /// hook::add::<OnWindowOpen<U>, U>(|pa, builder| {
303 /// builder.push(pa, status!("[file]{name_but_funky}[] {powerline_main_fmt}"));
304 /// });
305 /// }
306 /// ```
307 ///
308 /// Now, there are other types of arguments that you can also
309 /// pass. They update differently from the previous ones. The
310 /// previous arguments update when the [`File`] updates. The
311 /// following types of arguments update independently or not
312 /// at all:
313 ///
314 /// - A [`Text`] argument can include [`Form`]s and buttons;
315 /// - Any [`impl Display`], such as numbers, strings, chars, etc;
316 /// - [`RwData`] or [`DataMap`]s of the previous two types. These
317 /// will update whenever the data inside is changed;
318 /// - An [`(FnMut() -> Text | impl Display, FnMut() -> bool)`]
319 /// tuple. The first function returns what will be shown, while
320 /// the second function tells it to update;
321 ///
322 /// Here's an examples:
323 ///
324 /// ```rust
325 /// use std::sync::atomic::{AtomicUsize, Ordering};
326 ///
327 /// use duat_core::{data::RwData, hook::OnWindowOpen, prelude::*};
328 /// use duat_utils::widgets::status;
329 ///
330 /// # fn test<U: Ui>() {
331 /// let changing_text = RwData::new(txt!("Prev text").build());
332 ///
333 /// fn counter(pa: &Pass) -> usize {
334 /// static COUNT: AtomicUsize = AtomicUsize::new(0);
335 /// COUNT.fetch_add(1, Ordering::Relaxed)
336 /// }
337 ///
338 /// hook::add::<OnWindowOpen<U>, U>({
339 /// let changing_text = changing_text.clone();
340 /// move |pa, builder| {
341 /// let changing_text = changing_text.clone();
342 /// let checker = changing_text.checker();
343 ///
344 /// let text = txt!("Static text").build();
345 ///
346 /// builder.push(
347 /// pa,
348 /// status!("{changing_text} [counter]{}[] {text}", (counter, checker)),
349 /// );
350 /// }
351 /// });
352 /// # }
353 /// ```
354 ///
355 /// In the above example, I added some dynamic [`Text`], through
356 /// the usage of an [`RwData<Text>`], I added some static
357 /// [`Text`], some [`Form`]s (`"counter"` and `"default"`) and
358 /// even a counter, which will update whenever `changing_text`
359 /// is altered.
360 ///
361 /// [`StatusLine`]: super::StatusLine
362 /// [`txt!`]: duat_core::text::txt
363 /// [`File`]: duat_core::file::File
364 /// [`&File`]: duat_core::file::File
365 /// [`&Selections`]: duat_core::mode::Selections
366 /// [`&impl Widget`]: duat_core::ui::Widget
367 /// [`impl Display`]: std::fmt::Display
368 /// [`Text`]: duat_core::text::Text
369 /// [`RwData`]: duat_core::data::RwData
370 /// [`DataMap`]: duat_core::data::DataMap
371 /// [`FnMut() -> Arg`]: FnMut
372 /// [`(FnMut() -> Text | impl Display, FnMut() -> bool)`]: FnMut
373 /// [`RwData<Text>`]: duat_core::data::RwData
374 /// [`Form`]: duat_core::form::Form
375 pub macro status($($parts:tt)*) {{
376 #[allow(unused_imports)]
377 use $crate::{
378 private_exports::{
379 duat_core::{context::FileHandle, data::Pass, text::Builder, ui::PushSpecs},
380 format_like, parse_form, parse_status_part, parse_str
381 },
382 widgets::StatusLineCfg,
383 };
384
385 let text_fn= |_: &Pass, _: &mut Builder, _: &FileHandle<_>| {};
386 let checker = || false;
387
388 let (mut text_fn, checker) = format_like!(
389 parse_str,
390 [('{', parse_status_part, false), ('[', parse_form, true)],
391 (text_fn, checker),
392 $($parts)*
393 );
394
395 StatusLineCfg::new_with(
396 {
397 (
398 Box::new(move |pa: &Pass, mut builder: Builder, handle: &FileHandle<_>| {
399 text_fn(pa, &mut builder, &handle);
400 builder.build()
401 }),
402 Box::new(checker),
403 )
404 },
405 PushSpecs::below().with_ver_len(1.0),
406 )
407 }}
408}
409
410type TextFn<U> = Rc<RefCell<dyn FnMut(&Pass, &FileHandle<U>) -> Text>>;
411type BuilderFn<U> = Box<dyn FnMut(&Pass, Builder, &FileHandle<U>) -> Text>;