1use std::marker::PhantomData;
12use std::ops::{Deref, DerefMut};
13
14use ratatui::{buffer::Buffer, layout::Rect, style::Style, text::Span, widgets::StatefulWidget};
15
16use modalkit::actions::{Action, CommandBarAction, PromptAction, Promptable};
17use modalkit::editing::{
18 application::ApplicationInfo,
19 completion::CompletionList,
20 context::{EditContext, Resolve},
21 history::ScrollbackState,
22 rope::EditRope,
23 store::Store,
24};
25use modalkit::errors::EditResult;
26use modalkit::prelude::*;
27
28use super::{
29 textbox::{TextBox, TextBoxState},
30 PromptActions,
31 WindowOps,
32};
33
34pub struct CommandBarState<I: ApplicationInfo> {
36 scrollback: ScrollbackState,
37 prompt: String,
38 action: Option<(Action<I>, EditContext)>,
39 cmdtype: CommandType,
40 tbox_cmd: TextBoxState<I>,
41 tbox_search: TextBoxState<I>,
42}
43
44impl<I> CommandBarState<I>
45where
46 I: ApplicationInfo,
47{
48 pub fn new(store: &mut Store<I>) -> Self {
50 let buffer_cmd = store.load_buffer(I::content_of_command(CommandType::Command));
51 let buffer_search = store.load_buffer(I::content_of_command(CommandType::Search));
52
53 CommandBarState {
54 scrollback: ScrollbackState::Pending,
55 prompt: String::new(),
56 action: None,
57 cmdtype: CommandType::Command,
58 tbox_cmd: TextBoxState::new(buffer_cmd),
59 tbox_search: TextBoxState::new(buffer_search),
60 }
61 }
62
63 pub fn get_completions(&self) -> Option<CompletionList> {
65 self.deref().get_completions()
66 }
67
68 pub fn set_type(&mut self, prompt: &str, ct: CommandType, act: &Action<I>, ctx: &EditContext) {
70 self.prompt = prompt.into();
71 self.action = Some((act.clone(), ctx.clone()));
72 self.cmdtype = ct;
73 }
74
75 pub fn reset(&mut self) -> EditRope {
77 self.scrollback = ScrollbackState::Pending;
78
79 self.deref_mut().reset()
80 }
81
82 pub fn reset_text(&mut self) -> String {
84 self.reset().to_string()
85 }
86}
87
88impl<I> Deref for CommandBarState<I>
89where
90 I: ApplicationInfo,
91{
92 type Target = TextBoxState<I>;
93
94 fn deref(&self) -> &Self::Target {
95 match self.cmdtype {
96 CommandType::Command => &self.tbox_cmd,
97 CommandType::Search => &self.tbox_search,
98 }
99 }
100}
101
102impl<I> DerefMut for CommandBarState<I>
103where
104 I: ApplicationInfo,
105{
106 fn deref_mut(&mut self) -> &mut Self::Target {
107 match self.cmdtype {
108 CommandType::Command => &mut self.tbox_cmd,
109 CommandType::Search => &mut self.tbox_search,
110 }
111 }
112}
113
114impl<I> PromptActions<EditContext, Store<I>, I> for CommandBarState<I>
115where
116 I: ApplicationInfo,
117{
118 fn submit(
119 &mut self,
120 ctx: &EditContext,
121 store: &mut Store<I>,
122 ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
123 let rope = self.reset().trim_end_matches(|c| c == '\n');
124 store.registers.set_last_command(self.cmdtype, rope);
125
126 let mut acts = vec![(CommandBarAction::Unfocus.into(), ctx.clone())];
127 acts.extend(self.action.take());
128
129 Ok(acts)
130 }
131
132 fn abort(
133 &mut self,
134 _empty: bool,
135 ctx: &EditContext,
136 store: &mut Store<I>,
137 ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
138 let act = Action::CommandBar(CommandBarAction::Unfocus);
140
141 let text = self.reset().trim();
142 store.registers.set_aborted_command(self.cmdtype, text);
143
144 Ok(vec![(act, ctx.clone())])
145 }
146
147 fn recall(
148 &mut self,
149 filter: &RecallFilter,
150 dir: &MoveDir1D,
151 count: &Count,
152 ctx: &EditContext,
153 store: &mut Store<I>,
154 ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
155 let count = ctx.resolve(count);
156 let rope = self.deref().get();
157
158 let hist = store.registers.get_command_history(self.cmdtype);
159 let text = hist.recall(&rope, &mut self.scrollback, filter, *dir, count);
160
161 if let Some(text) = text {
162 self.set_text(text);
163 }
164
165 Ok(vec![])
166 }
167}
168
169impl<I> Promptable<EditContext, Store<I>, I> for CommandBarState<I>
170where
171 I: ApplicationInfo,
172{
173 fn prompt(
174 &mut self,
175 act: &PromptAction,
176 ctx: &EditContext,
177 store: &mut Store<I>,
178 ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
179 match act {
180 PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
181 PromptAction::Recall(filter, dir, count) => self.recall(filter, dir, count, ctx, store),
182 PromptAction::Submit => self.submit(ctx, store),
183 }
184 }
185}
186
187pub struct CommandBar<'a, I: ApplicationInfo> {
189 focused: bool,
190 message: Option<Span<'a>>,
191 style_prompt: Option<Style>,
192 style_text: Style,
193
194 _pc: PhantomData<I>,
195}
196
197impl<'a, I> CommandBar<'a, I>
198where
199 I: ApplicationInfo,
200{
201 pub fn new() -> Self {
203 CommandBar {
204 focused: false,
205 message: None,
206 style_prompt: None,
207 style_text: Style::default(),
208 _pc: PhantomData,
209 }
210 }
211
212 pub fn focus(mut self, focused: bool) -> Self {
214 self.focused = focused;
215 self
216 }
217
218 pub fn prompt_style(mut self, style: Style) -> Self {
222 self.style_prompt = Some(style);
223 self
224 }
225
226 pub fn style(mut self, style: Style) -> Self {
228 self.style_text = style;
229 self
230 }
231
232 pub fn status(mut self, msg: Option<Span<'a>>) -> Self {
235 self.message = msg;
236 self
237 }
238}
239
240impl<I> StatefulWidget for CommandBar<'_, I>
241where
242 I: ApplicationInfo,
243{
244 type State = CommandBarState<I>;
245
246 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
247 if self.focused {
248 let prompt_style = self.style_prompt.unwrap_or(self.style_text);
249 let prompt = Span::styled(&state.prompt, prompt_style);
250 let tbox = TextBox::new().prompt(prompt).style(self.style_text).oneline();
251 let tbox_state = match state.cmdtype {
252 CommandType::Command => &mut state.tbox_cmd,
253 CommandType::Search => &mut state.tbox_search,
254 };
255
256 tbox.render(area, buf, tbox_state);
257 } else if let Some(span) = self.message {
258 buf.set_span(area.left(), area.top(), &span, area.width);
259 }
260 }
261}
262
263impl<I> Default for CommandBar<'_, I>
264where
265 I: ApplicationInfo,
266{
267 fn default() -> Self {
268 CommandBar::new()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use modalkit::editing::application::EmptyInfo;
276 use modalkit::editing::context::EditContextBuilder;
277
278 #[test]
279 fn test_set_type_submit() {
280 let mut store = Store::<EmptyInfo>::default();
281 let mut cmdbar = CommandBarState::new(&mut store);
282
283 let act = Action::Suspend;
285 let ctx = EditContextBuilder::default().search_regex_dir(MoveDir1D::Previous).build();
286 cmdbar.set_type(":", CommandType::Command, &act, &ctx);
287
288 let res = cmdbar.submit(&EditContext::default(), &mut store).unwrap();
289 assert_eq!(res.len(), 2);
290 assert_eq!(res[0].0, Action::from(CommandBarAction::Unfocus));
291 assert_eq!(res[0].1, EditContext::default());
292 assert_eq!(res[1].0, act);
293 assert_eq!(res[1].1, ctx);
294
295 let act1 = Action::Suspend;
297 let ctx1 = EditContextBuilder::default().search_regex_dir(MoveDir1D::Previous).build();
298 cmdbar.set_type(":", CommandType::Command, &act1, &ctx1);
299 let act2 = Action::KeywordLookup(KeywordTarget::Selection);
300 let ctx2 = EditContextBuilder::default().search_regex_dir(MoveDir1D::Next).build();
301 cmdbar.set_type(":", CommandType::Command, &act2, &ctx2);
302
303 let res = cmdbar.submit(&EditContext::default(), &mut store).unwrap();
304 assert_eq!(res.len(), 2);
305 assert_eq!(res[0].0, Action::from(CommandBarAction::Unfocus));
306 assert_eq!(res[0].1, EditContext::default());
307 assert_eq!(res[1].0, act2);
308 assert_eq!(res[1].1, ctx2);
309 }
310}