diff options
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | Cargo.lock | 24 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | src/app.rs | 49 | ||||
-rw-r--r-- | src/browser_states.rs | 94 | ||||
-rw-r--r-- | src/commands.rs | 19 | ||||
-rw-r--r-- | src/event.rs | 122 | ||||
-rw-r--r-- | src/flat_tree.rs | 15 | ||||
-rw-r--r-- | src/main.rs | 1 |
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 @@ -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" @@ -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 @@ -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; |