summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2019-05-15 22:03:17 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2019-06-10 19:40:49 +0300
commit06b96449c107e46f75dfb2e478229e2587181cf6 (patch)
treed174eb305c48d7cdd481b6cf0964a44516ce2321
parent3c575c823da863573d831ab622c2f7b846bd60d5 (diff)
ui: add history and autocomplete in execute bar
closes #116 and #117
-rw-r--r--ui/src/components/mail/view/thread.rs2
-rw-r--r--ui/src/components/utilities.rs202
-rw-r--r--ui/src/components/utilities/widgets.rs79
-rw-r--r--ui/src/terminal/text_editing.rs10
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
}