add basic tab bar #166

This commit is contained in:
Jonah Caplan 2021-02-20 18:19:05 -05:00
parent 7b69fcb8e0
commit ce54127d7d
19 changed files with 182 additions and 24 deletions

8
Cargo.lock generated
View File

@ -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"

View File

@ -53,6 +53,7 @@ members = [
"zellij-tile",
"default-tiles/status-bar",
"default-tiles/strider",
"default-tiles/tab-bar",
]
[profile.release]

View File

@ -1,6 +1,10 @@
---
direction: Horizontal
parts:
- direction: Vertical
split_size:
Fixed: 1
plugin: tab-bar
- direction: Vertical
expansion_boundary: true
- direction: Vertical

View File

@ -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 $@
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 $@

View File

@ -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")));

View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-wasi"

View File

@ -0,0 +1,10 @@
[package]
name = "tab-bar"
version = "0.1.0"
authors = ["Jonah Caplan <jonahcaplan@gmail.com>"]
edition = "2018"
license = "MIT"
[dependencies]
colored = "2"
zellij-tile = { path = "../../zellij-tile" }

View File

@ -0,0 +1 @@
../../LICENSE.md

View File

@ -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;
}
}

View File

@ -50,6 +50,7 @@ fn split_horizontally_with_gap(rect: &PositionAndSize) -> (PositionAndSize, Posi
pub struct Tab {
pub index: usize,
pub position: usize,
panes: BTreeMap<PaneId, Box<dyn Pane>>,
panes_to_hide: HashSet<PaneId>,
active_terminal: Option<PaneId>,
@ -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<dyn OsApi>,
send_pty_instructions: SenderWithContext<PtyInstruction>,
@ -191,6 +193,7 @@ impl Tab {
};
Tab {
index,
position,
panes,
max_panes,
panes_to_hide: HashSet::new(),

View File

@ -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,
}
}
}

View File

@ -47,4 +47,5 @@ pub enum Action {
GoToPreviousTab,
/// Close the current tab.
CloseTab,
GoToTab(u32),
}

View File

@ -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 => {}
}

View File

@ -133,6 +133,9 @@ fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
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 => {

View File

@ -416,6 +416,9 @@ pub fn start(mut os_input: Box<dyn OsApi>, 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<dyn OsApi>, 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();

View File

@ -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<usize> = 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<usize> = 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<RawFd>) {
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();
}
}
}

View File

@ -18,6 +18,7 @@ pub enum PluginInstruction {
Input(u32, Vec<u8>), // plugin id, input bytes
GlobalInput(Vec<u8>), // input bytes
Unload(u32),
UpdateTabs(usize, usize), // num tabs, active tab
Quit,
}

View File

@ -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",

View File

@ -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);
})
}
};
}