summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--Cargo.lock24
-rw-r--r--Cargo.toml7
-rw-r--r--src/app.rs49
-rw-r--r--src/browser_states.rs94
-rw-r--r--src/commands.rs19
-rw-r--r--src/event.rs122
-rw-r--r--src/flat_tree.rs15
-rw-r--r--src/main.rs1
9 files changed, 246 insertions, 89 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4e3c8f..278d0d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+<a name="v0.8.3"></a>
+### v0.8.3 - 2019-06-16
+- mouse support: click to select, double-click to open
+
<a name="v0.8.2"></a>
### v0.8.2 - 2019-06-15
- fix wrong result of scrolling when help text fits the screen
diff --git a/Cargo.lock b/Cargo.lock
index 94f997d..58bb654 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -86,11 +86,11 @@ dependencies = [
[[package]]
name = "broot"
-version = "0.8.2"
+version = "0.8.3"
dependencies = [
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossterm 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossterm 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)",
"custom_error 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"directories 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -220,11 +220,11 @@ dependencies = [
[[package]]
name = "crossterm"
-version = "0.9.5"
+version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "crossterm_cursor 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossterm_input 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossterm_cursor 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossterm_input 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"crossterm_screen 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"crossterm_style 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"crossterm_terminal 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -233,7 +233,7 @@ dependencies = [
[[package]]
name = "crossterm_cursor"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"crossterm_utils 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -243,7 +243,7 @@ dependencies = [
[[package]]
name = "crossterm_input"
-version = "0.3.5"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"crossterm_screen 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -278,7 +278,7 @@ name = "crossterm_terminal"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "crossterm_cursor 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossterm_cursor 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"crossterm_utils 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"crossterm_winapi 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -620,7 +620,7 @@ name = "termimad"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
- "crossterm 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossterm 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"minimad 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -745,9 +745,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4"
"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
"checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c"
-"checksum crossterm 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3170cf0815299f3507810e4588f3addf073f2869e150375493f588877ba46c"
-"checksum crossterm_cursor 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3f8283b7f36402121adea49e1eb851f068eb2741a0a59be3aa8078e31e5c47f6"
-"checksum crossterm_input 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e225c90aa5ee122602398a309e74aa6e57f559497e96f90eff4437bdf2951f86"
+"checksum crossterm 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)" = "21ac79357981b3c35917a377e6138729b66316db7649f9f96fbb517bb02361e5"
+"checksum crossterm_cursor 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4b8ddb43937bfafbe07d349ee9497754ceac818ee872116afccb076f2de28d3d"
+"checksum crossterm_input 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a23a71b51ddc8f74e13e341179b1a26b20f0030d14ff8fbdd9da45fd0e342bc5"
"checksum crossterm_screen 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "90889b9f1d7867a583dede34deab1e32a10379e9eb70d920ca7895e144aa6d65"
"checksum crossterm_style 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "983596405fe738aac9645656b666073fe6e0a8bf088679b7e256916ee41b61f7"
"checksum crossterm_terminal 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "18792c97c5cdcc5fd3582df58188a793bf290af4a53d5fc8442c7d17e003b356"
diff --git a/Cargo.toml b/Cargo.toml
index b439b0f..da2220b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "broot"
-version = "0.8.2"
+version = "0.8.3"
authors = ["dystroy <denys.seguret@gmail.com>"]
repository = "https://github.com/Canop/broot"
description = "Fuzzy Search + tree + cd"
@@ -22,12 +22,13 @@ clap = "2.33"
glob = "0.3"
crossbeam = "0.7"
opener = "0.4"
-crossterm = "0.9.5"
+crossterm = "0.9.6"
termimad = "0.3.6"
#termimad = { path = "../termimad" }
-users = "0.9"
+
[target.'cfg(unix)'.dependencies]
jemallocator = "0.3"
+users = "0.9"
[profile.release]
lto = true # link time optimization - roughly halves the size of the exec
diff --git a/src/app.rs b/src/app.rs
index f13c699..74c46ba 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -7,17 +7,14 @@
//! - an operation which keeps the state
//! - a request to quit broot
//! - a request to launch an executable (thus leaving broot)
-use crossterm::{InputEvent, TerminalInput};
use std::io::{self, Write};
use std::result::Result;
-use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::{mpsc, Arc};
-use std::thread;
use crate::app_context::AppContext;
use crate::browser_states::BrowserState;
use crate::command_parsing::parse_command_sequence;
use crate::commands::Command;
+use crate::event::EventSource;
use crate::errors::ProgramError;
use crate::errors::TreeBuildError;
use crate::external::Launchable;
@@ -222,7 +219,7 @@ impl App {
let mut cmd = Command::new();
// if some commands were passed to the application
- // we execute them before even starting listening for keys
+ // we execute them before even starting listening for events
if let Some(unparsed_commands) = &con.launch_args.commands {
let commands = parse_command_sequence(unparsed_commands, con)?;
for arg_cmd in &commands {
@@ -237,56 +234,26 @@ impl App {
// we listen for events in a separate thread so that we can go on listening
// when a long search is running, and interrupt it if needed
- let (tx_keys, rx_keys) = mpsc::channel();
- let (tx_quit, rx_quit) = mpsc::channel();
- let cmd_count = Arc::new(AtomicUsize::new(0));
- let key_count = Arc::clone(&cmd_count);
- thread::spawn(move || {
- let input = TerminalInput::new();
- let mut crossterm_events = input.read_sync();
- loop {
- if let Some(event) = crossterm_events.next() {
- info!(" => crossterm event={:?}", event);
- if let InputEvent::Keyboard(key) = event {
- key_count.fetch_add(1, Ordering::SeqCst);
- // we send the command to the receiver in the
- // main event loop
- tx_keys.send(key).unwrap();
- let quit = rx_quit.recv().unwrap();
- if quit {
- // cleanly quitting this thread is necessary
- // to ensure stdin is properly closed when
- // we launch an external application in the same
- // terminal
- return;
- }
- } else {
- debug!("disregarding unrelevant event: {:?}", event);
- }
- } else {
- debug!("crossterm events iterator gave us a None"); // happens on windows
- }
- }
- });
+ let event_source = EventSource::new();
screen.write_input(&cmd)?;
screen.write_status_text("Hit <esc> to quit, '?' for help, or some letters to search")?;
self.state().write_flags(&mut screen, con)?;
loop {
if !self.quitting {
- self.do_pending_tasks(&cmd, &mut screen, con, TaskLifetime::new(&cmd_count))?;
+ self.do_pending_tasks(&cmd, &mut screen, con, event_source.new_task_lifetime())?;
}
- let k = match rx_keys.recv() {
- Ok(k) => k,
+ let event = match event_source.recv() {
+ Ok(event) => event,
Err(_) => {
// this is how we quit the application,
// when the input thread is properly closed
break;
}
};
- cmd.add_key(k);
+ cmd.add_event(event);
cmd = self.apply_command(cmd, &mut screen, con)?;
- tx_quit.send(self.quitting).unwrap();
+ event_source.unblock(self.quitting);
}
Ok(self.launch_at_end.take())
}
diff --git a/src/browser_states.rs b/src/browser_states.rs
index 5d9b4c9..99e4f85 100644
--- a/src/browser_states.rs
+++ b/src/browser_states.rs
@@ -82,6 +82,49 @@ impl BrowserState {
}
}
+ fn open_selection(
+ &mut self,
+ screen: &mut Screen,
+ con: &AppContext,
+ ) -> io::Result<AppStateCmdResult> {
+ let tree = match &self.filtered_tree {
+ Some(tree) => tree,
+ None => &self.tree,
+ };
+ if tree.selection == 0 {
+ Ok(AppStateCmdResult::Keep)
+ } else {
+ let line = tree.selected_line();
+ let tl = TaskLifetime::unlimited();
+ match &line.line_type {
+ LineType::File => {
+ opener(line.path.clone(), line.is_exe(), con)
+ }
+ LineType::Dir | LineType::SymLinkToDir(_) => {
+ Ok(AppStateCmdResult::from_optional_state(
+ BrowserState::new(
+ line.target(),
+ tree.options.without_pattern(),
+ screen,
+ &tl,
+ ),
+ Command::new(),
+ ))
+ }
+ LineType::SymLinkToFile(target) => {
+ opener(
+ PathBuf::from(target),
+ line.is_exe(), // today this always return false
+ con,
+ )
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ }
+}
+
}
fn opener(path: PathBuf, is_exe: bool, con: &AppContext) -> io::Result<AppStateCmdResult> {
@@ -152,44 +195,33 @@ impl AppState for BrowserState {
}
Ok(AppStateCmdResult::Keep)
}
- Action::OpenSelection => {
+ Action::Click(_, y) => {
+ let y = *y as i32 - 1; // click position starts at (1, 1)
+ match self.filtered_tree {
+ Some(ref mut tree) => {
+ tree.try_select_y(y);
+ }
+ None => {
+ self.tree.try_select_y(y);
+ }
+ };
+ Ok(AppStateCmdResult::Keep)
+ }
+ Action::DoubleClick(_, y) => {
let tree = match &self.filtered_tree {
Some(tree) => tree,
None => &self.tree,
};
- if tree.selection == 0 {
- Ok(AppStateCmdResult::Keep)
+ if tree.selection + 1 == *y as usize {
+ self.open_selection(screen, con)
} else {
- let line = tree.selected_line();
- let tl = TaskLifetime::unlimited();
- match &line.line_type {
- LineType::File => {
- opener(line.path.clone(), line.is_exe(), con)
- }
- LineType::Dir | LineType::SymLinkToDir(_) => {
- Ok(AppStateCmdResult::from_optional_state(
- BrowserState::new(
- line.target(),
- tree.options.without_pattern(),
- screen,
- &tl,
- ),
- Command::new(),
- ))
- }
- LineType::SymLinkToFile(target) => {
- opener(
- PathBuf::from(target),
- line.is_exe(), // today this always return false
- con,
- )
- }
- _ => {
- unreachable!();
- }
- }
+ // A double click always come after a simple click at
+ // same position. If it's not the selected line, it means
+ // the click wasn't on a selectable/openable tree line
+ Ok(AppStateCmdResult::Keep)
}
}
+ Action::OpenSelection => self.open_selection(screen, con),
Action::AltOpenSelection => {
let tree = match &self.filtered_tree {
Some(tree) => tree,
diff --git a/src/commands.rs b/src/commands.rs
index 967da1e..eacd49e 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -3,6 +3,7 @@
//! (verbs arent checked at this point)
use crate::verb_invocation::VerbInvocation;
+use crate::event::Event;
use crossterm::KeyEvent;
use regex::Regex;
@@ -36,6 +37,8 @@ pub enum Action {
Refresh, // refresh
Help, // goes to help state
Quit, // quit broot
+ Click(u16, u16), // usually a mouse click
+ DoubleClick(u16, u16), // always come after a simple click at same position
Unparsed, // or unparsable
}
@@ -126,7 +129,21 @@ impl Command {
Command { raw, parts, action }
}
- pub fn add_key(&mut self, key: KeyEvent) {
+ pub fn add_event(&mut self, event: Event) {
+ match event {
+ Event::Click(x, y) => {
+ self.action = Action::Click(x, y);
+ }
+ Event::DoubleClick(x, y) => {
+ self.action = Action::DoubleClick(x, y);
+ }
+ Event::Key(key) => {
+ self.add_key(key);
+ }
+ }
+ }
+
+ fn add_key(&mut self, key: KeyEvent) {
match key {
KeyEvent::Char('\t') => {
self.action = Action::Next;
diff --git a/src/event.rs b/src/event.rs
new file mode 100644
index 0000000..84add33
--- /dev/null
+++ b/src/event.rs
@@ -0,0 +1,122 @@
+use crossterm::TerminalInput;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::thread;
+use std::time::{Instant, Duration};
+use std::sync::{mpsc::{self, Sender, Receiver, RecvError}, Arc};
+use crate::task_sync::TaskLifetime;
+use crossterm::{InputEvent, KeyEvent, MouseEvent};
+
+const DOUBLE_CLICK_MAX_DURATION: Duration = Duration::from_millis(700);
+
+/// a valid user event
+#[derive(Debug, Clone)]
+pub enum Event {
+ Key(KeyEvent),
+ Click(u16, u16),
+ DoubleClick(u16, u16),
+}
+
+impl Event {
+ pub fn from_crossterm_event(crossterm_event: Option<InputEvent>) -> Option<Event> {
+ match crossterm_event {
+ Some(InputEvent::Keyboard(key)) => Some(Event::Key(key)),
+ Some(InputEvent::Mouse(MouseEvent::Release(x, y))) => Some(Event::Click(x, y)),
+ _ => None,
+ }
+ }
+}
+
+/// an event with time of occuring
+struct TimedEvent {
+ time: Instant,
+ event: Event,
+}
+impl From<Event> for TimedEvent {
+ fn from(event: Event) -> Self {
+ TimedEvent {
+ time: Instant::now(),
+ event,
+ }
+ }
+}
+
+/// a thread backed event listener. Can provide a task_lifetime which
+/// will expire as soon as a new event is received, thus allowing
+/// interruptible tasks.
+pub struct EventSource {
+ rx_events: Receiver<Event>,
+ tx_quit: Sender<bool>,
+ task_count: Arc<AtomicUsize>,
+}
+
+impl EventSource {
+ /// create a new source
+ pub fn new() -> EventSource {
+ let (tx_events, rx_events) = mpsc::channel();
+ let (tx_quit, rx_quit) = mpsc::channel();
+ let task_count = Arc::new(AtomicUsize::new(0));
+ let event_count = Arc::clone(&task_count);
+ thread::spawn(move || {
+ let input = TerminalInput::new();
+ let mut last_event: Option<TimedEvent> = None;
+ if let Err(e) = input.enable_mouse_mode() {
+ warn!("Error while enabling mouse. {:?}", e);
+ }
+ let mut crossterm_events = input.read_sync();
+ loop {
+ let crossterm_event = crossterm_events.next();
+ info!(" => crossterm event={:?}", crossterm_event);
+ if let Some(mut event) = Event::from_crossterm_event(crossterm_event) {
+ // save the event, and maybe change it
+ // (may change a click into a double-click)
+ if let Event::Click(x, y) = event {
+ if let Some(TimedEvent{time, event:Event::Click(_, last_y)}) = last_event {
+ if last_y == y && time.elapsed() < DOUBLE_CLICK_MAX_DURATION {
+ info!("DOUBLE CLICK");
+ event = Event::DoubleClick(x, y);
+ }
+ }
+ }
+ last_event = Some(TimedEvent::from(event.clone()));
+ event_count.fetch_add(1, Ordering::SeqCst);
+ // we send the even to the receiver in the main event loop
+ tx_events.send(event).unwrap();
+ let quit = rx_quit.recv().unwrap();
+ if quit {
+ // Cleanly quitting this thread is necessary
+ // to ensure stdin is properly closed when
+ // we launch an external application in the same
+ // terminal
+ // Disabling mouse mode is also necessary to let the
+ // terminal in a proper state.
+ input.disable_mouse_mode().unwrap();
+ return;
+ }
+ }
+ }
+ });
+ EventSource {
+ rx_events,
+ tx_quit,
+ task_count,
+ }
+ }
+
+ /// either start listening again, or quit, depending on the passed bool.
+ /// It's mandatory to call this with quit=true at end for a proper ending
+ /// of the thread (and its resources)
+ pub fn unblock(&self, quit: bool) {
+ self.tx_quit.send(quit).unwrap();
+ }
+
+ /// returns a task lifetime which will end when a new event is received
+ pub fn new_task_lifetime(&self) -> TaskLifetime {
+ TaskLifetime::new(&self.task_count)
+ }
+
+ /// receives a new event. Blocks until there's one.
+ /// Event listening will be off until the next call to unblock.
+ pub fn recv(&self) -> Result<Event, RecvError> {
+ self.rx_events.recv()
+ }
+}
diff --git a/src/flat_tree.rs b/src/flat_tree.rs
index 4bf07c0..1d81f37 100644
--- a/src/flat_tree.rs
+++ b/src/flat_tree.rs
@@ -242,7 +242,20 @@ impl Tree {
self.scroll = (self.scroll + dy).max(0).min(self.lines.len() as i32 - 5);
self.select_visible_line(page_height);
}
- pub fn select_visible_line(&mut self, page_height: i32) {
+ /// try to select a line (works if y+scroll falls on a selectable line)
+ pub fn try_select_y(&mut self, y: i32) -> bool {
+ let y = y + self.scroll;
+ if y >= 0 && y <= self.lines.len() as i32 {
+ let y = y as usize;
+ if self.lines[y].is_selectable() {
+ self.selection = y;
+ return true;
+ }
+ }
+ false
+ }
+ /// fix the selection so that it's a selectable visible line
+ fn select_visible_line(&mut self, page_height: i32) {
let sel = self.selection as i32;
if sel < self.scroll || sel >= self.scroll + page_height {
self.selection = self.scroll as usize;
diff --git a/src/main.rs b/src/main.rs
index eedf5d8..4ffaf89 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,6 +18,7 @@ mod conf;
mod displayable_tree;
mod errors;
mod external;
+mod event;
mod file_sizes;
mod flat_tree;
mod fuzzy_patterns;