diff options
author | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2019-05-15 22:03:17 +0300 |
---|---|---|
committer | Manos Pitsidianakis <el13635@mail.ntua.gr> | 2019-06-10 19:40:49 +0300 |
commit | 06b96449c107e46f75dfb2e478229e2587181cf6 (patch) | |
tree | d174eb305c48d7cdd481b6cf0964a44516ce2321 /ui | |
parent | 3c575c823da863573d831ab622c2f7b846bd60d5 (diff) |
ui: add history and autocomplete in execute bar
closes #116 and #117
Diffstat (limited to 'ui')
-rw-r--r-- | ui/src/components/mail/view/thread.rs | 2 | ||||
-rw-r--r-- | ui/src/components/utilities.rs | 202 | ||||
-rw-r--r-- | ui/src/components/utilities/widgets.rs | 79 | ||||
-rw-r--r-- | ui/src/terminal/text_editing.rs | 10 |
4 files changed, 267 insertions, 26 deletions
diff --git a/ui/src/components/mail/view/thread.rs b/ui/src/components/mail/view/thread.rs index 3f0099c0..bac5c8e4 100644 --- a/ui/src/components/mail/view/thread.rs +++ b/ui/src/components/mail/view/thread.rs @@ -448,6 +448,7 @@ impl ThreadView { self.highlight_line(grid, dest_area, src_area, idx); if rows < visibles.len() { ScrollBar::draw( + ScrollBar::default(), grid, ( upper_left!(area), @@ -500,6 +501,7 @@ impl ThreadView { self.highlight_line(grid, dest_area, src_area, entry_idx); if rows < visibles.len() { ScrollBar::draw( + ScrollBar::default(), grid, ( upper_left!(area), diff --git a/ui/src/components/utilities.rs b/ui/src/components/utilities.rs index 31770cba..ea7c208b 100644 --- a/ui/src/components/utilities.rs +++ b/ui/src/components/utilities.rs @@ -561,12 +561,15 @@ pub struct StatusBar { container: Box<Component>, status: String, notifications: VecDeque<String>, - ex_buffer: String, + ex_buffer: Field, display_buffer: String, mode: UIMode, height: usize, dirty: bool, id: ComponentId, + + auto_complete: AutoComplete, + cmd_history: Vec<String>, } impl fmt::Display for StatusBar { @@ -582,12 +585,15 @@ impl StatusBar { container, status: String::with_capacity(256), notifications: VecDeque::new(), - ex_buffer: String::with_capacity(256), + ex_buffer: Field::Text(UText::new(String::with_capacity(256)), None), display_buffer: String::with_capacity(8), dirty: true, mode: UIMode::Normal, height: 1, id: ComponentId::new_v4(), + auto_complete: AutoComplete::new(Vec::new()), + + cmd_history: Vec::new(), } } fn draw_status_bar(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { @@ -628,7 +634,7 @@ impl StatusBar { fn draw_execute_bar(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { clear_area(grid, area); write_string_to_grid( - &self.ex_buffer, + self.ex_buffer.as_str(), grid, Color::Byte(219), Color::Byte(88), @@ -683,6 +689,160 @@ impl Component for StatusBar { ), context, ); + /* don't autocomplete for less than 3 characters */ + if self.ex_buffer.as_str().split_graphemes().len() <= 2 { + self.container.set_dirty(); + return; + } + let suggestions: Vec<String> = self + .cmd_history + .iter() + .filter_map(|h| { + if h.starts_with(self.ex_buffer.as_str()) { + Some(h.clone()) + } else { + None + } + }) + .collect(); + if suggestions.is_empty() { + /* redraw self.container because we might have got ridden of an autocomplete + * box, and it must be drawn over */ + self.container.set_dirty(); + return; + } + /* redraw self.container because we have less suggestions than before */ + if suggestions.len() < self.auto_complete.suggestions().len() { + self.container.set_dirty(); + } + + if self.auto_complete.set_suggestions(suggestions) { + let len = self.auto_complete.suggestions().len() - 1; + self.auto_complete.set_cursor(len); + } + let hist_height = std::cmp::min(15, self.auto_complete.suggestions().len()); + let hist_area = if height < self.auto_complete.suggestions().len() { + let mut scrollbar = ScrollBar::default(); + scrollbar.set_show_arrows(false); + scrollbar.set_block_character(Some('▌')); + scrollbar.draw( + grid, + ( + ( + get_x(upper_left), + get_y(bottom_right) - height - hist_height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - height), + ), + self.auto_complete.cursor(), + hist_height, + self.auto_complete.suggestions().len(), + ); + change_colors( + grid, + ( + ( + get_x(upper_left), + get_y(bottom_right) - height - hist_height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - height), + ), + Color::Byte(197), // DeepPink2, + Color::Byte(174), //LightPink3 + ); + context.dirty_areas.push_back(( + ( + get_x(upper_left), + get_y(bottom_right) - height - hist_height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - height), + )); + ( + ( + get_x(upper_left) + 1, + get_y(bottom_right) - height - hist_height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - height), + ) + } else { + ( + ( + get_x(upper_left), + get_y(bottom_right) - height - hist_height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - height), + ) + }; + let offset = if hist_height + > (self.auto_complete.suggestions().len() - self.auto_complete.cursor()) + { + self.auto_complete.suggestions().len() - hist_height + } else { + self.auto_complete.cursor() + }; + clear_area(grid, hist_area); + change_colors( + grid, + hist_area, + Color::Byte(88), // DarkRed, + Color::Byte(174), //LightPink3 + ); + for (y_offset, s) in self + .auto_complete + .suggestions() + .iter() + .skip(offset) + .take(hist_height) + .enumerate() + { + write_string_to_grid( + s.as_str(), + grid, + Color::Byte(88), // DarkRed, + Color::Byte(174), //LightPink3 + ( + set_y( + upper_left!(hist_area), + get_y(bottom_right!(hist_area)) - hist_height + y_offset + 1, + ), + bottom_right!(hist_area), + ), + true, + ); + if y_offset + offset == self.auto_complete.cursor() { + change_colors( + grid, + ( + set_y( + upper_left!(hist_area), + get_y(bottom_right!(hist_area)) - hist_height + y_offset + 1, + ), + set_y( + bottom_right!(hist_area), + get_y(bottom_right!(hist_area)) - hist_height + y_offset + 1, + ), + ), + Color::Byte(88), // DarkRed, + Color::Byte(173), //LightSalmon3 + ); + write_string_to_grid( + &s.as_str()[self.ex_buffer.as_str().len()..], + grid, + Color::Byte(97), // MediumPurple3, + Color::Byte(88), //LightPink3 + ( + ( + get_y(upper_left) + + self.ex_buffer.as_str().split_graphemes().len(), + get_y(bottom_right) - height + 1, + ), + set_y(bottom_right, get_y(bottom_right) - height + 1), + ), + true, + ); + } + } + context.dirty_areas.push_back(hist_area); } _ => {} } @@ -721,9 +881,15 @@ impl Component for StatusBar { if !self.ex_buffer.is_empty() { context .replies - .push_back(UIEvent::Command(self.ex_buffer.clone())); + .push_back(UIEvent::Command(self.ex_buffer.as_str().to_string())); + } + if parse_command(&self.ex_buffer.as_str().as_bytes()) + .to_full_result() + .is_ok() + { + self.cmd_history.push(self.ex_buffer.as_str().to_string()); } - self.ex_buffer.clear() + self.ex_buffer.clear(); } UIMode::Execute => { self.height = 2; @@ -731,9 +897,20 @@ impl Component for StatusBar { _ => {} }; } + UIEvent::ExInput(Key::Char('\t')) => { + if let Some(suggestion) = self.auto_complete.get_suggestion() { + let mut utext = UText::new(suggestion); + let len = utext.as_str().len(); + utext.set_cursor(len); + self.container.set_dirty(); + self.set_dirty(); + self.ex_buffer = Field::Text(utext, None); + } + } UIEvent::ExInput(Key::Char(c)) => { self.dirty = true; - self.ex_buffer.push(*c); + self.ex_buffer + .process_event(&mut UIEvent::InsertInput(Key::Char(*c)), context); return true; } UIEvent::ExInput(Key::Ctrl('u')) => { @@ -741,11 +918,20 @@ impl Component for StatusBar { self.ex_buffer.clear(); return true; } - UIEvent::ExInput(Key::Backspace) | UIEvent::ExInput(Key::Ctrl('h')) => { + UIEvent::ExInput(k @ Key::Backspace) | UIEvent::ExInput(k @ Key::Ctrl('h')) => { self.dirty = true; - self.ex_buffer.pop(); + self.ex_buffer + .process_event(&mut UIEvent::InsertInput(k.clone()), context); return true; } + UIEvent::ExInput(Key::Up) => { + self.auto_complete.dec_cursor(); + self.dirty = true; + } + UIEvent::ExInput(Key::Down) => { + self.auto_complete.inc_cursor(); + self.dirty = true; + } UIEvent::Resize => { self.dirty = true; } diff --git a/ui/src/components/utilities/widgets.rs b/ui/src/components/utilities/widgets.rs index a2be3e16..5721ea4a 100644 --- a/ui/src/components/utilities/widgets.rs +++ b/ui/src/components/utilities/widgets.rs @@ -36,12 +36,12 @@ use Field::*; impl Default for Field { fn default() -> Field { - Field::Text(UText::new(String::new()), None) + Field::Text(UText::new(String::with_capacity(256)), None) } } impl Field { - fn as_str(&self) -> &str { + pub fn as_str(&self) -> &str { match self { Text(ref s, _) => s.as_str(), Choice(ref v, cursor) => { @@ -53,6 +53,9 @@ impl Field { } } } + pub fn is_empty(&self) -> bool { + self.as_str().is_empty() + } pub fn into_string(self) -> String { match self { @@ -61,7 +64,14 @@ impl Field { } } - fn draw_cursor( + pub fn clear(&mut self) { + match self { + Text(s, _) => s.clear(), + Choice(_, _) => {} + } + } + + pub fn draw_cursor( &mut self, grid: &mut CellBuffer, area: Area, @@ -168,7 +178,7 @@ impl Component for Field { } } } - UIEvent::InsertInput(Key::Backspace) => { + UIEvent::InsertInput(Key::Backspace) | UIEvent::InsertInput(Key::Ctrl('h')) => { if let Text(ref mut s, auto_complete) = self { s.backspace(); if let Some(ac) = auto_complete.as_mut() { @@ -565,7 +575,7 @@ where } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct AutoComplete { entries: Vec<String>, content: CellBuffer, @@ -639,9 +649,9 @@ impl AutoComplete { ret } - pub fn set_suggestions(&mut self, entries: Vec<String>) { + pub fn set_suggestions(&mut self, entries: Vec<String>) -> bool { if entries.len() == self.entries.len() && entries == self.entries { - return; + return false;; } let mut content = CellBuffer::new( @@ -671,6 +681,7 @@ impl AutoComplete { self.content = content; self.entries = entries; self.cursor = 0; + true } pub fn inc_cursor(&mut self) { @@ -684,6 +695,15 @@ impl AutoComplete { self.set_dirty(); } + pub fn cursor(&self) -> usize { + self.cursor + } + + pub fn set_cursor(&mut self, val: usize) { + debug_assert!(val < self.entries.len()); + self.cursor = val; + } + pub fn get_suggestion(&mut self) -> Option<String> { if self.entries.is_empty() { return None; @@ -694,20 +714,43 @@ impl AutoComplete { self.content.empty(); Some(ret) } + + pub fn suggestions(&self) -> &Vec<String> { + &self.entries + } } -pub struct ScrollBar(); +#[derive(Default)] +pub struct ScrollBar { + show_arrows: bool, + block_character: Option<char>, +} impl ScrollBar { - pub fn draw(grid: &mut CellBuffer, area: Area, pos: usize, visible_rows: usize, length: usize) { + pub fn set_show_arrows(&mut self, flag: bool) { + self.show_arrows = flag; + } + pub fn set_block_character(&mut self, val: Option<char>) { + self.block_character = val; + } + pub fn draw( + self, + grid: &mut CellBuffer, + area: Area, + pos: usize, + visible_rows: usize, + length: usize, + ) { if length == 0 { return; } - let height = height!(area); + let mut height = height!(area); if height < 3 { return; } - let height = height - 2; + if self.show_arrows { + height = height - 2; + } clear_area(grid, area); let visible_ratio: f32 = (std::cmp::min(visible_rows, length) as f32) / (length as f32); @@ -720,10 +763,12 @@ impl ScrollBar { temp } }; - let (upper_left, bottom_right) = area; + let (mut upper_left, bottom_right) = area; - grid[upper_left].set_ch('▴'); - let upper_left = (upper_left.0, upper_left.1 + 1); + if self.show_arrows { + grid[upper_left].set_ch('▴'); + upper_left = (upper_left.0, upper_left.1 + 1); + } for y in get_y(upper_left)..(get_y(upper_left) + scrollbar_offset) { grid[set_y(upper_left, y)].set_ch(' '); @@ -731,12 +776,14 @@ impl ScrollBar { for y in (get_y(upper_left) + scrollbar_offset) ..=(get_y(upper_left) + scrollbar_offset + scrollbar_height) { - grid[set_y(upper_left, y)].set_ch('█'); + grid[set_y(upper_left, y)].set_ch(self.block_character.unwrap_or('█')); } for y in (get_y(upper_left) + scrollbar_offset + scrollbar_height + 1)..get_y(bottom_right) { grid[set_y(upper_left, y)].set_ch(' '); } - grid[set_x(bottom_right, get_x(upper_left))].set_ch('▾'); + if self.show_arrows { + grid[set_x(bottom_right, get_x(upper_left))].set_ch('▾'); + } } } diff --git a/ui/src/terminal/text_editing.rs b/ui/src/terminal/text_editing.rs index 6ce24fe2..d6270852 100644 --- a/ui/src/terminal/text_editing.rs +++ b/ui/src/terminal/text_editing.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct UText { content: String, cursor_pos: usize, @@ -17,7 +17,7 @@ impl UText { } pub fn set_cursor(&mut self, cursor_pos: usize) { - if cursor_pos >= self.content.len() { + if cursor_pos > self.content.len() { return; } @@ -30,6 +30,12 @@ impl UText { self.content.as_str() } + pub fn clear(&mut self) { + self.content.clear(); + self.cursor_pos = 0; + self.grapheme_cursor_pos = 0; + } + pub fn into_string(self) -> String { self.content } |