ui: add history and autocomplete in execute bar

closes #116 and #117
embed
Manos Pitsidianakis 2019-05-15 22:03:17 +03:00
parent 3c575c823d
commit 06b96449c1
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
4 changed files with 267 additions and 26 deletions

View File

@ -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),

View File

@ -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()));
}
self.ex_buffer.clear()
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();
}
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;
}

View File

@ -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('▾');
}
}
}

View File

@ -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
}