diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index f0b5cebf..3bdceefd 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -1,23 +1,27 @@ mod state; use colored::*; -use state::{FsEntry, State}; -use std::{cmp::min, fs::read_dir, path::Path}; +use state::{refresh_directory, FsEntry, State}; +use std::{cmp::min, time::Instant}; use zellij_tile::prelude::*; -const ROOT: &str = "/host"; - register_plugin!(State); impl ZellijPlugin for State { fn load(&mut self) { refresh_directory(self); - subscribe(&[EventType::KeyPress]); + subscribe(&[EventType::KeyPress, EventType::Mouse]); } fn update(&mut self, event: Event) { - if let Event::KeyPress(key) = event { - match key { + let prev_event = if self.ev_history.len() == 2 { + self.ev_history.pop_front() + } else { + None + }; + self.ev_history.push_back((event.clone(), Instant::now())); + match event { + Event::KeyPress(key) => match key { Key::Up | Key::Char('k') => { *self.selected_mut() = self.selected().saturating_sub(1); } @@ -26,13 +30,8 @@ impl ZellijPlugin for State { *self.selected_mut() = min(self.files.len() - 1, next); } Key::Right | Key::Char('\n') | Key::Char('l') if !self.files.is_empty() => { - match self.files[self.selected()].clone() { - FsEntry::Dir(p, _) => { - self.path = p; - refresh_directory(self); - } - FsEntry::File(p, _) => open_file(p.strip_prefix(ROOT).unwrap()), - } + self.traverse_dir_or_open_file(); + self.ev_history.clear(); } Key::Left | Key::Char('h') => { if self.path.components().count() > 2 { @@ -44,25 +43,59 @@ impl ZellijPlugin for State { refresh_directory(self); } } - Key::Char('.') => { self.toggle_hidden_files(); refresh_directory(self); } _ => (), - }; + }, + Event::Mouse(mouse_event) => match mouse_event { + Mouse::ScrollDown(_) => { + let next = self.selected().saturating_add(1); + *self.selected_mut() = min(self.files.len().saturating_sub(1), next); + } + Mouse::ScrollUp(_) => { + *self.selected_mut() = self.selected().saturating_sub(1); + } + Mouse::MouseRelease(Some((line, _))) => { + if line < 0 { + return; + } + let mut should_select = true; + if let Some((Event::Mouse(Mouse::MouseRelease(Some((prev_line, _)))), t)) = + prev_event + { + if prev_line == line + && Instant::now().saturating_duration_since(t).as_millis() < 400 + { + self.traverse_dir_or_open_file(); + self.ev_history.clear(); + should_select = false; + } + } + if should_select && self.scroll() + (line as usize) < self.files.len() { + *self.selected_mut() = self.scroll() + (line as usize); + } + } + _ => {} + }, + _ => { + dbg!("Unknown event {:?}", event); + } } } fn render(&mut self, rows: usize, cols: usize) { for i in 0..rows { + // If the key was pressed, set selected so that we can see the cursor if self.selected() < self.scroll() { *self.scroll_mut() = self.selected(); } if self.selected() - self.scroll() + 2 > rows { *self.scroll_mut() = self.selected() + 2 - rows; } + let i = self.scroll() + i; if let Some(entry) = self.files.get(i) { let mut path = entry.as_line(cols).normal(); @@ -82,24 +115,3 @@ impl ZellijPlugin for State { } } } - -fn refresh_directory(state: &mut State) { - state.files = read_dir(Path::new(ROOT).join(&state.path)) - .unwrap() - .filter_map(|res| { - res.and_then(|d| { - if d.metadata()?.is_dir() { - let children = read_dir(d.path())?.count(); - Ok(FsEntry::Dir(d.path(), children)) - } else { - let size = d.metadata()?.len(); - Ok(FsEntry::File(d.path(), size)) - } - }) - .ok() - .filter(|d| !d.is_hidden_file() || !state.hide_hidden_files) - }) - .collect(); - - state.files.sort_unstable(); -} diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs index 0ae84ab2..7c19cb69 100644 --- a/default-plugins/strider/src/state.rs +++ b/default-plugins/strider/src/state.rs @@ -1,12 +1,20 @@ use pretty_bytes::converter as pb; -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::{HashMap, VecDeque}, + fs::read_dir, + path::{Path, PathBuf}, + time::Instant, +}; +use zellij_tile::prelude::*; +const ROOT: &str = "/host"; #[derive(Default)] pub struct State { pub path: PathBuf, pub files: Vec, pub cursor_hist: HashMap, pub hide_hidden_files: bool, + pub ev_history: VecDeque<(Event, Instant)>, // stores last event, can be expanded in future } impl State { @@ -25,6 +33,15 @@ impl State { pub fn toggle_hidden_files(&mut self) { self.hide_hidden_files = !self.hide_hidden_files; } + pub fn traverse_dir_or_open_file(&mut self) { + match self.files[self.selected()].clone() { + FsEntry::Dir(p, _) => { + self.path = p; + refresh_directory(self); + } + FsEntry::File(p, _) => open_file(p.strip_prefix(ROOT).unwrap()), + } + } } #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] @@ -61,3 +78,24 @@ impl FsEntry { self.name().starts_with('.') } } + +pub(crate) fn refresh_directory(state: &mut State) { + state.files = read_dir(Path::new(ROOT).join(&state.path)) + .unwrap() + .filter_map(|res| { + res.and_then(|d| { + if d.metadata()?.is_dir() { + let children = read_dir(d.path())?.count(); + Ok(FsEntry::Dir(d.path(), children)) + } else { + let size = d.metadata()?.len(); + Ok(FsEntry::File(d.path(), size)) + } + }) + .ok() + .filter(|d| !d.is_hidden_file() || !state.hide_hidden_files) + }) + .collect(); + + state.files.sort_unstable(); +} diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 85078126..c3469352 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -1,6 +1,9 @@ mod line; mod tab; +use std::cmp::{max, min}; +use std::convert::TryInto; + use zellij_tile::prelude::*; use crate::line::tab_line; @@ -15,7 +18,10 @@ pub struct LinePart { #[derive(Default)] struct State { tabs: Vec, + active_tab_idx: usize, mode_info: ModeInfo, + mouse_click_pos: usize, + should_render: bool, } static ARROW_SEPARATOR: &str = ""; @@ -25,13 +31,34 @@ register_plugin!(State); impl ZellijPlugin for State { fn load(&mut self) { set_selectable(false); - subscribe(&[EventType::TabUpdate, EventType::ModeUpdate]); + subscribe(&[ + EventType::TabUpdate, + EventType::ModeUpdate, + EventType::Mouse, + ]); } fn update(&mut self, event: Event) { match event { Event::ModeUpdate(mode_info) => self.mode_info = mode_info, - Event::TabUpdate(tabs) => self.tabs = tabs, + Event::TabUpdate(tabs) => { + // tabs are indexed starting from 1 so we need to add 1 + self.active_tab_idx = (&tabs).iter().position(|t| t.active).unwrap() + 1; + self.tabs = tabs; + } + Event::Mouse(me) => match me { + Mouse::LeftClick(_, col) => { + self.mouse_click_pos = col; + self.should_render = true; + } + Mouse::ScrollUp(_) => { + switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32); + } + Mouse::ScrollDown(_) => { + switch_tab_to(max(self.active_tab_idx.saturating_sub(1), 1) as u32); + } + _ => {} + }, _ => unimplemented!(), // FIXME: This should be unreachable, but this could be cleaner } } @@ -70,8 +97,21 @@ impl ZellijPlugin for State { self.mode_info.capabilities, ); let mut s = String::new(); - for bar_part in tab_line { - s = format!("{}{}", s, bar_part.part); + let mut len_cnt = 0; + dbg!(&tab_line); + for (idx, bar_part) in tab_line.iter().enumerate() { + s = format!("{}{}", s, &bar_part.part); + + if self.should_render + && self.mouse_click_pos > len_cnt + && self.mouse_click_pos <= len_cnt + bar_part.len + && idx > 2 + { + // First three elements of tab_line are "Zellij", session name and empty thing, hence the idx > 2 condition. + // Tabs are indexed starting from 1, therefore we need subtract 2 below. + switch_tab_to(TryInto::::try_into(idx).unwrap() - 2); + } + len_cnt += bar_part.len; } match self.mode_info.palette.cyan { PaletteColor::Rgb((r, g, b)) => { @@ -81,5 +121,6 @@ impl ZellijPlugin for State { println!("{}\u{1b}[48;5;{}m\u{1b}[0K", s, color); } } + self.should_render = false; } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 05bf12ee..f58d9f73 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -8,8 +8,9 @@ use crate::tab::Pane; use crate::ui::pane_boundaries_frame::PaneFrame; use crate::wasm_vm::PluginInstruction; use zellij_utils::pane_size::Offset; +use zellij_utils::position::Position; use zellij_utils::shared::ansi_len; -use zellij_utils::zellij_tile::prelude::PaletteColor; +use zellij_utils::zellij_tile::prelude::{Event, Mouse, PaletteColor}; use zellij_utils::{ channels::SenderWithContext, pane_size::{Dimension, PaneGeom}, @@ -254,14 +255,50 @@ impl Pane for PluginPane { self.geom.y -= count; self.should_render = true; } - fn scroll_up(&mut self, _count: usize) { - //unimplemented!() + fn scroll_up(&mut self, count: usize) { + self.send_plugin_instructions + .send(PluginInstruction::Update( + Some(self.pid), + Event::Mouse(Mouse::ScrollUp(count)), + )) + .unwrap(); } - fn scroll_down(&mut self, _count: usize) { - //unimplemented!() + fn scroll_down(&mut self, count: usize) { + self.send_plugin_instructions + .send(PluginInstruction::Update( + Some(self.pid), + Event::Mouse(Mouse::ScrollDown(count)), + )) + .unwrap(); } fn clear_scroll(&mut self) { - //unimplemented!() + unimplemented!(); + } + fn start_selection(&mut self, start: &Position) { + self.send_plugin_instructions + .send(PluginInstruction::Update( + Some(self.pid), + Event::Mouse(Mouse::LeftClick(start.line.0, start.column.0)), + )) + .unwrap(); + } + fn update_selection(&mut self, position: &Position) { + self.send_plugin_instructions + .send(PluginInstruction::Update( + Some(self.pid), + Event::Mouse(Mouse::MouseHold(position.line.0, position.column.0)), + )) + .unwrap(); + } + fn end_selection(&mut self, end: Option<&Position>) { + self.send_plugin_instructions + .send(PluginInstruction::Update( + Some(self.pid), + Event::Mouse(Mouse::MouseRelease( + end.map(|Position { line, column }| (line.0, column.0)), + )), + )) + .unwrap(); } fn is_scrolled(&self) -> bool { false diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index ec570c5e..2b966d76 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -1,5 +1,8 @@ //! `Tab`s holds multiple panes. It tracks their coordinates (x/y) and size, //! as well as how they should be resized + +use zellij_utils::{position::Position, serde, zellij_tile}; + use crate::ui::pane_resizer::PaneResizer; use crate::{ os_input_output::ServerOsApi, @@ -19,16 +22,12 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, }; use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PaletteColor}; -use zellij_utils::input::layout::Direction; -use zellij_utils::pane_size::{Offset, Size, Viewport}; use zellij_utils::{ input::{ - layout::{Layout, Run}, + layout::{Direction, Layout, Run}, parse_keys, }, - pane_size::{Dimension, PaneGeom}, - position::Position, - serde, zellij_tile, + pane_size::{Dimension, Offset, PaneGeom, Size, Viewport}, }; const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this @@ -2322,12 +2321,12 @@ impl Tab { } } pub fn scroll_terminal_up(&mut self, point: &Position, lines: usize) { - if let Some(pane) = self.get_pane_at(point) { + if let Some(pane) = self.get_pane_at(point, false) { pane.scroll_up(lines); } } pub fn scroll_terminal_down(&mut self, point: &Position, lines: usize) { - if let Some(pane) = self.get_pane_at(point) { + if let Some(pane) = self.get_pane_at(point, false) { pane.scroll_down(lines); if !pane.is_scrolled() { if let PaneId::Terminal(pid) = pane.pid() { @@ -2336,32 +2335,42 @@ impl Tab { } } } - fn get_pane_at(&mut self, point: &Position) -> Option<&mut Box> { - if let Some(pane_id) = self.get_pane_id_at(point) { + fn get_pane_at( + &mut self, + point: &Position, + search_selectable: bool, + ) -> Option<&mut Box> { + if let Some(pane_id) = self.get_pane_id_at(point, search_selectable) { self.panes.get_mut(&pane_id) } else { None } } - fn get_pane_id_at(&self, point: &Position) -> Option { + + fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Option { if self.fullscreen_is_active { return self.get_active_pane_id(); } - - self.get_selectable_panes() - .find(|(_, p)| p.contains(point)) - .map(|(&id, _)| id) + if search_selectable { + self.get_selectable_panes() + .find(|(_, p)| p.contains(point)) + .map(|(&id, _)| id) + } else { + self.get_panes() + .find(|(_, p)| p.contains(point)) + .map(|(&id, _)| id) + } } pub fn handle_left_click(&mut self, position: &Position) { self.focus_pane_at(position); - if let Some(pane) = self.get_pane_at(position) { + if let Some(pane) = self.get_pane_at(position, false) { let relative_position = pane.relative_position(position); pane.start_selection(&relative_position); }; } fn focus_pane_at(&mut self, point: &Position) { - if let Some(clicked_pane) = self.get_pane_id_at(point) { + if let Some(clicked_pane) = self.get_pane_id_at(point, true) { self.active_terminal = Some(clicked_pane); } } @@ -2369,7 +2378,7 @@ impl Tab { let active_pane_id = self.get_active_pane_id(); // on release, get the selected text from the active pane, and reset it's selection let mut selected_text = None; - if active_pane_id != self.get_pane_id_at(position) { + if active_pane_id != self.get_pane_id_at(position, true) { if let Some(active_pane_id) = active_pane_id { if let Some(active_pane) = self.panes.get_mut(&active_pane_id) { active_pane.end_selection(None); @@ -2377,7 +2386,7 @@ impl Tab { active_pane.reset_selection(); } } - } else if let Some(pane) = self.get_pane_at(position) { + } else if let Some(pane) = self.get_pane_at(position, true) { let relative_position = pane.relative_position(position); pane.end_selection(Some(&relative_position)); selected_text = pane.get_selected_text(); diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs index 1d67e847..8f851513 100644 --- a/zellij-server/src/wasm_vm.rs +++ b/zellij-server/src/wasm_vm.rs @@ -241,6 +241,7 @@ pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObj host_set_selectable, host_get_plugin_ids, host_open_file, + host_switch_tab_to, host_set_timeout, host_exec_cmd, } @@ -298,6 +299,13 @@ fn host_open_file(plugin_env: &PluginEnv) { .unwrap(); } +fn host_switch_tab_to(plugin_env: &PluginEnv, tab_idx: u32) { + plugin_env + .senders + .send_to_screen(ScreenInstruction::GoToTab(tab_idx)) + .unwrap(); +} + fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { // There is a fancy, high-performance way to do this with zero additional threads: // If the plugin thread keeps a BinaryHeap of timer structs, it can manage multiple and easily `.peek()` at the diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 3e5062b9..c98ebb24 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -25,6 +25,16 @@ pub enum Key { Esc, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] + +pub enum Mouse { + ScrollUp(usize), // number of lines + ScrollDown(usize), // number of lines + LeftClick(isize, usize), // line and column + MouseHold(isize, usize), // line and column + MouseRelease(Option<(isize, usize)>), // line and column +} + #[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(name(EventType))] @@ -33,6 +43,7 @@ pub enum Event { ModeUpdate(ModeInfo), TabUpdate(Vec), KeyPress(Key), + Mouse(Mouse), Timer(f64), CopyToClipboard, InputReceived, diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 6904d3f7..c664d36b 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -34,6 +34,10 @@ pub fn open_file(path: &Path) { unsafe { host_open_file() }; } +pub fn switch_tab_to(tab_idx: u32) { + unsafe { host_switch_tab_to(tab_idx) }; +} + pub fn set_timeout(secs: f64) { unsafe { host_set_timeout(secs) }; } @@ -63,6 +67,7 @@ extern "C" { fn host_set_selectable(selectable: i32); fn host_get_plugin_ids(); fn host_open_file(); + fn host_switch_tab_to(tab_idx: u32); fn host_set_timeout(secs: f64); fn host_exec_cmd(); }