diff options
Diffstat (limited to 'src/ui/views')
-rw-r--r-- | src/ui/views/mod.rs | 4 | ||||
-rw-r--r-- | src/ui/views/tui_command_menu.rs | 101 | ||||
-rw-r--r-- | src/ui/views/tui_textfield.rs | 242 |
3 files changed, 347 insertions, 0 deletions
diff --git a/src/ui/views/mod.rs b/src/ui/views/mod.rs index e6b1c41..70517cf 100644 --- a/src/ui/views/mod.rs +++ b/src/ui/views/mod.rs @@ -1,7 +1,11 @@ +mod tui_command_menu; mod tui_folder_view; +mod tui_textfield; mod tui_view; mod tui_worker_view; +pub use self::tui_command_menu::TuiCommandMenu; pub use self::tui_folder_view::TuiFolderView; +pub use self::tui_textfield::TuiTextField; pub use self::tui_view::TuiView; pub use self::tui_worker_view::TuiWorkerView; diff --git a/src/ui/views/tui_command_menu.rs b/src/ui/views/tui_command_menu.rs new file mode 100644 index 0000000..fa2e857 --- /dev/null +++ b/src/ui/views/tui_command_menu.rs @@ -0,0 +1,101 @@ +use std::iter::Iterator; + +use termion::event::{Event, Key}; +use tui::layout::Rect; +use tui::widgets::Clear; + +use crate::commands::{CommandKeybind, KeyCommand}; +use crate::config::JoshutoCommandMapping; +use crate::context::JoshutoContext; +use crate::ui::views::TuiView; +use crate::ui::widgets::TuiMenu; +use crate::ui::TuiBackend; +use crate::util::event::JoshutoEvent; +use crate::util::input; +use crate::util::to_string::ToString; + +const BORDER_HEIGHT: usize = 1; +const BOTTOM_MARGIN: usize = 1; + +pub struct TuiCommandMenu; + +impl TuiCommandMenu { + pub fn new() -> Self { + Self {} + } + + pub fn get_input<'a>( + &mut self, + backend: &mut TuiBackend, + context: &mut JoshutoContext, + m: &'a JoshutoCommandMapping, + ) -> Option<&'a KeyCommand> { + let mut map: &JoshutoCommandMapping = &m; + let terminal = backend.terminal_mut(); + context.flush_event(); + + loop { + let _ = terminal.draw(|frame| { + let f_size: Rect = frame.size(); + + { + let view = TuiView::new(&context); + frame.render_widget(view, f_size); + } + + { + // draw menu + let mut display_vec: Vec<String> = map + .as_ref() + .iter() + .map(|(k, v)| format!(" {} {}", k.to_string(), v)) + .collect(); + display_vec.sort(); + let display_str: Vec<&str> = display_vec.iter().map(|v| v.as_str()).collect(); + let display_str_len = display_str.len(); + + let y = if (f_size.height as usize) + < display_str_len + BORDER_HEIGHT + BOTTOM_MARGIN + { + 0 + } else { + f_size.height + - (BORDER_HEIGHT + BOTTOM_MARGIN) as u16 + - display_str_len as u16 + }; + + let menu_rect = Rect { + x: 0, + y, + width: f_size.width, + height: (display_str_len + BORDER_HEIGHT) as u16, + }; + + frame.render_widget(Clear, menu_rect); + frame.render_widget(TuiMenu::new(&display_str), menu_rect); + } + }); + + if let Ok(event) = context.poll_event() { + match event { + JoshutoEvent::Termion(event) => { + match event { + Event::Key(Key::Esc) => return None, + event => match map.as_ref().get(&event) { + Some(CommandKeybind::SimpleKeybind(s)) => { + return Some(s); + } + Some(CommandKeybind::CompositeKeybind(m)) => { + map = m; + } + None => return None, + }, + } + context.flush_event(); + } + event => input::process_noninteractive(event, context), + } + } + } + } +} diff --git a/src/ui/views/tui_textfield.rs b/src/ui/views/tui_textfield.rs new file mode 100644 index 0000000..db1bc6f --- /dev/null +++ b/src/ui/views/tui_textfield.rs @@ -0,0 +1,242 @@ +use rustyline::completion::{Candidate, Completer, FilenameCompleter, Pair}; +use rustyline::line_buffer; + +use termion::event::{Event, Key}; +use tui::layout::Rect; +use tui::widgets::Clear; + +use crate::context::JoshutoContext; +use crate::ui::views::TuiView; +use crate::ui::widgets::{TuiMenu, TuiMultilineText}; +use crate::ui::TuiBackend; +use crate::util::event::JoshutoEvent; +use crate::util::input; + +struct CompletionTracker { + pub index: usize, + pub pos: usize, + pub original: String, + pub candidates: Vec<Pair>, +} + +impl CompletionTracker { + pub fn new(pos: usize, candidates: Vec<Pair>, original: String) -> Self { + CompletionTracker { + index: 0, + pos, + original, + candidates, + } + } +} + +pub struct TuiTextField<'a> { + _prompt: &'a str, + _prefix: &'a str, + _suffix: &'a str, + _menu_items: Vec<&'a str>, +} + +impl<'a> TuiTextField<'a> { + pub fn menu_items<I>(&mut self, items: I) -> &mut Self + where + I: Iterator<Item = &'a str>, + { + self._menu_items = items.collect(); + self + } + + pub fn prompt(&mut self, prompt: &'a str) -> &mut Self { + self._prompt = prompt; + self + } + + pub fn prefix(&mut self, prefix: &'a str) -> &mut Self { + self._prefix = prefix; + self + } + + pub fn suffix(&mut self, suffix: &'a str) -> &mut Self { + self._suffix = suffix; + self + } + + pub fn get_input( + &mut self, + backend: &mut TuiBackend, + context: &mut JoshutoContext, + ) -> Option<String> { + context.flush_event(); + + let mut line_buffer = line_buffer::LineBuffer::with_capacity(255); + let completer = FilenameCompleter::new(); + + let mut completion_tracker: Option<CompletionTracker> = None; + + let char_idx = self._prefix.chars().map(|c| c.len_utf8()).sum(); + + line_buffer.insert_str(0, self._suffix); + line_buffer.insert_str(0, self._prefix); + line_buffer.set_pos(char_idx); + + let terminal = backend.terminal_mut(); + + loop { + terminal + .draw(|frame| { + let area: Rect = frame.size(); + if area.height == 0 { + return; + } + { + let mut view = TuiView::new(&context); + view.show_bottom_status = false; + frame.render_widget(view, area); + } + + let cursor_xpos = line_buffer.pos(); + + let area_width = area.width as usize; + let buffer_str = line_buffer.as_str(); + let line_str = format!("{}{}", self._prompt, buffer_str); + let multiline = + TuiMultilineText::new(line_str.as_str(), area_width, Some(cursor_xpos)); + let multiline_height = multiline.len(); + + { + let menu_widget = TuiMenu::new(self._menu_items.as_slice()); + let menu_len = menu_widget.len(); + let menu_y = if menu_len + 1 > area.height as usize { + 0 + } else { + (area.height as usize - menu_len - 1) as u16 + }; + + let menu_rect = Rect { + x: 0, + y: menu_y - multiline_height as u16, + width: area.width, + height: menu_len as u16 + 1, + }; + frame.render_widget(Clear, menu_rect); + frame.render_widget(menu_widget, menu_rect); + } + + let multiline_rect = Rect { + x: 0, + y: area.height - multiline_height as u16, + width: area.width, + height: multiline_height as u16, + }; + + frame.render_widget(Clear, multiline_rect); + frame.render_widget(multiline, multiline_rect); + }) + .unwrap(); + + if let Ok(event) = context.poll_event() { + match event { + JoshutoEvent::Termion(Event::Key(key)) => { + match key { + Key::Backspace => { + if line_buffer.backspace(1) { + completion_tracker.take(); + } + } + Key::Left => { + if line_buffer.move_backward(1) { + completion_tracker.take(); + } + } + Key::Right => { + if line_buffer.move_forward(1) { + completion_tracker.take(); + } + } + Key::Delete => { + if line_buffer.delete(1).is_some() { + completion_tracker.take(); + } + } + Key::Home => { + line_buffer.move_home(); + completion_tracker.take(); + } + Key::End => { + line_buffer.move_end(); + completion_tracker.take(); + } + Key::Up => {} + Key::Down => {} + Key::Esc => { + return None; + } + 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.display(), + ); + s.index += 1; + } + } + } + Key::Char('\n') => { + break; + } + Key::Char(c) => { + if line_buffer.insert(c, 1).is_some() { + completion_tracker.take(); + } + } + _ => {} + } + context.flush_event(); + } + JoshutoEvent::Termion(_) => { + context.flush_event(); + } + event => input::process_noninteractive(event, context), + }; + } + } + if line_buffer.as_str().is_empty() { + None + } else { + let input_string = line_buffer.to_string(); + Some(input_string) + } + } +} + +impl<'a> std::default::Default for TuiTextField<'a> { + fn default() -> Self { + Self { + _prompt: "", + _prefix: "", + _suffix: "", + _menu_items: vec![], + } + } +} |