summaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
authorJiayi Zhao <jeff.no.zhao@gmail.com>2020-02-22 19:54:41 -0500
committerJiayi Zhao <jeff.no.zhao@gmail.com>2020-02-22 19:54:41 -0500
commitd38dcdbbee44187bdb605dbf9bbf9c6c6d3e4f35 (patch)
tree91b059b99793fe250c67f66be3930a298117e0b3 /src/ui
parent03594099dafb4cda04e50f087df61cf76e2034d0 (diff)
implement textfield widget
- for asking users for long input strings - implement prompt widget - for prompting users for a single key response
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/widgets/mod.rs2
-rw-r--r--src/ui/widgets/tui_prompt.rs60
-rw-r--r--src/ui/widgets/tui_textfield.rs176
-rw-r--r--src/ui/widgets/tui_view.rs40
4 files changed, 258 insertions, 20 deletions
diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs
index e36c904..49fbfc5 100644
--- a/src/ui/widgets/mod.rs
+++ b/src/ui/widgets/mod.rs
@@ -2,6 +2,7 @@ pub mod tui_dirlist;
pub mod tui_dirlist_detailed;
pub mod tui_footer;
pub mod tui_menu;
+pub mod tui_prompt;
pub mod tui_textfield;
pub mod tui_topbar;
pub mod tui_view;
@@ -10,6 +11,7 @@ pub use self::tui_dirlist::TuiDirList;
pub use self::tui_dirlist_detailed::TuiDirListDetailed;
pub use self::tui_footer::TuiFooter;
pub use self::tui_menu::{TuiCommandMenu, TuiMenu};
+pub use self::tui_prompt::TuiPrompt;
pub use self::tui_textfield::TuiTextField;
pub use self::tui_topbar::TuiTopBar;
pub use self::tui_view::TuiView;
diff --git a/src/ui/widgets/tui_prompt.rs b/src/ui/widgets/tui_prompt.rs
new file mode 100644
index 0000000..21526f3
--- /dev/null
+++ b/src/ui/widgets/tui_prompt.rs
@@ -0,0 +1,60 @@
+use termion::event::Key;
+use tui::layout::Rect;
+use tui::style::{Color, Style};
+use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
+use unicode_width::UnicodeWidthStr;
+
+use crate::context::JoshutoContext;
+use crate::ui::TuiBackend;
+use crate::util::event::{Event, Events};
+
+use super::TuiView;
+
+pub struct TuiPrompt<'a> {
+ prompt: &'a str,
+}
+
+impl<'a> TuiPrompt<'a> {
+ pub fn new(prompt: &'a str) -> Self {
+ Self { prompt }
+ }
+
+ pub fn get_key(&mut self, backend: &mut TuiBackend, context: &JoshutoContext) -> Key {
+ loop {
+ backend.terminal.draw(|mut frame| {
+ let f_size = frame.size();
+ if f_size.height == 0 {
+ return;
+ }
+
+ {
+ let mut view = TuiView::new(&context);
+ view.show_bottom_status = false;
+ view.render(&mut frame, f_size);
+ }
+
+ let prompt_style = Style::default().fg(Color::LightYellow);
+
+ let text = [Text::styled(self.prompt, prompt_style)];
+
+ let textfield_rect = Rect {
+ x: 0,
+ y: f_size.height - 1,
+ width: f_size.width,
+ height: 1,
+ };
+
+ Paragraph::new(text.iter())
+ .wrap(true)
+ .render(&mut frame, textfield_rect);
+ });
+
+ if let Ok(event) = context.events.next() {
+ match event {
+ Event::Input(key) => return key,
+ _ => {}
+ };
+ }
+ }
+ }
+}
diff --git a/src/ui/widgets/tui_textfield.rs b/src/ui/widgets/tui_textfield.rs
index c0f54a8..9dbbad3 100644
--- a/src/ui/widgets/tui_textfield.rs
+++ b/src/ui/widgets/tui_textfield.rs
@@ -1,9 +1,11 @@
+use std::io::Write;
+
use rustyline::completion::{Candidate, Completer, FilenameCompleter, Pair};
use rustyline::line_buffer;
-use termion::clear;
use termion::cursor::Goto;
use termion::event::Key;
+use tui::backend::Backend;
use tui::layout::Rect;
use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
@@ -13,7 +15,7 @@ use crate::context::JoshutoContext;
use crate::ui::TuiBackend;
use crate::util::event::{Event, Events};
-use super::TuiMenu;
+use super::{TuiMenu, TuiView};
struct CompletionTracker {
pub index: usize,
@@ -34,12 +36,31 @@ impl CompletionTracker {
}
pub struct TuiTextField<'a> {
- menu: Option<&'a mut TuiMenu<'a>>,
+ _prompt: &'a str,
+ _prefix: &'a str,
+ _suffix: &'a str,
+ _menu: Option<&'a mut TuiMenu<'a>>,
}
impl<'a> TuiTextField<'a> {
- pub fn new(menu: &'a mut TuiMenu<'a>) -> Self {
- Self { menu: Some(menu) }
+ pub fn menu(mut self, menu: &'a mut TuiMenu<'a>) -> Self {
+ self._menu = Some(menu);
+ self
+ }
+
+ pub fn prompt(mut self, prompt: &'a str) -> Self {
+ self._prompt = prompt;
+ self
+ }
+
+ pub fn prefix(mut self, prefix: &'a str) -> Self {
+ self._prefix = prefix;
+ self
+ }
+
+ pub fn suffix(mut self, suffix: &'a str) -> Self {
+ self._suffix = suffix;
+ self
}
pub fn get_input(
@@ -47,11 +68,44 @@ impl<'a> TuiTextField<'a> {
backend: &mut TuiBackend,
context: &JoshutoContext,
) -> Option<String> {
- let mut input_string = String::with_capacity(64);
+ let mut line_buffer = line_buffer::LineBuffer::with_capacity(255);
+ let completer = FilenameCompleter::new();
+
+ let mut completion_tracker: Option<CompletionTracker> = None;
+
+ let mut char_idx = self
+ ._prefix
+ .char_indices()
+ .last()
+ .map(|(i, c)| i)
+ .unwrap_or(0);
+
+ line_buffer.insert_str(0, self._prefix);
+ line_buffer.insert_str(line_buffer.len(), self._suffix);
+ line_buffer.set_pos(char_idx);
+
+ backend.terminal.show_cursor();
+ let mut cursor_xpos = line_buffer.pos() + 1;
+ {
+ let frame = backend.terminal.get_frame();
+ let f_size = frame.size();
+ backend
+ .terminal
+ .set_cursor(cursor_xpos as u16, f_size.height - 1);
+ }
loop {
backend.terminal.draw(|mut frame| {
let f_size = frame.size();
+ if f_size.height == 0 {
+ return;
+ }
+
+ {
+ let mut view = TuiView::new(&context);
+ view.show_bottom_status = false;
+ view.render(&mut frame, f_size);
+ }
let top_rect = Rect {
x: 0,
@@ -60,27 +114,129 @@ impl<'a> TuiTextField<'a> {
height: 1,
};
- if let Some(menu) = self.menu.as_mut() {
+ if let Some(menu) = self._menu.as_mut() {
menu.render(&mut frame, top_rect);
}
+
+ let cmd_prompt_style = Style::default().fg(Color::LightGreen);
+
+ let text = [
+ Text::styled(self._prompt, cmd_prompt_style),
+ Text::raw(line_buffer.as_str()),
+ ];
+
+ let textfield_rect = Rect {
+ x: 0,
+ y: f_size.height - 1,
+ width: f_size.width,
+ height: 1,
+ };
+
+ Paragraph::new(text.iter())
+ .wrap(true)
+ .render(&mut frame, textfield_rect);
});
if let Ok(event) = context.events.next() {
match event {
+ Event::Input(Key::Backspace) => {
+ if line_buffer.backspace(1) {
+ completion_tracker.take();
+ }
+ }
+ Event::Input(Key::Left) => {
+ if line_buffer.move_backward(1) {
+ completion_tracker.take();
+ }
+ }
+ Event::Input(Key::Right) => {
+ if line_buffer.move_forward(1) {
+ completion_tracker.take();
+ }
+ }
+ Event::Input(Key::Delete) => {
+ if line_buffer.delete(1).is_some() {
+ completion_tracker.take();
+ }
+ }
+ Event::Input(Key::Home) => {
+ line_buffer.move_end();
+ completion_tracker.take();
+ }
+ Event::Input(Key::End) => {
+ line_buffer.move_end();
+ completion_tracker.take();
+ }
+ Event::Input(Key::Up) => {}
+ Event::Input(Key::Down) => {}
Event::Input(Key::Esc) => {
+ backend.terminal.hide_cursor();
return None;
}
+ Event::Input(Key::Char('\t')) => {
+ if completion_tracker.is_none() {
+ let res =
+ completer.complete_path(line_buffer.as_str(), line_buffer.pos());
+ if let Ok((pos, mut candidates)) = res {
+ candidates.sort_by(|x, y| {
+ x.display()
+ .partial_cmp(y.display())
+ .unwrap_or(std::cmp::Ordering::Less)
+ });
+ let ct = CompletionTracker::new(
+ pos,
+ candidates,
+ String::from(line_buffer.as_str()),
+ );
+ completion_tracker = Some(ct);
+ }
+ }
+
+ if let Some(ref mut s) = completion_tracker {
+ if s.index < s.candidates.len() {
+ let candidate = &s.candidates[s.index];
+ completer.update(&mut line_buffer, s.pos, candidate.replacement());
+ s.index += 1;
+ }
+ }
+ }
Event::Input(Key::Char('\n')) => {
break;
}
Event::Input(Key::Char(c)) => {
- input_string.push(c);
+ if line_buffer.insert(c, 1).is_some() {
+ completion_tracker.take();
+ }
}
_ => {}
};
}
+ cursor_xpos = line_buffer.pos() + 1;
+ {
+ let frame = backend.terminal.get_frame();
+ let f_size = frame.size();
+ backend
+ .terminal
+ .set_cursor(cursor_xpos as u16, f_size.height - 1);
+ }
+ }
+ backend.terminal.hide_cursor();
+ if line_buffer.as_str().is_empty() {
+ None
+ } else {
+ let strin = line_buffer.to_string();
+ Some(strin)
+ }
+ }
+}
+
+impl<'a> std::default::Default for TuiTextField<'a> {
+ fn default() -> Self {
+ Self {
+ _prompt: "",
+ _prefix: "",
+ _suffix: "",
+ _menu: None,
}
- eprintln!("You typed: {}", input_string);
- Some(input_string)
}
}
diff --git a/src/ui/widgets/tui_view.rs b/src/ui/widgets/tui_view.rs
index 57f644c..0f2d64d 100644
--- a/src/ui/widgets/tui_view.rs
+++ b/src/ui/widgets/tui_view.rs
@@ -1,6 +1,7 @@
use tui::buffer::Buffer;
use tui::layout::{Direction, Layout, Rect};
-use tui::widgets::Widget;
+use tui::style::{Color, Modifier, Style};
+use tui::widgets::{Paragraph, Text, Widget};
use unicode_width::UnicodeWidthStr;
use super::{TuiDirList, TuiDirListDetailed, TuiFooter, TuiTopBar};
@@ -8,13 +9,17 @@ use crate::context::JoshutoContext;
pub struct TuiView<'a> {
pub context: &'a JoshutoContext,
+ pub show_bottom_status: bool,
}
use super::super::{DEFAULT_LAYOUT, NO_PREVIEW_LAYOUT};
impl<'a> TuiView<'a> {
pub fn new(context: &'a JoshutoContext) -> Self {
- Self { context }
+ Self {
+ context,
+ show_bottom_status: true,
+ }
}
}
@@ -55,15 +60,30 @@ impl<'a> Widget for TuiView<'a> {
if let Some(curr_list) = curr_list.as_ref() {
TuiDirListDetailed::new(&curr_list).draw(layout_rect[1], buf);
+ let rect = Rect {
+ x: 0,
+ y: f_size.height - 1,
+ width: f_size.width,
+ height: 1,
+ };
+
+ let message_style = Style::default()
+ .fg(Color::LightCyan)
+ .modifier(Modifier::BOLD);
+
+ if self.show_bottom_status {
+ /* draw the bottom status bar */
+ if let Some(msg) = self.context.worker_msg.as_ref() {
+ let text = [Text::styled(msg, message_style)];
+
+ Paragraph::new(text.iter()).wrap(true).draw(rect, buf);
+ } else if !self.context.message_queue.is_empty() {
+ let text = [Text::styled(&self.context.message_queue[0], message_style)];
- if let Some(entry) = curr_list.get_curr_ref() {
- let rect = Rect {
- x: 0,
- y: f_size.height - 1,
- width: f_size.width,
- height: 1,
- };
- TuiFooter::new(entry).draw(rect, buf);
+ Paragraph::new(text.iter()).wrap(true).draw(rect, buf);
+ } else if let Some(entry) = curr_list.get_curr_ref() {
+ TuiFooter::new(entry).draw(rect, buf);
+ }
}
};