From 4de0e5241874154b2b664c35bbff815223fcea84 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Mon, 1 Jun 2020 22:02:40 +0200 Subject: [PATCH] Support more commands (#101) closes #83 --- Cargo.lock | 1 + Cargo.toml | 1 + src/app.rs | 67 +++++------------- src/cmdbar.rs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ src/keys.rs | 1 + src/main.rs | 1 + src/strings.rs | 4 +- src/ui/style.rs | 11 ++- 8 files changed, 209 insertions(+), 53 deletions(-) create mode 100644 src/cmdbar.rs diff --git a/Cargo.lock b/Cargo.lock index 6fe0fe23..ce411e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,7 @@ dependencies = [ "serde", "simplelog", "tui", + "unicode-width", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 63c04de9..b55ea7a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ backtrace = "0.3" ron = "0.6" serde = "1.0" anyhow = "1.0.31" +unicode-width = "0.1" [features] default=[] diff --git a/src/app.rs b/src/app.rs index 73a08ee6..da19aa49 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::{ accessors, + cmdbar::CommandBar, components::{ event_pump, CommandBlocking, CommandInfo, CommitComponent, Component, DrawableComponent, HelpComponent, MsgComponent, @@ -15,15 +16,13 @@ use anyhow::{anyhow, Result}; use asyncgit::{sync, AsyncNotification, CWD}; use crossbeam_channel::Sender; use crossterm::event::Event; -use itertools::Itertools; -use std::borrow::Cow; use strings::commands; use tui::{ backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, + layout::{Constraint, Direction, Layout, Rect}, style::Modifier, style::Style, - widgets::{Block, Borders, Paragraph, Tabs, Text}, + widgets::{Block, Borders, Tabs}, Frame, }; /// @@ -34,7 +33,7 @@ pub struct App { reset: ResetComponent, commit: CommitComponent, stashmsg_popup: StashMsgComponent, - current_commands: Vec, + cmdbar: CommandBar, tab: usize, revlog: Revlog, status_tab: Status, @@ -60,7 +59,7 @@ impl App { &theme, ), do_quit: false, - current_commands: Vec::new(), + cmdbar: CommandBar::new(&theme), help: HelpComponent::new(&theme), msg: MsgComponent::new(&theme), tab: 0, @@ -78,17 +77,23 @@ impl App { &mut self, f: &mut Frame, ) -> Result<()> { + let fsize = f.size(); + + self.cmdbar.refresh_width(fsize.width); + let chunks_main = Layout::default() .direction(Direction::Vertical) .constraints( [ Constraint::Length(2), Constraint::Min(2), - Constraint::Length(1), + Constraint::Length(self.cmdbar.height()), ] .as_ref(), ) - .split(f.size()); + .split(fsize); + + self.cmdbar.draw(f, chunks_main[2]); self.draw_tabs(f, chunks_main[0]); @@ -101,13 +106,6 @@ impl App { _ => return Err(anyhow!("unknown tab")), }; - Self::draw_commands( - f, - chunks_main[2], - self.current_commands.as_slice(), - self.theme, - ); - self.draw_popups(f)?; Ok(()) @@ -135,6 +133,10 @@ impl App { self.toggle_tabs(true)?; NeedsUpdate::COMMANDS } + keys::CMD_BAR_TOGGLE => { + self.cmdbar.toggle_more(); + NeedsUpdate::empty() + } _ => NeedsUpdate::empty(), }; @@ -267,8 +269,7 @@ impl App { fn update_commands(&mut self) { self.help.set_cmds(self.commands(true)); - self.current_commands = self.commands(false); - self.current_commands.sort_by_key(|e| e.order); + self.cmdbar.set_cmds(self.commands(false)); } fn process_queue(&mut self) -> Result { @@ -417,36 +418,4 @@ impl App { r, ); } - - fn draw_commands( - f: &mut Frame, - r: Rect, - cmds: &[CommandInfo], - theme: Theme, - ) { - let splitter = Text::Styled( - Cow::from(strings::CMD_SPLITTER), - Style::default(), - ); - - let texts = cmds - .iter() - .filter_map(|c| { - if c.show_in_quickbar() { - Some(Text::Styled( - Cow::from(c.text.name), - theme.toolbar(c.enabled), - )) - } else { - None - } - }) - .collect::>(); - - f.render_widget( - Paragraph::new(texts.iter().intersperse(&splitter)) - .alignment(Alignment::Left), - r, - ); - } } diff --git a/src/cmdbar.rs b/src/cmdbar.rs new file mode 100644 index 00000000..7f90c6ab --- /dev/null +++ b/src/cmdbar.rs @@ -0,0 +1,176 @@ +use crate::{components::CommandInfo, strings, ui::style::Theme}; +use std::borrow::Cow; +use tui::{ + backend::Backend, + layout::{Alignment, Rect}, + widgets::{Paragraph, Text}, + Frame, +}; +use unicode_width::UnicodeWidthStr; + +enum DrawListEntry { + LineBreak, + Splitter, + Command(Command), +} + +struct Command { + txt: String, + enabled: bool, + line: usize, +} + +/// helper to be used while drawing +pub struct CommandBar { + draw_list: Vec, + cmd_infos: Vec, + theme: Theme, + lines: u16, + width: u16, + expandable: bool, + expanded: bool, +} + +const MORE_WIDTH: u16 = 11; + +impl CommandBar { + pub const fn new(theme: &Theme) -> Self { + Self { + draw_list: Vec::new(), + cmd_infos: Vec::new(), + theme: *theme, + lines: 0, + width: 0, + expandable: false, + expanded: false, + } + } + + pub fn refresh_width(&mut self, width: u16) { + if width != self.width { + self.refresh_list(width); + self.width = width; + } + } + + fn is_multiline(&self, width: u16) -> bool { + let mut line_width = 0_usize; + for c in &self.cmd_infos { + let entry_w = UnicodeWidthStr::width(c.text.name); + + if line_width + entry_w > width as usize { + return true; + } + + line_width += entry_w + 1; + } + + false + } + + fn refresh_list(&mut self, width: u16) { + self.draw_list.clear(); + + let width = if self.is_multiline(width) { + width.saturating_sub(MORE_WIDTH) + } else { + width + }; + + let mut line_width = 0_usize; + let mut lines = 1_u16; + + for c in &self.cmd_infos { + let entry_w = UnicodeWidthStr::width(c.text.name); + + if line_width + entry_w > width as usize { + self.draw_list.push(DrawListEntry::LineBreak); + line_width = 0; + lines += 1; + } else if line_width > 0 { + self.draw_list.push(DrawListEntry::Splitter); + } + + line_width += entry_w + 1; + + self.draw_list.push(DrawListEntry::Command(Command { + txt: c.text.name.to_string(), + enabled: c.enabled, + line: lines.saturating_sub(1) as usize, + })); + } + + self.expandable = lines > 1; + + self.lines = lines; + } + + pub fn set_cmds(&mut self, cmds: Vec) { + self.cmd_infos = cmds + .into_iter() + .filter(CommandInfo::show_in_quickbar) + .collect::>(); + self.cmd_infos.sort_by_key(|e| e.order); + self.refresh_list(self.width); + } + + pub fn height(&self) -> u16 { + if self.expandable && self.expanded { + self.lines + } else { + 1_u16 + } + } + + pub fn toggle_more(&mut self) { + if self.expandable { + self.expanded = !self.expanded; + } + } + + pub fn draw(&self, f: &mut Frame, r: Rect) { + let splitter = Text::Raw(Cow::from(strings::CMD_SPLITTER)); + + let texts = self + .draw_list + .iter() + .map(|c| match c { + DrawListEntry::Command(c) => Text::Styled( + Cow::from(c.txt.as_str()), + self.theme.commandbar(c.enabled, c.line), + ), + DrawListEntry::LineBreak => { + Text::Raw(Cow::from("\n")) + } + DrawListEntry::Splitter => splitter.clone(), + }) + .collect::>(); + + f.render_widget( + Paragraph::new(texts.iter()).alignment(Alignment::Left), + r, + ); + + if self.expandable { + let r = Rect::new( + r.width.saturating_sub(MORE_WIDTH), + r.y + r.height.saturating_sub(1), + MORE_WIDTH, + 1, + ); + + f.render_widget( + Paragraph::new( + vec![Text::Raw(Cow::from(if self.expanded { + "less [.]" + } else { + "more [.]" + }))] + .iter(), + ) + .alignment(Alignment::Right), + r, + ); + } + } +} diff --git a/src/keys.rs b/src/keys.rs index 340d3b9a..90533b7b 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -50,3 +50,4 @@ pub const STASHING_TOGGLE_INDEX: KeyEvent = pub const STASH_APPLY: KeyEvent = no_mod(KeyCode::Enter); pub const STASH_DROP: KeyEvent = with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT); +pub const CMD_BAR_TOGGLE: KeyEvent = no_mod(KeyCode::Char('.')); diff --git a/src/main.rs b/src/main.rs index 26f9a57a..22db83e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ #![allow(clippy::module_name_repetitions)] mod app; +mod cmdbar; mod components; mod keys; mod poll; diff --git a/src/strings.rs b/src/strings.rs index f2b0044c..4aa8f742 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -63,7 +63,7 @@ pub mod commands { ); /// pub static DIFF_HOME_END: CommandText = CommandText::new( - "Jump up/down [home,end,shift+up,shift+down]", + "Jump up/down [home,end,\u{11014}up,\u{2191}down]", "scroll to top or bottom of diff", CMD_GROUP_DIFF, ); @@ -155,7 +155,7 @@ pub mod commands { ); /// pub static QUIT: CommandText = CommandText::new( - "Quit [ctrl+c]", + "Quit [^c]", "quit gitui application", CMD_GROUP_GENERAL, ); diff --git a/src/ui/style.rs b/src/ui/style.rs index 689e45e3..64948b17 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -22,6 +22,8 @@ pub struct Theme { #[serde(with = "ColorDef")] selection_bg: Color, #[serde(with = "ColorDef")] + cmdbar_extra_lines_bg: Color, + #[serde(with = "ColorDef")] disabled_fg: Color, #[serde(with = "ColorDef")] diff_line_add: Color, @@ -153,13 +155,17 @@ impl Theme { Style::default().fg(self.danger_fg) } - pub fn toolbar(&self, enabled: bool) -> Style { + pub fn commandbar(&self, enabled: bool, line: usize) -> Style { if enabled { Style::default().fg(self.command_fg) } else { Style::default().fg(self.disabled_fg) } - .bg(self.selection_bg) + .bg(if line == 0 { + self.selection_bg + } else { + self.cmdbar_extra_lines_bg + }) } pub fn commit_hash(&self, selected: bool) -> Style { @@ -225,6 +231,7 @@ impl Default for Theme { selected_tab: Color::Yellow, command_fg: Color::White, selection_bg: Color::Rgb(0, 0, 100), + cmdbar_extra_lines_bg: Color::Rgb(0, 0, 80), disabled_fg: Color::DarkGray, diff_line_add: Color::Green, diff_line_delete: Color::Red,