Skip to main content

duat_base/modes/
pager.rs

1//! A simple mode for scrolling through widgets
2//!
3//! This mode takes in a `W` argument, meaning it can act on any
4//! [`Widget`]. In Duat, it is normally used with the [`LogBook`], in
5//! order to allow scrolling through the logs.
6//!
7//! It is also capable of searching through the [`Text`], via a
8//! companion [`PagerSearch`] mode.
9use std::{
10    marker::PhantomData,
11    sync::{LazyLock, Mutex},
12};
13
14use duat_core::{
15    Ns,
16    buffer::Buffer,
17    context::{self, Handle},
18    data::Pass,
19    form, hook,
20    mode::{self, KeyEvent, Mode, alt, event, shift},
21    text::{RegexHaystack, Text, txt},
22    ui::{PrintInfo, RwArea, Widget},
23};
24
25use crate::{
26    hooks::{SearchPerformed, SearchUpdated},
27    modes::{Prompt, PromptMode, RunCommands},
28    widgets::LogBook,
29};
30
31static SEARCH: Mutex<String> = Mutex::new(String::new());
32static PAGER_NS: LazyLock<Ns> = LazyLock::new(Ns::new);
33
34/// A simple mode, meant for scrolling and searching through [`Text`]
35pub struct Pager<W: Widget = LogBook>(PhantomData<W>);
36
37impl<W: Widget> Pager<W> {
38    /// Returns a new [`Pager`]
39    pub fn new() -> Self {
40        Self(PhantomData)
41    }
42}
43
44impl<W: Widget> Mode for Pager<W> {
45    type Widget = W;
46
47    fn bindings() -> mode::Bindings {
48        use duat_core::mode::KeyCode::*;
49
50        if mode::alt_is_reverse() {
51            mode::bindings!(match _ {
52                event!(Char('j') | Down) => txt!("Scroll down"),
53                event!(Char('k') | Up) => txt!("Scroll up"),
54                event!('J') | shift!(Down) => txt!("Scroll to the bottom"),
55                event!('K') | shift!(Up) => txt!("Scroll to the top"),
56                event!('/') => txt!("[mode]Search[] ahead"),
57                alt!('/') => txt!("[mode]Search[] behind"),
58                event!('n') | alt!('n') => txt!("Go to [a]next[],[a]previous[] search match"),
59                event!(Esc) => txt!("[mode]Leave[] pager mode"),
60                event!(':') => txt!("[a]Run commands[] in prompt line"),
61            })
62        } else {
63            mode::bindings!(match _ {
64                event!(Char('j') | Down) => txt!("Scroll down"),
65                event!(Char('k') | Up) => txt!("Scroll up"),
66                event!('J') | shift!(Down) => txt!("Scroll to the bottom"),
67                event!('K') | shift!(Up) => txt!("Scroll to the top"),
68                event!('/') => txt!("[mode]Search[] ahead"),
69                event!('?') => txt!("[mode]Search[] behind"),
70                event!('n' | 'N') => txt!("Go to [a]next[],[a]previous[] search match"),
71                event!(Esc) => txt!("[mode]Leave[] pager mode"),
72                event!(':') => txt!("[a]Run commands[] in prompt line"),
73            })
74        }
75    }
76
77    fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget>) {
78        use duat_core::mode::KeyCode::*;
79
80        match (key, duat_core::mode::alt_is_reverse()) {
81            (event!(Char('j') | Down), _) => handle.scroll_ver(pa, 1),
82            (event!('J') | shift!(Down), _) => handle.scroll_ver(pa, i32::MAX),
83            (event!(Char('k') | Up), _) => handle.scroll_ver(pa, -1),
84            (event!('K') | shift!(Up), _) => handle.scroll_ver(pa, i32::MIN),
85            (event!('/'), _) => mode::set(pa, PagerSearch::new(pa, &handle, true)),
86            (alt!('/'), true) | (event!('?'), false) => {
87                mode::set(pa, PagerSearch::new(pa, &handle, false));
88            }
89            (event!('n'), _) => {
90                let se = SEARCH.lock().unwrap();
91
92                let point = handle.start_points(pa).real;
93
94                let text = handle.read(pa).text();
95                let Some(r) = text.search(&*se).range(point..).next() else {
96                    context::error!("[a]{se}[] was not found");
97                    return;
98                };
99
100                let point = handle.text(pa).point_at_byte(r.start);
101                handle.scroll_to_points(pa, point.to_two_points_after());
102            }
103            (alt!('n'), true) | (event!('N'), false) => {
104                let se = SEARCH.lock().unwrap();
105
106                let point = handle.start_points(pa).real;
107
108                let text = handle.read(pa).text();
109                let Some(r) = text.search(&*se).range(..point).next() else {
110                    context::error!("[a]{se}[] was not found");
111                    return;
112                };
113
114                let point = handle.text(pa).point_at_byte(r.start);
115                handle.scroll_to_points(pa, point.to_two_points_after());
116            }
117            (event!(Esc), _) => mode::reset::<Buffer>(pa),
118            (event!(':'), _) => mode::set(pa, RunCommands::new()),
119            _ => {}
120        }
121    }
122}
123
124impl<W: Widget> Clone for Pager<W> {
125    fn clone(&self) -> Self {
126        Self(PhantomData)
127    }
128}
129
130impl<W: Widget> Default for Pager<W> {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136/// The searcher [`PromptMode`] for a [`Pager`]ed [`Widget`]
137pub struct PagerSearch<W: Widget> {
138    is_fwd: bool,
139    prev: String,
140    orig: PrintInfo,
141    handle: Handle<W>,
142}
143
144impl<W: Widget> PagerSearch<W> {
145    #[allow(clippy::new_ret_no_self)]
146    fn new(pa: &Pass, handle: &Handle<W>, is_fwd: bool) -> Prompt {
147        Prompt::new(Self {
148            is_fwd,
149            prev: String::new(),
150            orig: handle.area().get_print_info(pa),
151            handle: handle.clone(),
152        })
153    }
154}
155
156impl<W: Widget> PromptMode for PagerSearch<W> {
157    type ExitWidget = W;
158
159    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
160        let ns = *PAGER_NS;
161        text.remove_tags(ns, ..);
162
163        if text == self.prev.as_str() {
164            return text;
165        } else {
166            let prev = std::mem::replace(&mut self.prev, text.to_string());
167            hook::trigger(pa, SearchUpdated((prev, self.prev.clone())));
168        }
169
170        let (widget, area) = self.handle.write_with_area(pa);
171
172        let mut parts = widget.text_mut().parts();
173
174        match parts.strs.try_search(text.to_string()) {
175            Ok(matches) => {
176                area.set_print_info(self.orig.clone());
177                parts.tags.remove(*PAGER_NS, ..);
178
179                let ast = regex_syntax::ast::parse::Parser::new()
180                    .parse(&text.to_string())
181                    .unwrap();
182
183                crate::tag_from_ast(*PAGER_NS, &mut text, &ast);
184
185                let id = form::id_of!("pager.search");
186
187                for range in matches {
188                    parts.tags.insert(*PAGER_NS, range, id.to_tag(0));
189                }
190            }
191            Err(err) => {
192                let regex_syntax::Error::Parse(err) = *err else {
193                    unreachable!("As far as I can tell, regex_syntax has goofed up");
194                };
195
196                let span = err.span();
197                let id = form::id_of!("regex.error");
198
199                text.insert_tag(*PAGER_NS, span.start.offset..span.end.offset, id.to_tag(0));
200            }
201        }
202
203        text
204    }
205
206    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
207        let point = self.handle.start_points(pa).real;
208
209        match self.handle.text(pa).try_search(text.to_string()) {
210            Ok(matches) => {
211                if self.is_fwd {
212                    let Some(range) = matches.clone().range(point..).next() else {
213                        context::error!("[a]{}[] was not found", text.to_string());
214                        return;
215                    };
216
217                    let start = self.handle.text(pa).point_at_byte(range.start);
218                    self.handle
219                        .scroll_to_points(pa, start.to_two_points_after());
220                } else {
221                    let Some(range) = matches.range(..point).next_back() else {
222                        context::error!("[a]{}[] was not found", text.to_string());
223                        return;
224                    };
225
226                    let start = self.handle.text(pa).point_at_byte(range.start);
227                    self.handle
228                        .scroll_to_points(pa, start.to_two_points_after());
229                }
230
231                *SEARCH.lock().unwrap() = text.to_string();
232                hook::trigger(pa, SearchPerformed(text.to_string()));
233            }
234            Err(err) => {
235                let regex_syntax::Error::Parse(err) = *err else {
236                    unreachable!("As far as I can tell, regex_syntax has goofed up");
237                };
238
239                let range = err.span().start.offset..err.span().end.offset;
240                let err = txt!(
241                    "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
242                    range,
243                    &text[range],
244                    err.kind()
245                );
246
247                context::error!("{err}")
248            }
249        }
250    }
251
252    fn prompt(&self) -> Text {
253        txt!("[prompt]pager search")
254    }
255
256    fn return_handle(&self) -> Option<Handle<dyn Widget>> {
257        Some(self.handle.clone().to_dyn())
258    }
259}
260
261impl<W: Widget> Clone for PagerSearch<W> {
262    fn clone(&self) -> Self {
263        Self {
264            is_fwd: self.is_fwd,
265            prev: self.prev.clone(),
266            orig: self.orig.clone(),
267            handle: self.handle.clone(),
268        }
269    }
270}