diff --git a/src/bin.rs b/src/bin.rs index c879094b..fb4957b6 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -332,7 +332,7 @@ fn run_app(opt: Opt) -> Result<()> { &state.context, )); - let status_bar = Box::new(StatusBar::new(window)); + let status_bar = Box::new(StatusBar::new(&state.context, window)); state.register_component(status_bar); #[cfg(feature = "dbus-notifications")] diff --git a/src/command.rs b/src/command.rs index 6c6f56b4..7bb79d88 100644 --- a/src/command.rs +++ b/src/command.rs @@ -751,6 +751,19 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str Ok((input, PrintSetting(setting.to_string()))) } ) + }, + { tags: ["toggle mouse"], + desc: "toggle mouse support", + tokens: &[One(Literal("toggle")), One(Literal("mouse"))], + parser:( + fn toggle_mouse(input: &[u8]) -> IResult<&[u8], Action> { + let (input, _) = tag("toggle")(input)?; + let (input, _) = is_a(" ")(input)?; + let (input, _) = tag("mouse")(input)?; + let (input, _) = eof(input)?; + Ok((input, ToggleMouse)) + } + ) } ]); @@ -843,6 +856,7 @@ pub fn parse_command(input: &[u8]) -> Result { rename_mailbox, account_action, print_setting, + toggle_mouse, ))(input) .map(|(_, v)| v) .map_err(|err| err.into()) diff --git a/src/command/actions.rs b/src/command/actions.rs index 0dd97e68..ee14e579 100644 --- a/src/command/actions.rs +++ b/src/command/actions.rs @@ -120,6 +120,7 @@ pub enum Action { Mailbox(AccountName, MailboxOperation), AccountAction(AccountName, AccountAction), PrintSetting(String), + ToggleMouse, } impl Action { @@ -139,6 +140,7 @@ impl Action { Action::Mailbox(_, _) => true, Action::AccountAction(_, _) => false, Action::PrintSetting(_) => false, + Action::ToggleMouse => false, } } } diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index 698a7f73..acce08c9 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -466,6 +466,7 @@ pub struct Listing { cmd_buf: String, /// This is the width of the right container to the entire width. ratio: usize, // right/(container width) * 100 + menu_width: WidgetWidth, focus: ListingFocus, } @@ -494,7 +495,25 @@ impl Component for Listing { let total_cols = get_x(bottom_right) - get_x(upper_left); let right_component_width = if self.menu_visibility { - (self.ratio * total_cols) / 100 + if self.focus == ListingFocus::Menu { + (self.ratio * total_cols) / 100 + } else { + match self.menu_width { + WidgetWidth::Set(ref mut v) | WidgetWidth::Hold(ref mut v) => { + if *v == 0 { + *v = 1; + } else if *v >= total_cols { + *v = total_cols.saturating_sub(2); + } + total_cols.saturating_sub(*v) + } + WidgetWidth::Unset => { + self.menu_width = + WidgetWidth::Set(total_cols - ((self.ratio * total_cols) / 100)); + (self.ratio * total_cols) / 100 + } + } + } } else { total_cols }; @@ -570,7 +589,7 @@ impl Component for Listing { } } UIEvent::AccountStatusChange(account_hash) => { - let account_index = context + let account_index: usize = context .accounts .get_index_of(account_hash) .expect("Invalid account_hash in UIEventMailbox{Delete,Create}"); @@ -631,6 +650,44 @@ impl Component for Listing { let shortcuts = self.get_shortcuts(context); if self.focus == ListingFocus::Mailbox { match *event { + UIEvent::Input(Key::Mouse(MouseEvent::Press(MouseButton::Left, x, _y))) + if self.menu_visibility => + { + match self.menu_width { + WidgetWidth::Hold(wx) | WidgetWidth::Set(wx) + if wx + 1 == usize::from(x) => + { + self.menu_width = WidgetWidth::Hold(wx - 1); + } + WidgetWidth::Set(_) => return false, + WidgetWidth::Hold(x) => { + self.menu_width = WidgetWidth::Set(x); + } + WidgetWidth::Unset => return false, + } + self.set_dirty(true); + return true; + } + UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.menu_visibility => { + match self.menu_width { + WidgetWidth::Hold(ref mut hx) => { + *hx = usize::from(x).saturating_sub(1); + } + _ => return false, + } + self.set_dirty(true); + return true; + } + UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y))) if self.menu_visibility => { + match self.menu_width { + WidgetWidth::Hold(_) => { + self.menu_width = WidgetWidth::Set(usize::from(x).saturating_sub(1)); + } + _ => return false, + } + self.set_dirty(true); + return true; + } UIEvent::Input(Key::Left) if self.menu_visibility => { self.focus = ListingFocus::Menu; self.ratio = 50; @@ -1281,6 +1338,7 @@ impl Listing { show_divider: false, menu_visibility: true, ratio: 90, + menu_width: WidgetWidth::Unset, focus: ListingFocus::Mailbox, cmd_buf: String::with_capacity(4), }; diff --git a/src/components/utilities.rs b/src/components/utilities.rs index f3636c28..146010df 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -629,10 +629,12 @@ impl Component for Pager { pub struct StatusBar { container: Box, status: String, + status_message: String, ex_buffer: Field, ex_buffer_cmd_history_pos: Option, display_buffer: String, mode: UIMode, + mouse: bool, height: usize, dirty: bool, id: ComponentId, @@ -651,15 +653,17 @@ impl fmt::Display for StatusBar { } impl StatusBar { - pub fn new(container: Box) -> Self { + pub fn new(context: &Context, container: Box) -> Self { StatusBar { container, status: String::with_capacity(256), + status_message: String::with_capacity(256), ex_buffer: Field::Text(UText::new(String::with_capacity(256)), None), ex_buffer_cmd_history_pos: None, display_buffer: String::with_capacity(8), dirty: true, mode: UIMode::Normal, + mouse: context.settings.terminal.use_mouse.is_true(), height: 1, id: ComponentId::new_v4(), auto_complete: AutoComplete::new(Vec::new()), @@ -1016,7 +1020,19 @@ impl Component for StatusBar { match event { UIEvent::ChangeMode(m) => { let offset = self.status.find('|').unwrap_or_else(|| self.status.len()); - self.status.replace_range(..offset, &format!("{} ", m)); + self.status.replace_range(..offset, &format!("{} {}", m, + if self.mouse { + context + .settings + .terminal + .mouse_flag + .as_ref() + .map(|s| s.as_str()) + .unwrap_or("🖱️ ") + } else { + "" + }, + )); self.set_dirty(true); self.container.set_dirty(true); self.mode = *m; @@ -1175,7 +1191,44 @@ impl Component for StatusBar { self.dirty = true; } UIEvent::StatusEvent(StatusEvent::UpdateStatus(ref mut s)) => { - self.status = format!("{} | {}", self.mode, std::mem::replace(s, String::new())); + self.status_message.clear(); + self.status_message.push_str(s.as_str()); + self.status = format!( + "{} {}| {}", + self.mode, + if self.mouse { + context + .settings + .terminal + .mouse_flag + .as_ref() + .map(|s| s.as_str()) + .unwrap_or("🖱️ ") + } else { + "" + }, + &self.status_message, + ); + self.dirty = true; + } + UIEvent::StatusEvent(StatusEvent::SetMouse(val)) => { + self.mouse = *val; + self.status = format!( + "{} {}| {}", + self.mode, + if self.mouse { + context + .settings + .terminal + .mouse_flag + .as_ref() + .map(|s| s.as_str()) + .unwrap_or("🖱️ ") + } else { + "" + }, + &self.status_message, + ); self.dirty = true; } UIEvent::StatusEvent(StatusEvent::JobCanceled(ref job_id)) diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index 577e084e..1dd54259 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -70,6 +70,11 @@ pub struct PagerSettingsOverride { #[serde(alias = "minimum-width")] #[serde(default)] pub minimum_width: Option, + #[doc = " Maximum text width in columns."] + #[doc = " Default: None"] + #[serde(alias = "minimum-width")] + #[serde(default)] + pub max_width: Option>, #[doc = " Choose `text/html` alternative if `text/plain` is empty in `multipart/alternative`"] #[doc = " attachments."] #[doc = " Default: true"] @@ -89,6 +94,7 @@ impl Default for PagerSettingsOverride { format_flowed: None, split_long_lines: None, minimum_width: None, + max_width: None, auto_choose_multipart_alternative: None, } } diff --git a/src/conf/pager.rs b/src/conf/pager.rs index 99754037..b4f09f44 100644 --- a/src/conf/pager.rs +++ b/src/conf/pager.rs @@ -79,6 +79,11 @@ pub struct PagerSettings { #[serde(default = "eighty_val", alias = "minimum-width")] pub minimum_width: usize, + /// Maximum text width in columns. + /// Default: None + #[serde(default = "none", alias = "minimum-width")] + pub max_width: Option, + /// Choose `text/html` alternative if `text/plain` is empty in `multipart/alternative` /// attachments. /// Default: true @@ -101,6 +106,7 @@ impl Default for PagerSettings { format_flowed: true, split_long_lines: true, minimum_width: 80, + max_width: None, auto_choose_multipart_alternative: ToggleFlag::InternalVal(true), } } @@ -121,6 +127,7 @@ impl DotAddressable for PagerSettings { "format_flowed" => self.format_flowed.lookup(field, tail), "split_long_lines" => self.split_long_lines.lookup(field, tail), "minimum_width" => self.minimum_width.lookup(field, tail), + "max_width" => self.max_width.lookup(field, tail), "auto_choose_multipart_alternative" => { self.auto_choose_multipart_alternative.lookup(field, tail) } diff --git a/src/conf/terminal.rs b/src/conf/terminal.rs index 2b6c94e5..2138f171 100644 --- a/src/conf/terminal.rs +++ b/src/conf/terminal.rs @@ -35,6 +35,14 @@ pub struct TerminalSettings { pub themes: Themes, pub ascii_drawing: bool, pub use_color: ToggleFlag, + /// Use mouse events. This will disable text selection, but you will be able to resize some + /// widgets. + /// Default: False + pub use_mouse: ToggleFlag, + /// String to show in status bar if mouse is active. + /// Default: "🖱️ " + #[serde(deserialize_with = "non_empty_string")] + pub mouse_flag: Option, #[serde(deserialize_with = "non_empty_string")] pub window_title: Option, #[serde(deserialize_with = "non_empty_string")] @@ -48,6 +56,8 @@ impl Default for TerminalSettings { themes: Themes::default(), ascii_drawing: false, use_color: ToggleFlag::InternalVal(true), + use_mouse: ToggleFlag::InternalVal(false), + mouse_flag: Some("🖱️ ".to_string()), window_title: Some("meli".to_string()), file_picker_command: None, } @@ -76,6 +86,8 @@ impl DotAddressable for TerminalSettings { "themes" => Err(MeliError::new("unimplemented")), "ascii_drawing" => self.ascii_drawing.lookup(field, tail), "use_color" => self.use_color.lookup(field, tail), + "use_mouse" => self.use_mouse.lookup(field, tail), + "mouse_flag" => self.mouse_flag.lookup(field, tail), "window_title" => self.window_title.lookup(field, tail), "file_picker_command" => self.file_picker_command.lookup(field, tail), other => Err(MeliError::new(format!( diff --git a/src/state.rs b/src/state.rs index 2d0fbee8..88536b15 100644 --- a/src/state.rs +++ b/src/state.rs @@ -178,6 +178,7 @@ pub struct State { overlay_grid: CellBuffer, draw_rate_limit: RateLimit, stdout: Option, + mouse: bool, child: Option, draw_horizontal_segment_fn: fn(&mut CellBuffer, &mut StateStdout, usize, usize, usize) -> (), pub mode: UIMode, @@ -318,6 +319,7 @@ impl State { grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), overlay_grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), stdout: None, + mouse: settings.terminal.use_mouse.is_true(), child: None, mode: UIMode::Normal, components: Vec::with_capacity(8), @@ -419,13 +421,16 @@ impl State { /// Switch back to the terminal's main screen (The command line the user sees before opening /// the application) pub fn switch_to_main_screen(&mut self) { + let mouse = self.mouse; write!( self.stdout(), - "{}{}{}{}", + "{}{}{}{}{disable_sgr_mouse}{disable_mouse}", termion::screen::ToMainScreen, cursor::Show, RestoreWindowTitleIconFromStack, BracketModeEnd, + disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" }, + disable_mouse = if mouse { DisableMouse.as_ref() } else { "" }, ) .unwrap(); self.flush(); @@ -439,7 +444,7 @@ impl State { write!( &mut stdout, - "{save_title_to_stack}{}{}{}{window_title}{}{}", + "{save_title_to_stack}{}{}{}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}", termion::screen::ToAlternateScreen, cursor::Hide, clear::All, @@ -451,6 +456,12 @@ impl State { } else { String::new() }, + enable_mouse = if self.mouse { EnableMouse.as_ref() } else { "" }, + enable_sgr_mouse = if self.mouse { + EnableSGRMouse.as_ref() + } else { + "" + }, ) .unwrap(); @@ -458,6 +469,27 @@ impl State { self.flush(); } + pub fn set_mouse(&mut self, value: bool) { + if let Some(stdout) = self.stdout.as_mut() { + write!( + stdout, + "{mouse}{sgr_mouse}", + mouse = if value { + AsRef::::as_ref(&EnableMouse) + } else { + AsRef::::as_ref(&DisableMouse) + }, + sgr_mouse = if value { + AsRef::::as_ref(&EnableSGRMouse) + } else { + AsRef::::as_ref(&DisableSGRMouse) + }, + ) + .unwrap(); + } + self.flush(); + } + pub fn receiver(&self) -> Receiver { self.context.receiver.clone() } @@ -945,6 +977,11 @@ impl State { .unwrap_or_else(|err| err.to_string()) )))); } + ToggleMouse => { + self.mouse = !self.mouse; + self.set_mouse(self.mouse); + self.rcv_event(UIEvent::StatusEvent(StatusEvent::SetMouse(self.mouse))); + } v => { self.rcv_event(UIEvent::Action(v)); } diff --git a/src/terminal.rs b/src/terminal.rs index 43ada7a4..3114413d 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -78,16 +78,25 @@ macro_rules! derive_csi_sequence { }; } -/* derive_csi_sequence!( - #[doc = ""] - (DisableMouse, "?1000l") + ///Ps = 1 0 0 2 ⇒ Don't use Cell Motion Mouse Tracking, xterm + (DisableMouse, "?1002l") ); + derive_csi_sequence!( - #[doc = ""] - (EnableMouse, "?1000h") + ///Ps = 1 0 0 2 ⇒ Use Cell Motion Mouse Tracking, xterm + (EnableMouse, "?1002h") +); + +derive_csi_sequence!( + ///Ps = 1 0 0 6 Enable SGR Mouse Mode, xterm. + (EnableSGRMouse, "?1006h") +); + +derive_csi_sequence!( + ///Ps = 1 0 0 6 Disable SGR Mouse Mode, xterm. + (DisableSGRMouse, "?1006l") ); -*/ derive_csi_sequence!( #[doc = "`CSI Ps ; Ps ; Ps t`, where `Ps = 2 2 ; 0` -> Save xterm icon and window title on stack."] diff --git a/src/terminal/cells.rs b/src/terminal/cells.rs index 62ea8a13..7386505f 100644 --- a/src/terminal/cells.rs +++ b/src/terminal/cells.rs @@ -2826,3 +2826,10 @@ impl core::cmp::PartialOrd for FormatTag { Some(self.cmp(&other)) } } + +#[derive(Debug, Copy, Hash, Clone, PartialEq, Eq)] +pub enum WidgetWidth { + Unset, + Hold(usize), + Set(usize), +} diff --git a/src/terminal/keys.rs b/src/terminal/keys.rs index 51ba61a0..bdb4f456 100644 --- a/src/terminal/keys.rs +++ b/src/terminal/keys.rs @@ -65,9 +65,13 @@ pub enum Key { Null, /// Esc key. Esc, + Mouse(termion::event::MouseEvent), Paste(String), } +pub use termion::event::MouseButton; +pub use termion::event::MouseEvent; + impl fmt::Display for Key { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use crate::Key::*; @@ -93,6 +97,7 @@ impl fmt::Display for Key { PageDown => write!(f, "PageDown"), Delete => write!(f, "Delete"), Insert => write!(f, "Insert"), + Mouse(_) => write!(f, "Mouse"), } } } @@ -205,6 +210,10 @@ pub fn get_events( closure((ret, buf)); continue 'poll_while; } + (Ok((TermionEvent::Mouse(mev), bytes)), InputMode::Normal) => { + closure((Key::Mouse(mev), bytes)); + continue 'poll_while; + } _ => { continue 'poll_while; } // Mouse events or errors. @@ -342,6 +351,7 @@ impl Serialize for Key { Key::Alt(c) => serializer.serialize_str(&format!("M-{}", c)), Key::Ctrl(c) => serializer.serialize_str(&format!("C-{}", c)), Key::Null => serializer.serialize_str("Null"), + Key::Mouse(_) => unreachable!(), Key::Paste(s) => serializer.serialize_str(s), } } diff --git a/src/types.rs b/src/types.rs index 5d517d2b..5c690c57 100644 --- a/src/types.rs +++ b/src/types.rs @@ -55,6 +55,7 @@ pub enum StatusEvent { NewJob(JobId), JobFinished(JobId), JobCanceled(JobId), + SetMouse(bool), } /// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads