Skip to main content

below_view/
command_palette.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::cell::RefCell;
16use std::collections::HashMap;
17use std::collections::VecDeque;
18use std::rc::Rc;
19
20use cursive::event::Event;
21use cursive::event::EventResult;
22use cursive::event::Key;
23use cursive::theme::ColorStyle;
24use cursive::vec::Vec2;
25use cursive::views::EditView;
26use cursive::views::NamedView;
27use cursive::Cursive;
28use cursive::Printer;
29use cursive::View;
30
31use crate::controllers::Controllers;
32use crate::stats_view::StatsView;
33use crate::stats_view::ViewBridge;
34
35const MAX_CMD_HISTORY: usize = 10;
36
37/// Command palette will have different mode:
38/// Info is used to show info like full cgroup path.
39/// Alert is used to show error messages.
40/// Command is used to turn command palette in Command mode.
41// TODO: command mode for command palette.
42#[derive(PartialEq)]
43enum CPMode {
44    Info,
45    Alert,
46    Command,
47}
48
49/// TextView that used to display extra information
50///
51/// Currently, we will use command palette to display extra information like
52/// full cgroup name. But the idea for this view is something like vim's command palette
53/// that use for input operation command like search, filter, rearrange, apply config, etc.
54pub struct CommandPalette {
55    content: String,
56    filter_info: Option<(String, String)>,
57    fold: bool,
58    mode: CPMode,
59    cmd_view: RefCell<EditView>,
60    cmd_controllers: Rc<RefCell<HashMap<&'static str, Controllers>>>,
61    cmd_history: VecDeque<String>,
62    cur_cmd_idx: usize,
63}
64
65impl View for CommandPalette {
66    fn draw(&self, printer: &Printer) {
67        // Right most X position that contains text
68        let mut max_x = printer.size.x;
69
70        printer.print_hline((0, 0), printer.size.x, "─");
71        if let Some((field, filter)) = &self.filter_info {
72            let output = format!(
73                "|| Filtered Column: {:>10.10} | Filter: {:>10.10} ||",
74                field, filter
75            );
76            max_x -= output.len();
77            printer.print((max_x, 0), &output);
78        }
79
80        if self.fold {
81            let text = "| Fold |";
82            max_x -= text.len();
83            printer.print((max_x, 0), text);
84        }
85
86        match self.mode {
87            CPMode::Command => {
88                printer.print((0, 1), ":");
89                let inner_printer = printer.offset((1, 1));
90                self.cmd_view.borrow_mut().layout(inner_printer.size);
91                self.cmd_view.borrow().draw(&inner_printer);
92            }
93            _ => {
94                // Message should adapt the screen size
95                let mut msg_len_left = self.content.len();
96                let mut idx = 0;
97                let mut line = 1;
98                while msg_len_left > printer.size.x {
99                    self.print(printer, (0, line), idx);
100                    msg_len_left -= printer.size.x;
101                    idx += printer.size.x;
102                    line += 1;
103                }
104                self.print(printer, (0, line), idx);
105            }
106        }
107    }
108
109    fn on_event(&mut self, event: Event) -> EventResult {
110        match event {
111            Event::Key(Key::Up) => {
112                self.prev_cmd();
113                EventResult::Consumed(None)
114            }
115            Event::Key(Key::Down) => {
116                self.next_cmd();
117                EventResult::Consumed(None)
118            }
119            _ => self.cmd_view.borrow_mut().on_event(event),
120        }
121    }
122
123    fn required_size(&mut self, constraint: Vec2) -> Vec2 {
124        Vec2::new(1, self.content.len() / constraint.x + 2)
125    }
126}
127
128impl CommandPalette {
129    /// Create a new CommandPalette
130    pub fn new<V: 'static + ViewBridge>(
131        name: &'static str,
132        content: &str,
133        cmd_controllers: Rc<RefCell<HashMap<&'static str, Controllers>>>,
134    ) -> Self {
135        Self {
136            content: content.into(),
137            filter_info: None,
138            fold: false,
139            mode: CPMode::Info,
140            cmd_view: RefCell::new(
141                EditView::new()
142                    .on_submit(move |c, cmd| {
143                        Self::handle_cmd_history(name, c, cmd);
144                        Self::run_cmd::<V>(name, c, cmd)
145                    })
146                    .style(ColorStyle::terminal_default()),
147            ),
148            cmd_controllers,
149            cmd_history: VecDeque::new(),
150            cur_cmd_idx: 0,
151        }
152    }
153
154    fn handle_cmd_history(name: &'static str, c: &mut Cursive, cmd: &str) {
155        c.call_on_name(
156            &format!("{}_cmd_palette", name),
157            |cp: &mut NamedView<CommandPalette>| {
158                let mut cmd_palette = cp.get_mut();
159                cmd_palette.cmd_history.push_back(cmd.into());
160                if cmd_palette.cmd_history.len() > MAX_CMD_HISTORY {
161                    cmd_palette.cmd_history.pop_front();
162                }
163                cmd_palette.cur_cmd_idx = cmd_palette.cmd_history.len() - 1;
164            },
165        );
166    }
167
168    fn prev_cmd(&mut self) {
169        if self.cmd_history.is_empty() {
170            return;
171        }
172        self.cmd_view
173            .borrow_mut()
174            .set_content(&self.cmd_history[self.cur_cmd_idx]);
175        if self.cur_cmd_idx > 0 {
176            self.cur_cmd_idx -= 1;
177        }
178    }
179
180    fn next_cmd(&mut self) {
181        if self.cmd_history.is_empty() {
182            return;
183        }
184        if self.cur_cmd_idx == self.cmd_history.len() - 1 {
185            self.cmd_view.borrow_mut().set_content("");
186        } else {
187            self.cur_cmd_idx += 1;
188            self.cmd_view
189                .borrow_mut()
190                .set_content(&self.cmd_history[self.cur_cmd_idx]);
191        }
192    }
193
194    /// Run the captured command
195    // In this function, we should avoid borrowing command palette object, since
196    // it will cause a double mut borrow in the handler.
197    pub fn run_cmd<V: 'static + ViewBridge>(name: &'static str, c: &mut Cursive, cmd: &str) {
198        let cmd_vec = cmd.trim().split(' ').collect::<Vec<&str>>();
199        let controller = c
200            .find_name::<Self>(&format!("{}_cmd_palette", name))
201            .expect("Fail to get cmd_palette")
202            .cmd_controllers
203            .borrow()
204            .get(cmd_vec[0])
205            .unwrap_or(&Controllers::Unknown)
206            .clone();
207
208        match controller {
209            Controllers::Unknown => {
210                let mut cp = c
211                    .find_name::<Self>(&format!("{}_cmd_palette", name))
212                    .expect("Fail to get cmd_palette");
213                cp.mode = CPMode::Alert;
214                cp.content = "Unknown Command".into();
215                cp.cmd_view.borrow_mut().set_content("");
216            }
217            _ => {
218                controller.handle(&mut StatsView::<V>::get_view(c), &cmd_vec);
219                controller.callback::<V>(c, &cmd_vec);
220                c.call_on_name(
221                    &format!("{}_cmd_palette", name),
222                    |cp: &mut NamedView<CommandPalette>| {
223                        cp.get_mut().reset_cmd();
224                    },
225                );
226            }
227        }
228    }
229
230    pub fn reset_cmd(&mut self) {
231        self.mode = CPMode::Info;
232        self.cmd_view.borrow_mut().set_content("");
233    }
234
235    /// Turn cmd_palette into command input mode
236    pub fn invoke_cmd(&mut self) {
237        self.mode = CPMode::Command;
238        self.content = "".into()
239    }
240
241    /// Check if command palette is in command mode
242    pub fn is_cmd_mode(&self) -> bool {
243        self.mode == CPMode::Command
244    }
245
246    /// Set the display info
247    pub fn set_info<T: Into<String>>(&mut self, content: T) {
248        self.content = content.into();
249        if self.mode != CPMode::Command {
250            self.mode = CPMode::Info;
251        }
252    }
253
254    /// Set alert
255    /// This will preempt the command palette mode.
256    pub fn set_alert<T: Into<String>>(&mut self, content: T) {
257        if self.mode == CPMode::Alert {
258            // Attach to current alert if it is not consumed.
259            self.content = format!("{} |=| {}", self.content, content.into());
260        } else {
261            self.content = content.into();
262            if self.mode != CPMode::Command {
263                self.mode = CPMode::Alert;
264            }
265        }
266    }
267
268    pub fn set_filter(&mut self, filter_info: Option<(String, String)>) {
269        self.filter_info = filter_info;
270    }
271
272    pub fn toggle_fold(&mut self) {
273        self.fold = !self.fold;
274    }
275
276    fn print_info(&self, printer: &Printer, pos: Vec2, idx: usize) {
277        if idx + printer.size.x > self.content.len() {
278            printer.print(pos, &self.content[idx..]);
279        } else {
280            printer.print(pos, &self.content[idx..idx + printer.size.x]);
281        }
282    }
283
284    fn print_alert(&self, printer: &Printer, pos: Vec2, idx: usize) {
285        printer.with_color(ColorStyle::title_primary(), |printer| {
286            if idx + printer.size.x > self.content.len() {
287                printer.print(pos, &self.content[idx..]);
288            } else {
289                printer.print(pos, &self.content[idx..idx + printer.size.x]);
290            }
291        })
292    }
293
294    fn print<T: Into<Vec2>>(&self, printer: &Printer, pos: T, idx: usize) {
295        match self.mode {
296            CPMode::Info => self.print_info(printer, pos.into(), idx),
297            CPMode::Alert => self.print_alert(printer, pos.into(), idx),
298            _ => {}
299        }
300    }
301
302    pub fn is_alerting(&self) -> bool {
303        self.mode == CPMode::Alert
304    }
305
306    pub fn get_content(&self) -> &str {
307        &self.content
308    }
309}