Skip to main content

duat_base/widgets/
promptline.rs

1//! A multi-purpose widget
2//!
3//! This widget serves many purposes, of note is [running commands],
4//! but it can also be used for [incremental search],
5//! [piping selections], and even [user made functionalities].
6//!
7//! These functionalities are all controled by a [`Mode`] that has
8//! control over the [`Text`] in the [`PromptLine`]. By default, that
9//! is the [`Prompt`] mode, which can take in a [`PromptMode`] in
10//! order to determine how the [`Text`] will be interpreted.
11//!
12//! [running commands]: crate::mode::RunCommands
13//! [incremental search]: crate::mode::IncSearch
14//! [piping selections]: crate::mode::PipeSelections
15//! [user made functionalities]: PromptMode
16//! [`Mode`]: crate::mode::Mode
17//! [`Prompt`]: crate::modes::Prompt
18use std::{any::TypeId, collections::HashMap};
19
20use duat_core::{
21    Ns,
22    context::Handle,
23    data::Pass,
24    hook::{self, FocusedOn, KeySent, UnfocusedFrom},
25    opts::PrintOpts,
26    text::{Text, TextMut},
27    ui::{PushSpecs, PushTarget, Side, Widget},
28};
29
30use crate::modes::PromptMode;
31
32/// Add the [`PromptLine`] hooks.
33pub fn add_promptline_hooks() {
34    let ns = Ns::new();
35
36    hook::add::<FocusedOn<PromptLine>>(move |_, (_, promptline)| {
37        let promptline = promptline.clone();
38
39        hook::add::<KeySent>(move |pa, _| {
40            let (pl, area) = promptline.write_with_area(pa);
41
42            if pl.request_width {
43                let width = area.size_of_text(pl.print_opts(), &pl.text).unwrap().x;
44                area.set_width(width + pl.print_opts().scrolloff.x as f32)
45                    .unwrap();
46            }
47
48            if let Some(main) = pl.text.selections().get_main() {
49                area.scroll_around_points(
50                    &pl.text,
51                    main.caret().to_two_points_after(),
52                    pl.print_opts(),
53                );
54            }
55        })
56        .grouped(ns);
57    });
58
59    hook::add::<UnfocusedFrom<PromptLine>>(move |_, _| hook::remove(ns));
60}
61
62/// A multi purpose text [`Widget`]
63///
64/// This [`Widget`] will be used by a [`Prompt`]-type [`Mode`], which
65/// in turn will make use of a [`PromptMode`]. These are "ways of
66/// interpreting the input". In Duat, there are 3 built-in
67/// [`PromptMode`]s:
68///
69/// - [`RunCommands`]: Will interpret the prompt as a Duat command to
70///   be executed.
71/// - [`IncSearch`]: Will read the prompt as a regex, and modify the
72///   active [`Buffer`] according to a given [`IncSearcher`].
73/// - [`PipeSelections`]: Will pass each selection to a shell command,
74///   replacing the selections with the `stdout`.
75///
76/// [`Prompt`]: crate::modes::Prompt
77/// [`Mode`]: duat_core::mode::Mode
78/// [`RunCommands`]: crate::modes::RunCommands
79/// [`IncSearch`]: crate::modes::IncSearch
80/// [`Buffer`]: duat_core::buffer::Buffer
81/// [`IncSearcher`]: crate::modes::IncSearcher
82/// [`PipeSelections`]: crate::modes::PipeSelections
83pub struct PromptLine {
84    pub(crate) text: Text,
85    prompts: HashMap<TypeId, Text>,
86    request_width: bool,
87}
88
89impl PromptLine {
90    /// Returns a [`PromptLineBuilder`], which can be used to push
91    /// `PromptLine`s around
92    pub fn builder() -> PromptLineBuilder {
93        PromptLineBuilder::default()
94    }
95
96    /// Returns the prompt for a [`PromptMode`] if there is any
97    pub fn prompt_of<M: PromptMode>(&self) -> Option<Text> {
98        self.prompts.get(&TypeId::of::<M>()).cloned()
99    }
100
101    /// Sets the prompt for the given [`PromptMode`]
102    pub fn set_prompt<M: PromptMode>(&mut self, text: Text) {
103        self.prompts.entry(TypeId::of::<M>()).or_insert(text);
104    }
105
106    /// Returns the prompt for a [`TypeId`], if there is any
107    pub fn prompt_of_id(&self, id: TypeId) -> Option<Text> {
108        self.prompts.get(&id).cloned()
109    }
110}
111
112impl Widget for PromptLine {
113    fn text(&self) -> &Text {
114        &self.text
115    }
116
117    fn text_mut(&mut self) -> TextMut<'_> {
118        self.text.as_mut()
119    }
120
121    fn print_opts(&self) -> PrintOpts {
122        let mut opts = PrintOpts::default_for_input();
123        opts.force_scrolloff = true;
124        opts
125    }
126}
127
128/// A builder for a new [`PromptLine`]
129///
130/// Can be acquired with [`PromptLine::builder`].
131pub struct PromptLineBuilder {
132    prompts: Option<HashMap<TypeId, Text>>,
133    specs: PushSpecs,
134    request_width: bool,
135}
136
137impl Default for PromptLineBuilder {
138    fn default() -> Self {
139        Self {
140            prompts: None,
141            specs: PushSpecs {
142                side: Side::Below,
143                height: Some(1.0),
144                ..Default::default()
145            },
146            request_width: false,
147        }
148    }
149}
150
151impl PromptLineBuilder {
152    /// Pushes a [`PromptLine`] onto a [`PushTarget`]
153    ///
154    /// This could be a [`Window`] or a [`Handle`], for a [`Buffer`],
155    /// for example.
156    ///
157    /// [`Window`]: duat_core::ui::Window
158    /// [`Buffer`]: duat_core::buffer::Buffer
159    pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<PromptLine> {
160        let promptline = PromptLine {
161            text: Text::default(),
162            prompts: self.prompts.unwrap_or_default(),
163            request_width: self.request_width,
164        };
165
166        push_target.push_outer(pa, promptline, self.specs)
167    }
168
169    /// Changes the default [prompt] for a given [mode]
170    ///
171    /// [prompt]: Text
172    /// [mode]: PromptMode
173    pub fn set_prompt<M: PromptMode>(mut self, prompt: Text) -> Self {
174        self.prompts
175            .get_or_insert_default()
176            .insert(TypeId::of::<M>(), prompt);
177        self
178    }
179
180    /// Places the [`PromptLine`] above, as opposed to below
181    pub fn above(self) -> Self {
182        Self {
183            specs: PushSpecs { side: Side::Above, ..self.specs },
184            ..self
185        }
186    }
187
188    /// Places the [`PromptLine`] below, this is the default
189    pub fn below(self) -> Self {
190        Self {
191            specs: PushSpecs { side: Side::Below, ..self.specs },
192            ..self
193        }
194    }
195
196    /// Hides the [`PromptLine`] by default
197    pub fn hidden(self) -> Self {
198        Self {
199            specs: PushSpecs { hidden: true, ..self.specs },
200            ..self
201        }
202    }
203
204    /// Requests the width when printing to the screen
205    pub(crate) fn request_width(self) -> Self {
206        Self { request_width: true, ..self }
207    }
208}