duat_base/widgets/prompt_line.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 context::Handle,
22 data::Pass,
23 opts::PrintOpts,
24 text::Text,
25 ui::{PushSpecs, PushTarget, Side, Widget},
26};
27
28use crate::modes::PromptMode;
29
30/// A multi purpose text [`Widget`]
31///
32/// This [`Widget`] will be used by a [`Prompt`]-type [`Mode`], which
33/// in turn will make use of a [`PromptMode`]. These are "ways of
34/// interpreting the input". In Duat, there are 3 built-in
35/// [`PromptMode`]s:
36///
37/// - [`RunCommands`]: Will interpret the prompt as a Duat command to
38/// be executed.
39/// - [`IncSearch`]: Will read the prompt as a regex, and modify the
40/// active [`Buffer`] according to a given [`IncSearcher`].
41/// - [`PipeSelections`]: Will pass each selection to a shell command,
42/// replacing the selections with the `stdout`.
43///
44/// [`Prompt`]: crate::modes::Prompt
45/// [`Mode`]: duat_core::mode::Mode
46/// [`RunCommands`]: crate::modes::RunCommands
47/// [`IncSearch`]: crate::modes::IncSearch
48/// [`Buffer`]: duat_core::buffer::Buffer
49/// [`IncSearcher`]: crate::modes::IncSearcher
50/// [`PipeSelections`]: crate::modes::PipeSelections
51pub struct PromptLine {
52 text: Text,
53 prompts: HashMap<TypeId, Text>,
54 request_width: bool,
55}
56
57impl PromptLine {
58 /// Returns a [`PromptLineBuilder`], which can be used to push
59 /// `PromptLine`s around
60 pub fn builder() -> PromptLineBuilder {
61 PromptLineBuilder::default()
62 }
63
64 /// Returns the prompt for a [`PromptMode`] if there is any
65 pub fn prompt_of<M: PromptMode>(&self) -> Option<Text> {
66 self.prompts.get(&TypeId::of::<M>()).cloned()
67 }
68
69 /// Sets the prompt for the given [`PromptMode`]
70 pub fn set_prompt<M: PromptMode>(&mut self, text: Text) {
71 self.prompts.entry(TypeId::of::<M>()).or_insert(text);
72 }
73
74 /// Returns the prompt for a [`TypeId`], if there is any
75 pub fn prompt_of_id(&self, id: TypeId) -> Option<Text> {
76 self.prompts.get(&id).cloned()
77 }
78}
79
80impl Widget for PromptLine {
81 fn update(pa: &mut Pass, handle: &Handle<Self>) {
82 let (pl, area) = handle.write_with_area(pa);
83
84 if pl.request_width {
85 let width = area.width_of_text(pl.get_print_opts(), &pl.text).unwrap();
86 area.set_width(width + pl.get_print_opts().scrolloff.x as f32)
87 .unwrap();
88 }
89
90 if let Some(main) = pl.text.selections().get_main() {
91 area.scroll_around_points(
92 &pl.text,
93 main.caret().to_two_points_after(),
94 pl.get_print_opts(),
95 );
96 }
97 }
98
99 fn needs_update(&self, _: &Pass) -> bool {
100 false
101 }
102
103 fn text(&self) -> &Text {
104 &self.text
105 }
106
107 fn text_mut(&mut self) -> &mut Text {
108 &mut self.text
109 }
110
111 fn get_print_opts(&self) -> PrintOpts {
112 let mut opts = PrintOpts::default_for_input();
113 opts.force_scrolloff = true;
114 opts
115 }
116}
117
118/// A builder for a new [`PromptLine`]
119///
120/// Can be acquired with [`PromptLine::builder`].
121pub struct PromptLineBuilder {
122 prompts: Option<HashMap<TypeId, Text>>,
123 specs: PushSpecs,
124 request_width: bool,
125}
126
127impl Default for PromptLineBuilder {
128 fn default() -> Self {
129 Self {
130 prompts: None,
131 specs: PushSpecs {
132 side: Side::Below,
133 height: Some(1.0),
134 ..Default::default()
135 },
136 request_width: false,
137 }
138 }
139}
140
141impl PromptLineBuilder {
142 /// Pushes a [`PromptLine`] onto a [`PushTarget`]
143 ///
144 /// This could be a [`Window`] or a [`Handle`], for a [`Buffer`],
145 /// for example.
146 ///
147 /// [`Window`]: duat_core::ui::Window
148 /// [`Buffer`]: duat_core::buffer::Buffer
149 pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<PromptLine> {
150 let prompt_line = PromptLine {
151 text: Text::default(),
152 prompts: self.prompts.unwrap_or_default(),
153 request_width: self.request_width,
154 };
155
156 push_target.push_outer(pa, prompt_line, self.specs)
157 }
158
159 /// Changes the default [prompt] for a given [mode]
160 ///
161 /// [prompt]: Text
162 /// [mode]: PromptMode
163 pub fn set_prompt<M: PromptMode>(mut self, prompt: Text) -> Self {
164 self.prompts
165 .get_or_insert_default()
166 .insert(TypeId::of::<M>(), prompt);
167 self
168 }
169
170 /// Places the [`PromptLine`] above, as opposed to below
171 pub fn above(self) -> Self {
172 Self {
173 specs: PushSpecs { side: Side::Above, ..self.specs },
174 ..self
175 }
176 }
177
178 /// Places the [`PromptLine`] below, this is the default
179 pub fn below(self) -> Self {
180 Self {
181 specs: PushSpecs { side: Side::Below, ..self.specs },
182 ..self
183 }
184 }
185
186 /// Hides the [`PromptLine`] by default
187 pub fn hidden(self) -> Self {
188 Self {
189 specs: PushSpecs { hidden: true, ..self.specs },
190 ..self
191 }
192 }
193
194 /// Requests the width when printing to the screen
195 pub(crate) fn request_width(self) -> Self {
196 Self { request_width: true, ..self }
197 }
198}