mirror of
https://git.meli.delivery/meli/meli
synced 2024-10-30 21:20:34 +00:00
Add opt-in mouse support
Sidebar width can be resized with mouse hold and drag.
This commit is contained in:
parent
20840625d6
commit
188e020bd1
@ -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")]
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
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));
|
||||
}
|
||||
|
@ -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."]
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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…
Reference in New Issue
Block a user