diff options
author | Jiayi Zhao <jeff.no.zhao@gmail.com> | 2020-02-17 21:37:44 -0500 |
---|---|---|
committer | Jiayi Zhao <jeff.no.zhao@gmail.com> | 2020-02-17 21:37:44 -0500 |
commit | aa9b25ab36d6da75d4163f13de7cad5f5ed2ce69 (patch) | |
tree | 8c798b86e1ddcc7425d2b89fd417b5267cabf91d /src/ui | |
parent | e7bcdd1fc7685e1f132adc267e56cdae2925d632 (diff) |
rework menu widget
- menu loop is now contained in widget rather than in run.rs
- add tui_footer for showing file info
- move TuiDirListDetailed into its own file
- add format util file for formatting file info
- add support for showing file size, symlink paths and modified times
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/mod.rs | 14 | ||||
-rw-r--r-- | src/ui/tui_backend.rs | 26 | ||||
-rw-r--r-- | src/ui/widgets/mod.rs | 10 | ||||
-rw-r--r-- | src/ui/widgets/tui_dirlist.rs | 136 | ||||
-rw-r--r-- | src/ui/widgets/tui_dirlist_detailed.rs | 117 | ||||
-rw-r--r-- | src/ui/widgets/tui_footer.rs | 61 | ||||
-rw-r--r-- | src/ui/widgets/tui_menu.rs | 162 |
7 files changed, 394 insertions, 132 deletions
diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 74f080d..844a70c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,18 @@ +use tui::layout::Constraint; + mod tui_backend; pub mod widgets; pub use tui_backend::*; + +pub const DEFAULT_LAYOUT: [tui::layout::Constraint; 3] = [ + Constraint::Ratio(1, 8), + Constraint::Ratio(3, 8), + Constraint::Ratio(4, 8), +]; + +pub const NO_PREVIEW_LAYOUT: [tui::layout::Constraint; 3] = [ + Constraint::Ratio(1, 8), + Constraint::Ratio(7, 8), + Constraint::Ratio(0, 8), +]; diff --git a/src/ui/tui_backend.rs b/src/ui/tui_backend.rs index 583f501..4c64497 100644 --- a/src/ui/tui_backend.rs +++ b/src/ui/tui_backend.rs @@ -8,13 +8,15 @@ use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::widgets::Widget; use unicode_width::UnicodeWidthStr; -use super::widgets::{TuiDirList, TuiDirListDetailed, TuiTopBar}; +use super::widgets::{TuiDirList, TuiDirListDetailed, TuiFooter, TuiTopBar}; use crate::context::JoshutoContext; pub struct TuiBackend { pub terminal: tui::Terminal<TermionBackend<AlternateScreen<RawTerminal<std::io::Stdout>>>>, } +use super::{DEFAULT_LAYOUT, NO_PREVIEW_LAYOUT}; + impl TuiBackend { pub fn new() -> std::io::Result<Self> { let stdout = std::io::stdout().into_raw_mode()?; @@ -53,16 +55,8 @@ impl TuiBackend { } let constraints = match child_list { - Some(_) => [ - Constraint::Ratio(1, 6), - Constraint::Ratio(2, 6), - Constraint::Ratio(3, 6), - ], - None => [ - Constraint::Ratio(1, 6), - Constraint::Ratio(5, 6), - Constraint::Ratio(0, 6), - ], + Some(_) => DEFAULT_LAYOUT, + None => NO_PREVIEW_LAYOUT, }; let layout_rect = Layout::default() .direction(Direction::Horizontal) @@ -76,6 +70,16 @@ impl TuiBackend { if let Some(curr_list) = curr_list.as_ref() { TuiDirListDetailed::new(&curr_list).render(&mut frame, layout_rect[1]); + if let Some(entry) = curr_list.get_curr_ref() { + let top_rect = Rect { + x: 0, + y: f_size.height - 1, + width: f_size.width, + height: 1, + }; + TuiFooter::new(entry) + .render(&mut frame, top_rect); + } }; if let Some(curr_list) = child_list.as_ref() { diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index e619450..d80d4ca 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,5 +1,11 @@ -pub mod tui_topbar; pub mod tui_dirlist; +pub mod tui_dirlist_detailed; +pub mod tui_footer; +pub mod tui_menu; +pub mod tui_topbar; -pub use self::tui_dirlist::{TuiDirList, TuiDirListDetailed}; +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; pub use self::tui_topbar::TuiTopBar; diff --git a/src/ui/widgets/tui_dirlist.rs b/src/ui/widgets/tui_dirlist.rs index 73425c4..77684c7 100644 --- a/src/ui/widgets/tui_dirlist.rs +++ b/src/ui/widgets/tui_dirlist.rs @@ -6,6 +6,7 @@ use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthChar; use crate::fs::JoshutoDirList; +use crate::util::format; pub struct TuiDirList<'a> { dirlist: &'a JoshutoDirList, @@ -56,123 +57,42 @@ impl<'a> Widget for TuiDirList<'a> { .take(area.height as usize) { let name = entry.file_name(); - let mut style = entry.get_style(); - - if i == screen_index { - style = style.modifier(Modifier::REVERSED); - } let name_width = name.width(); - if name_width < area_width { - buf.set_stringn(x, y + i as u16, - name, - area_width, style); - } else { - match name.rfind('.') { - None => { - buf.set_stringn(x, y + i as u16, - name, - area_width, style); - } - Some(p_ind) => { - let ext_width = name[p_ind..].width(); - let file_name_width = area_width - ext_width - 1; - - buf.set_stringn(x, y + i as u16, - &name[..p_ind], - file_name_width, style); - buf.set_string(x + file_name_width as u16, y + i as u16, - "…", style); - buf.set_string(x + file_name_width as u16 + 1, y + i as u16, - &name[p_ind..], style); - } - } - } - } - } -} - -const FILE_SIZE_WIDTH: usize = 8; - -pub struct TuiDirListDetailed<'a> { - dirlist: &'a JoshutoDirList, -} - -impl<'a> TuiDirListDetailed<'a> { - pub fn new(dirlist: &'a JoshutoDirList) -> Self { - Self { dirlist } - } -} - -impl<'a> Widget for TuiDirListDetailed<'a> { - fn draw(&mut self, area: Rect, buf: &mut Buffer) { - if area.width < 1 || area.height < 1 { - return; - } - - if area.width < 4 { - return; - } - - let x = area.left(); - let y = area.top(); - - let dir_len = self.dirlist.contents.len(); - if dir_len == 0 { - let style = Style::default() - .bg(Color::Red) - .fg(Color::White); - buf.set_stringn(x, y, "empty", area.width as usize, style); - return; - } - - let curr_index = self.dirlist.index.unwrap(); - let skip_dist = curr_index / area.height as usize * area.height as usize; - let screen_index = if skip_dist > 0 { - curr_index % skip_dist - } else { - curr_index - }; - - let area_width = area.width as usize; - for (i, entry) in self - .dirlist - .contents - .iter() - .skip(skip_dist) - .enumerate() - .take(area.height as usize) - { - let name = entry.file_name(); let mut style = entry.get_style(); - if i == screen_index { style = style.modifier(Modifier::REVERSED); } - let file_type = entry.metadata.file_type; - let name_width = name.width(); + let file_type = entry.metadata.file_type; if file_type.is_dir() { - buf.set_stringn(x, y + i as u16, - name, - area_width, style); + if name_width <= area_width { + buf.set_stringn(x, y + i as u16, + name, + area_width, style); + } else { + buf.set_stringn(x, y + i as u16, + name, + area_width - 1, style); + buf.set_string(x + area_width as u16 - 1, y + i as u16, + "…", style); + } continue; } - - if name_width < area_width - FILE_SIZE_WIDTH { + if name_width < area_width { buf.set_stringn(x, y + i as u16, name, - area_width - FILE_SIZE_WIDTH, style); + area_width, style); } else { match name.rfind('.') { None => { buf.set_stringn(x, y + i as u16, name, - area_width - FILE_SIZE_WIDTH, style); + area_width, style); } Some(p_ind) => { let ext_width = name[p_ind..].width(); - let file_name_width = area_width - FILE_SIZE_WIDTH - ext_width - 1; + let file_name_width = area_width - ext_width - 1; buf.set_stringn(x, y + i as u16, &name[..p_ind], @@ -184,28 +104,6 @@ impl<'a> Widget for TuiDirListDetailed<'a> { } } } - let file_size_string = file_size_to_string(entry.metadata.len as f64); - buf.set_string(x + (area_width - FILE_SIZE_WIDTH) as u16, y + i as u16, - file_size_string, style); } } } - -fn file_size_to_string(mut file_size: f64) -> String { - const FILE_UNITS: [&str; 6] = ["B", "K", "M", "G", "T", "E"]; - const CONV_RATE: f64 = 1024.0; - - let mut index = 0; - while file_size > CONV_RATE { - file_size /= CONV_RATE; - index += 1; - } - - if file_size >= 100.0 { - format!("{:>4.0} {}", file_size, FILE_UNITS[index]) - } else if file_size >= 10.0 { - format!("{:>4.1} {}", file_size, FILE_UNITS[index]) - } else { - format!("{:>4.2} {}", file_size, FILE_UNITS[index]) - } -} diff --git a/src/ui/widgets/tui_dirlist_detailed.rs b/src/ui/widgets/tui_dirlist_detailed.rs new file mode 100644 index 0000000..2e01a26 --- /dev/null +++ b/src/ui/widgets/tui_dirlist_detailed.rs @@ -0,0 +1,117 @@ +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Modifier, Style}; +use tui::widgets::Widget; +use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; + +use crate::fs::JoshutoDirList; +use crate::util::format; + +const FILE_SIZE_WIDTH: usize = 8; + +pub struct TuiDirListDetailed<'a> { + dirlist: &'a JoshutoDirList, +} + +impl<'a> TuiDirListDetailed<'a> { + pub fn new(dirlist: &'a JoshutoDirList) -> Self { + Self { dirlist } + } +} + +impl<'a> Widget for TuiDirListDetailed<'a> { + fn draw(&mut self, area: Rect, buf: &mut Buffer) { + if area.width < 1 || area.height < 1 { + return; + } + + if area.width < 4 { + return; + } + + let x = area.left(); + let y = area.top(); + + let dir_len = self.dirlist.contents.len(); + if dir_len == 0 { + let style = Style::default() + .bg(Color::Red) + .fg(Color::White); + buf.set_stringn(x, y, "empty", area.width as usize, style); + return; + } + + let curr_index = self.dirlist.index.unwrap(); + let skip_dist = curr_index / area.height as usize * area.height as usize; + + let screen_index = if skip_dist > 0 { + curr_index % skip_dist + } else { + curr_index + }; + + let area_width = area.width as usize; + for (i, entry) in self + .dirlist + .contents + .iter() + .skip(skip_dist) + .enumerate() + .take(area.height as usize) + { + let name = entry.file_name(); + let name_width = name.width(); + + let mut style = entry.get_style(); + if i == screen_index { + style = style.modifier(Modifier::REVERSED); + } + + let file_type = entry.metadata.file_type; + if file_type.is_dir() { + if name_width <= area_width { + buf.set_stringn(x, y + i as u16, + name, + area_width, style); + } else { + buf.set_stringn(x, y + i as u16, + name, + area_width - 1, style); + buf.set_string(x + area_width as u16 - 1, y + i as u16, + "…", style); + } + continue; + } + + if name_width < area_width - FILE_SIZE_WIDTH { + buf.set_stringn(x, y + i as u16, + name, + area_width - FILE_SIZE_WIDTH, style); + } else { + match name.rfind('.') { + None => { + buf.set_stringn(x, y + i as u16, + name, + area_width - FILE_SIZE_WIDTH, style); + } + Some(p_ind) => { + let ext_width = name[p_ind..].width(); + let file_name_width = area_width - FILE_SIZE_WIDTH - ext_width - 2; + + buf.set_stringn(x, y + i as u16, + &name[..p_ind], + file_name_width, style); + buf.set_string(x + file_name_width as u16, y + i as u16, + "…", style); + buf.set_string(x + file_name_width as u16 + 1, y + i as u16, + &name[p_ind..], style); + } + } + } + let file_size_string = format::file_size_to_string(entry.metadata.len as f64); + buf.set_string(x + (area_width - FILE_SIZE_WIDTH) as u16, y + i as u16, + file_size_string, style); + } + } +} diff --git a/src/ui/widgets/tui_footer.rs b/src/ui/widgets/tui_footer.rs new file mode 100644 index 0000000..ac34b98 --- /dev/null +++ b/src/ui/widgets/tui_footer.rs @@ -0,0 +1,61 @@ +use std::fs; +use std::path::Path; + +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Modifier, Style}; +use tui::widgets::{Paragraph, Text, Widget}; + +use crate::fs::JoshutoDirEntry; +use crate::util::format; + +use crate::{HOSTNAME, USERNAME}; + +pub struct TuiFooter<'a> { + entry: &'a JoshutoDirEntry, +} + +impl<'a> TuiFooter<'a> { + pub fn new(entry: &'a JoshutoDirEntry) -> Self { + Self { entry } + } +} + +impl<'a> Widget for TuiFooter<'a> { + fn draw(&mut self, area: Rect, buf: &mut Buffer) { + use std::os::unix::fs::PermissionsExt; + + let mode = self.entry.metadata.permissions.mode(); + let mode = format::mode_to_string(mode); + + let mode_style = Style::default() + .fg(Color::Cyan); + + let mtime = self.entry.metadata.modified; + let mtime = format::mtime_to_string(mtime); + + let size = self.entry.metadata.len; + let size = format::file_size_to_string(size as f64); + + let mut text = vec![ + Text::styled(mode, mode_style), + Text::raw(" "), + Text::raw(mtime), + Text::raw(" "), + Text::raw(size), + ]; + + if self.entry.metadata.file_type.is_symlink() { + if let Ok(path) = fs::read_link(self.entry.file_path()) { + text.push(Text::styled(" -> ", mode_style)); + if let Some(s) = path.to_str() { + text.push(Text::styled(s.to_string(), mode_style)); + } + } + } + + Paragraph::new(text.iter()) + .wrap(true) + .draw(area, buf); + } +} diff --git a/src/ui/widgets/tui_menu.rs b/src/ui/widgets/tui_menu.rs new file mode 100644 index 0000000..64100f4 --- /dev/null +++ b/src/ui/widgets/tui_menu.rs @@ -0,0 +1,162 @@ +use std::io::{self, Write}; +use std::iter::Iterator; + +use tui::buffer::Buffer; +use termion::clear; +use termion::cursor::Goto; +use termion::event::Key; +use termion::raw::IntoRawMode; +use termion::screen::AlternateScreen; +use tui::backend::TermionBackend; +use tui::layout::{Constraint, Direction, Layout, Rect}; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; +use tui::Terminal; +use unicode_width::UnicodeWidthStr; + +use crate::commands::{CommandKeybind, CursorMoveUp, JoshutoCommand, JoshutoRunnable}; +use crate::config::JoshutoCommandMapping; +use crate::context::JoshutoContext; +use crate::ui::TuiBackend; +use crate::util::event::{Event, Events}; +use super::{TuiDirList, TuiDirListDetailed, TuiTopBar}; + +use crate::{HOSTNAME, USERNAME}; +use super::super::{DEFAULT_LAYOUT, NO_PREVIEW_LAYOUT}; + +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: &JoshutoContext, m: &'a JoshutoCommandMapping) -> + Option<&'a Box<JoshutoCommand>> { + let mut map: &JoshutoCommandMapping = &m; + let events = &context.events; + + let curr_tab = context.curr_tab_ref(); + + let curr_list = curr_tab.curr_list_ref(); + let parent_list = curr_tab.parent_list_ref(); + let child_list = curr_tab.child_list_ref(); + + loop { + backend.terminal.draw(|mut frame| { + let f_size = frame.size(); + + { + let top_rect = Rect { + x: 0, + y: 0, + width: f_size.width, + height: 1, + }; + + TuiTopBar::new(curr_tab.curr_path.as_path()) + .render(&mut frame, top_rect); + } + + let constraints = match child_list { + Some(_) => DEFAULT_LAYOUT, + None => NO_PREVIEW_LAYOUT, + }; + let layout_rect = Layout::default() + .direction(Direction::Horizontal) + .margin(1) + .constraints(constraints.as_ref()) + .split(f_size); + + if let Some(curr_list) = parent_list.as_ref() { + TuiDirList::new(&curr_list).render(&mut frame, layout_rect[0]); + }; + + if let Some(curr_list) = curr_list.as_ref() { + TuiDirListDetailed::new(&curr_list).render(&mut frame, layout_rect[1]); + }; + + if let Some(curr_list) = child_list.as_ref() { + TuiDirList::new(&curr_list).render(&mut frame, layout_rect[2]); + }; + + { + // draw menu + let mut display_vec: Vec<String> = map + .iter() + .map(|(k, v)| { + format!(" {:?} {}", k, 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: y, + width: f_size.width, + height: (display_str_len + BORDER_HEIGHT) as u16, + }; + + TuiMenu::new(&display_str).render(&mut frame, menu_rect); + } + }); + if let Ok(event) = events.next() { + match event { + Event::Input(Key::Esc) => { + return None; + } + Event::Input(key) => { + match map.get(&key) { + Some(CommandKeybind::SimpleKeybind(s)) => { + return Some(s); + } + Some(CommandKeybind::CompositeKeybind(m)) => { + map = m; + } + None => return None, + } + } + _ => {}, + } + } + } + + } +} + +pub struct TuiMenu<'a> { + options: &'a Vec<&'a str>, +} + +impl<'a> TuiMenu<'a> { + pub fn new(options: &'a Vec<&str>) -> Self { + Self { options } + } +} + +impl<'a> Widget for TuiMenu<'a> { + fn draw(&mut self, area: Rect, buf: &mut Buffer) { + let text_iter = self.options.iter().map(|s| Text::raw(*s)); + let block = Block::default() + .borders(Borders::TOP); + + List::new(text_iter) + .block(block) + .draw(area, buf); + } +} |