Browse Source

Add opt-in mouse support

Sidebar width can be resized with mouse hold and drag.
jmap-eventsource
Manos Pitsidianakis 1 year ago
parent
commit
188e020bd1
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 2
      src/bin.rs
  2. 14
      src/command.rs
  3. 2
      src/command/actions.rs
  4. 62
      src/components/mail/listing.rs
  5. 59
      src/components/utilities.rs
  6. 6
      src/conf/overrides.rs
  7. 7
      src/conf/pager.rs
  8. 12
      src/conf/terminal.rs
  9. 41
      src/state.rs
  10. 21
      src/terminal.rs
  11. 7
      src/terminal/cells.rs
  12. 10
      src/terminal/keys.rs
  13. 1
      src/types.rs

2
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")]

14
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<Action, MeliError> {
rename_mailbox,
account_action,
print_setting,
toggle_mouse,
))(input)
.map(|(_, v)| v)
.map_err(|err| err.into())

2
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,
}
}
}

62
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),
};

59
src/components/utilities.rs

@ -629,10 +629,12 @@ impl Component for Pager {
pub struct StatusBar {
container: Box<dyn Component>,
status: String,
status_message: String,
ex_buffer: Field,
ex_buffer_cmd_history_pos: Option<usize>,
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<dyn Component>) -> Self {
pub fn new(context: &Context, container: Box<dyn Component>) -> 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))

6
src/conf/overrides.rs

@ -70,6 +70,11 @@ pub struct PagerSettingsOverride {
#[serde(alias = "minimum-width")]
#[serde(default)]
pub minimum_width: Option<usize>,
#[doc = " Maximum text width in columns."]
#[doc = " Default: None"]
#[serde(alias = "minimum-width")]
#[serde(default)]
pub max_width: Option<Option<usize>>,
#[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,
}
}

7
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<usize>,
/// 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)
}

12
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<String>,
#[serde(deserialize_with = "non_empty_string")]
pub window_title: Option<String>,
#[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!(

41
src/state.rs

@ -178,6 +178,7 @@ pub struct State {
overlay_grid: CellBuffer,
draw_rate_limit: RateLimit,
stdout: Option<StateStdout>,
mouse: bool,
child: Option<ForkType>,
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::<str>::as_ref(&EnableMouse)
} else {
AsRef::<str>::as_ref(&DisableMouse)
},
sgr_mouse = if value {
AsRef::<str>::as_ref(&EnableSGRMouse)
} else {
AsRef::<str>::as_ref(&DisableSGRMouse)
},
)
.unwrap();
}
self.flush();
}
pub fn receiver(&self) -> Receiver<ThreadEvent> {
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));
}

21
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!(
///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!(
#[doc = ""]
(EnableMouse, "?1000h")
///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."]

7
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),
}

10
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),
}
}

1
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

Loading…
Cancel
Save