diff --git a/Cargo.lock b/Cargo.lock index 1275abf81..fcb872586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1579,6 +1579,14 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tab-bar" +version = "0.1.0" +dependencies = [ + "colored", + "zellij-tile", +] + [[package]] name = "tap" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 3782c73f7..4dbe24cab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ "zellij-tile", "default-tiles/status-bar", "default-tiles/strider", + "default-tiles/tab-bar", ] [profile.release] diff --git a/assets/layouts/default.yaml b/assets/layouts/default.yaml index b8667b8d2..f06bab17d 100644 --- a/assets/layouts/default.yaml +++ b/assets/layouts/default.yaml @@ -1,6 +1,10 @@ --- direction: Horizontal parts: + - direction: Vertical + split_size: + Fixed: 1 + plugin: tab-bar - direction: Vertical expansion_boundary: true - direction: Vertical diff --git a/build-all.sh b/build-all.sh index 3243ea45c..b3094bf2a 100755 --- a/build-all.sh +++ b/build-all.sh @@ -1,19 +1,25 @@ #!/bin/sh +total=6 + # This is temporary while https://github.com/rust-lang/cargo/issues/7004 is open -echo "Building zellij-tile (1/5)..." +echo "Building zellij-tile (1/$total)..." cd zellij-tile cargo build --release -echo "Building status-bar (2/5)..." +echo "Building status-bar (2/$total)..." cd ../default-tiles/status-bar cargo build --release -echo "Building strider (3/5)..." +echo "Building strider (3/$total)..." cd ../strider cargo build --release -echo "Optimising WASM executables (4/5)..." +echo "Building tab-bar (4/$total)..." +cd ../tab-bar +cargo build --release +echo "Optimising WASM executables (5/$total)..." cd ../.. wasm-opt -O target/wasm32-wasi/release/status-bar.wasm -o target/status-bar.wasm || cp target/wasm32-wasi/release/status-bar.wasm target/status-bar.wasm wasm-opt -O target/wasm32-wasi/release/strider.wasm -o target/strider.wasm || cp target/wasm32-wasi/release/strider.wasm target/strider.wasm -echo "Building zellij (5/5)..." -cargo build $@ \ No newline at end of file +wasm-opt -O target/wasm32-wasi/release/tab-bar.wasm -o assets/plugins/tab-bar.wasm || cp target/wasm32-wasi/release/tab-bar.wasm target/tab-bar.wasm +echo "Building zellij (6/$total)..." +cargo build $@ diff --git a/build.rs b/build.rs index a9a8c8083..e7d2f36af 100644 --- a/build.rs +++ b/build.rs @@ -42,6 +42,7 @@ fn main() { let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); let data_dir = project_dirs.data_dir(); drop(fs::remove_file(data_dir.join("plugins/status-bar.wasm"))); + drop(fs::remove_file(data_dir.join("plugins/tab-bar.wasm"))); drop(fs::remove_file(data_dir.join("plugins/strider.wasm"))); drop(fs::remove_file(data_dir.join("layouts/default.yaml"))); drop(fs::remove_file(data_dir.join("layouts/strider.yaml"))); diff --git a/default-tiles/tab-bar/.cargo/config.toml b/default-tiles/tab-bar/.cargo/config.toml new file mode 100644 index 000000000..6b77899cb --- /dev/null +++ b/default-tiles/tab-bar/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/default-tiles/tab-bar/Cargo.toml b/default-tiles/tab-bar/Cargo.toml new file mode 100644 index 000000000..1a0c5f783 --- /dev/null +++ b/default-tiles/tab-bar/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tab-bar" +version = "0.1.0" +authors = ["Jonah Caplan "] +edition = "2018" +license = "MIT" + +[dependencies] +colored = "2" +zellij-tile = { path = "../../zellij-tile" } diff --git a/default-tiles/tab-bar/LICENSE.md b/default-tiles/tab-bar/LICENSE.md new file mode 120000 index 000000000..f0608a63a --- /dev/null +++ b/default-tiles/tab-bar/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/default-tiles/tab-bar/src/main.rs b/default-tiles/tab-bar/src/main.rs new file mode 100644 index 000000000..78a57aca0 --- /dev/null +++ b/default-tiles/tab-bar/src/main.rs @@ -0,0 +1,40 @@ +use colored::*; +use zellij_tile::*; + +#[derive(Default)] +struct State { + active_tab_index: usize, + num_tabs: usize, +} + +register_tile!(State); + +impl ZellijTile for State { + fn init(&mut self) { + set_selectable(false); + set_invisible_borders(true); + set_max_height(1); + self.active_tab_index = 0; + self.num_tabs = 0; + } + + fn draw(&mut self, _rows: usize, _cols: usize) { + let mut s = String::new(); + let active_tab = self.active_tab_index + 1; + for i in 1..=self.num_tabs { + let tab; + if i == active_tab { + tab = format!("*{} ", i).black().bold().on_magenta(); + } else { + tab = format!("-{} ", i).white(); + } + s = format!("{}{}", s, tab); + } + println!("Tabs: {}\u{1b}[40m\u{1b}[0K", s); + } + + fn update_tabs(&mut self, active_tab_index: usize, num_tabs: usize) { + self.active_tab_index = active_tab_index; + self.num_tabs = num_tabs; + } +} diff --git a/src/client/tab.rs b/src/client/tab.rs index bdbf7db13..bf0963677 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -50,6 +50,7 @@ fn split_horizontally_with_gap(rect: &PositionAndSize) -> (PositionAndSize, Posi pub struct Tab { pub index: usize, + pub position: usize, panes: BTreeMap>, panes_to_hide: HashSet, active_terminal: Option, @@ -168,6 +169,7 @@ impl Tab { // FIXME: Too many arguments here! Maybe bundle all of the senders for the whole program in a struct? pub fn new( index: usize, + position: usize, full_screen_ws: &PositionAndSize, mut os_api: Box, send_pty_instructions: SenderWithContext, @@ -191,6 +193,7 @@ impl Tab { }; Tab { index, + position, panes, max_panes, panes_to_hide: HashSet::new(), diff --git a/src/common/errors.rs b/src/common/errors.rs index 3d105dbca..03aec7140 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -197,6 +197,7 @@ pub enum ScreenContext { SwitchTabNext, SwitchTabPrev, CloseTab, + GoToTab, } impl From<&ScreenInstruction> for ScreenContext { @@ -234,6 +235,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::SwitchTabNext => ScreenContext::SwitchTabNext, ScreenInstruction::SwitchTabPrev => ScreenContext::SwitchTabPrev, ScreenInstruction::CloseTab => ScreenContext::CloseTab, + ScreenInstruction::GoToTab(_) => ScreenContext::GoToTab, } } } @@ -277,6 +279,7 @@ pub enum PluginContext { GlobalInput, Unload, Quit, + Tabs, } impl From<&PluginInstruction> for PluginContext { @@ -288,6 +291,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::GlobalInput(_) => PluginContext::GlobalInput, PluginInstruction::Unload(_) => PluginContext::Unload, PluginInstruction::Quit => PluginContext::Quit, + PluginInstruction::UpdateTabs(..) => PluginContext::Tabs, } } } diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index eee8b1b6b..fe5e2dadb 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -47,4 +47,5 @@ pub enum Action { GoToPreviousTab, /// Close the current tab. CloseTab, + GoToTab(u32), } diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 2a8c3bfab..5c4f1971a 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -227,6 +227,11 @@ impl InputHandler { .unwrap(); self.command_is_executing.wait_until_pane_is_closed(); } + Action::GoToTab(i) => { + self.send_screen_instructions + .send(ScreenInstruction::GoToTab(i)) + .unwrap(); + } Action::NoOp => {} } diff --git a/src/common/input/keybinds.rs b/src/common/input/keybinds.rs index c573a557e..e56488b4c 100644 --- a/src/common/input/keybinds.rs +++ b/src/common/input/keybinds.rs @@ -133,6 +133,9 @@ fn get_defaults_for_mode(mode: &InputMode) -> Result { Key::Ctrl('g'), vec![Action::SwitchToMode(InputMode::Normal)], ); + for i in '1'..='9' { + defaults.insert(Key::Char(i), vec![Action::GoToTab(i.to_digit(10).unwrap())]); + } defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); } InputMode::Scroll => { diff --git a/src/common/mod.rs b/src/common/mod.rs index 8f37994f5..f78303ea6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -416,6 +416,9 @@ pub fn start(mut os_input: Box, opts: CliArgs) { screen.apply_layout(layout, new_pane_pids); command_is_executing.done_opening_new_pane(); } + ScreenInstruction::GoToTab(tab_index) => { + screen.go_to_tab(tab_index as usize) + } ScreenInstruction::Quit => { break; } @@ -511,6 +514,17 @@ pub fn start(mut os_input: Box, opts: CliArgs) { buf_tx.send(wasi_stdout(&plugin_env.wasi_env)).unwrap(); } + PluginInstruction::UpdateTabs(active_tab_index, num_tabs) => { + for (instance, _) in plugin_map.values() { + let handler = instance.exports.get_function("update_tabs").unwrap(); + handler + .call(&[ + Value::I32(active_tab_index as i32), + Value::I32(num_tabs as i32), + ]) + .unwrap(); + } + } // FIXME: Deduplicate this with the callback below! PluginInstruction::Input(pid, input_bytes) => { let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); diff --git a/src/common/screen.rs b/src/common/screen.rs index 1e04e231d..7747b43be 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -45,6 +45,7 @@ pub enum ScreenInstruction { SwitchTabNext, SwitchTabPrev, CloseTab, + GoToTab(u32), } /// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes). @@ -98,8 +99,10 @@ impl Screen { /// [pane](crate::client::panes) with PTY file descriptor `pane_id`. pub fn new_tab(&mut self, pane_id: RawFd) { let tab_index = self.get_new_tab_index(); + let position = self.tabs.len(); let tab = Tab::new( tab_index, + position, &self.full_screen_ws, self.os_api.clone(), self.send_pty_instructions.clone(), @@ -110,6 +113,7 @@ impl Screen { ); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); + self.update_tabs(); self.render(); } @@ -126,34 +130,52 @@ impl Screen { /// Sets this [`Screen`]'s active [`Tab`] to the next tab. pub fn switch_tab_next(&mut self) { - let active_tab_id = self.get_active_tab().unwrap().index; - let tab_ids: Vec = self.tabs.keys().copied().collect(); - let first_tab = tab_ids.get(0).unwrap(); - let active_tab_id_position = tab_ids.iter().position(|id| id == &active_tab_id).unwrap(); - if let Some(next_tab) = tab_ids.get(active_tab_id_position + 1) { - self.active_tab_index = Some(*next_tab); - } else { - self.active_tab_index = Some(*first_tab); + let active_tab_pos = self.get_active_tab().unwrap().position; + let new_tab_pos = (active_tab_pos + 1) % self.tabs.len(); + + for tab in self.tabs.values() { + if tab.position == new_tab_pos { + self.active_tab_index = Some(tab.index); + break; + } } + self.update_tabs(); self.render(); } /// Sets this [`Screen`]'s active [`Tab`] to the previous tab. pub fn switch_tab_prev(&mut self) { - let active_tab_id = self.get_active_tab().unwrap().index; - let tab_ids: Vec = self.tabs.keys().copied().collect(); - let first_tab = tab_ids.get(0).unwrap(); - let last_tab = tab_ids.last().unwrap(); - - let active_tab_id_position = tab_ids.iter().position(|id| id == &active_tab_id).unwrap(); - if active_tab_id == *first_tab { - self.active_tab_index = Some(*last_tab) - } else if let Some(prev_tab) = tab_ids.get(active_tab_id_position - 1) { - self.active_tab_index = Some(*prev_tab) + let active_tab_pos = self.get_active_tab().unwrap().position; + let new_tab_pos = if active_tab_pos == 0 { + self.tabs.len() - 1 + } else { + active_tab_pos - 1 + }; + for tab in self.tabs.values() { + if tab.position == new_tab_pos { + self.active_tab_index = Some(tab.index); + break; + } } + self.update_tabs(); self.render(); } + pub fn go_to_tab(&mut self, mut tab_index: usize) { + tab_index -= 1; + let active_tab = self.get_active_tab().unwrap(); + match self.tabs.values().find(|t| t.position == tab_index) { + Some(t) => { + if t.index != active_tab.index { + self.active_tab_index = Some(t.index); + self.update_tabs(); + self.render(); + } + } + None => {} + } + } + /// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens /// to be the last tab. pub fn close_tab(&mut self) { @@ -174,6 +196,13 @@ impl Screen { self.send_app_instructions .send(AppInstruction::Exit) .unwrap(); + } else { + for t in self.tabs.values_mut() { + if t.position > active_tab.position { + t.position -= 1; + } + } + self.update_tabs(); } } @@ -213,8 +242,10 @@ impl Screen { /// and switching to it. pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec) { let tab_index = self.get_new_tab_index(); + let position = self.tabs.len(); let mut tab = Tab::new( tab_index, + position, &self.full_screen_ws, self.os_api.clone(), self.send_pty_instructions.clone(), @@ -226,5 +257,17 @@ impl Screen { tab.apply_layout(layout, new_pids); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); + self.update_tabs(); + } + + fn update_tabs(&self) { + if let Some(active_tab) = self.get_active_tab() { + self.send_plugin_instructions + .send(PluginInstruction::UpdateTabs( + active_tab.position, + self.tabs.len(), + )) + .unwrap(); + } } } diff --git a/src/common/wasm_vm.rs b/src/common/wasm_vm.rs index 70808b5d3..5d4b1e304 100644 --- a/src/common/wasm_vm.rs +++ b/src/common/wasm_vm.rs @@ -18,6 +18,7 @@ pub enum PluginInstruction { Input(u32, Vec), // plugin id, input bytes GlobalInput(Vec), // input bytes Unload(u32), + UpdateTabs(usize, usize), // num tabs, active tab Quit, } diff --git a/src/main.rs b/src/main.rs index 91a7b4227..c4123e556 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ pub fn main() { let data_dir = project_dirs.data_dir(); let assets = asset_map! { "target/status-bar.wasm" => "plugins/status-bar.wasm", + "target/tab-bar.wasm" => "plugins/tab-bar.wasm", "target/strider.wasm" => "plugins/strider.wasm", "assets/layouts/default.yaml" => "layouts/default.yaml", "assets/layouts/strider.yaml" => "layouts/strider.yaml", diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index 1ed56b893..b5ba63fe2 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -7,6 +7,7 @@ pub trait ZellijTile { fn draw(&mut self, rows: usize, cols: usize) {} fn handle_key(&mut self, key: Key) {} fn handle_global_key(&mut self, key: Key) {} + fn update_tabs(&mut self, active_tab_index: usize, num_active_tabs: usize) {} } #[macro_export] @@ -42,5 +43,14 @@ macro_rules! register_tile { state.borrow_mut().handle_global_key($crate::get_key()); }); } + + #[no_mangle] + pub fn update_tabs(active_tab_index: i32, num_active_tabs: i32) { + STATE.with(|state| { + state + .borrow_mut() + .update_tabs(active_tab_index as usize, num_active_tabs as usize); + }) + } }; }