feat(tabs): add ability to set tab name (#212)

* send all tabs in vec

* works but no input filtering

* add event types

* add event handler for tab events

* fmt fixups

* update tab name in place, and escape rename works

* rename handle_tab_event handle_tab_rename_keypress

* handle empty new_name when renaming

* fix(tabs): pad active tab name too

* fix(tabs): report proper length

* fix(tabs): always render active tab

* style(fmt): rustfmt

Co-authored-by: Aram Drevekenin <aram@poor.dev>
This commit is contained in:
Jonah Caplan 2021-03-08 04:06:23 -05:00 committed by GitHub
parent 845478fe11
commit 44b0246e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 286 additions and 68 deletions

View File

@ -5,6 +5,8 @@ parts:
split_size:
Fixed: 1
plugin: tab-bar
events:
- Tab
- direction: Vertical
expansion_boundary: true
- direction: Vertical

View File

@ -104,7 +104,7 @@ fn key_path(help: &Help) -> LinePart {
len,
)
}
InputMode::Tab => {
InputMode::Tab | InputMode::RenameTab => {
let mode_shortcut_text = "t ";
let superkey = superkey_text.bold().on_magenta();
let first_superkey_separator = ARROW_SEPARATOR.magenta().on_black();

View File

@ -115,9 +115,7 @@ fn add_next_tabs_msg(
title_bar: &mut Vec<LinePart>,
cols: usize,
) {
while get_current_title_len(&title_bar) +
// get_tabs_after_len(tabs_after_active.len()) >= cols {
right_more_message(tabs_after_active.len()).len
while get_current_title_len(&title_bar) + right_more_message(tabs_after_active.len()).len
>= cols
{
tabs_after_active.insert(0, title_bar.pop().unwrap());

View File

@ -4,7 +4,7 @@ mod tab;
use zellij_tile::*;
use crate::line::tab_line;
use crate::tab::nameless_tab;
use crate::tab::tab_style;
#[derive(Debug)]
pub struct LinePart {
@ -12,10 +12,25 @@ pub struct LinePart {
len: usize,
}
#[derive(PartialEq)]
enum BarMode {
Normal,
Rename,
}
impl Default for BarMode {
fn default() -> Self {
BarMode::Normal
}
}
#[derive(Default)]
struct State {
active_tab_index: usize,
num_tabs: usize,
tabs: Vec<TabData>,
mode: BarMode,
new_name: String,
}
static ARROW_SEPARATOR: &str = "";
@ -29,20 +44,32 @@ impl ZellijTile for State {
set_max_height(1);
self.active_tab_index = 0;
self.num_tabs = 0;
self.mode = BarMode::Normal;
self.new_name = String::new();
}
fn draw(&mut self, _rows: usize, cols: usize) {
if self.num_tabs == 0 {
if self.tabs.is_empty() {
return;
}
let mut all_tabs: Vec<LinePart> = vec![];
for i in 0..self.num_tabs {
let tab = nameless_tab(i, i == self.active_tab_index);
let mut active_tab_index = 0;
for t in self.tabs.iter_mut() {
let mut tabname = t.name.clone();
if t.active && self.mode == BarMode::Rename {
if self.new_name.is_empty() {
tabname = String::from("Enter name...");
} else {
tabname = self.new_name.clone();
}
active_tab_index = t.position;
} else if t.active {
active_tab_index = t.position;
}
let tab = tab_style(tabname, t.active, t.position);
all_tabs.push(tab);
}
let tab_line = tab_line(all_tabs, self.active_tab_index, cols);
let tab_line = tab_line(all_tabs, active_tab_index, cols);
let mut s = String::new();
for bar_part in tab_line {
s = format!("{}{}", s, bar_part.part);
@ -50,8 +77,19 @@ impl ZellijTile for State {
println!("{}\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;
fn update_tabs(&mut self) {
self.tabs = get_tabs();
}
fn handle_tab_rename_keypress(&mut self, key: Key) {
self.mode = BarMode::Rename;
match key {
Key::Char('\n') | Key::Esc => {
self.mode = BarMode::Normal;
self.new_name.clear();
}
Key::Char(c) => self.new_name = format!("{}{}", self.new_name, c),
_ => {}
}
}
}

View File

@ -9,11 +9,11 @@ pub fn active_tab(text: String, is_furthest_to_the_left: bool) -> LinePart {
ARROW_SEPARATOR.black().on_magenta()
};
let right_separator = ARROW_SEPARATOR.magenta().on_black();
let tab_styled_text = format!("{}{}{}", left_separator, text, right_separator)
let tab_styled_text = format!("{} {} {}", left_separator, text, right_separator)
.black()
.bold()
.on_magenta();
let tab_text_len = text.chars().count() + 2; // 2 for left and right separators
let tab_text_len = text.chars().count() + 4; // 2 for left and right separators, 2 for the text padding
LinePart {
part: format!("{}", tab_styled_text),
len: tab_text_len,
@ -27,26 +27,27 @@ pub fn non_active_tab(text: String, is_furthest_to_the_left: bool) -> LinePart {
ARROW_SEPARATOR.black().on_green()
};
let right_separator = ARROW_SEPARATOR.green().on_black();
let tab_styled_text = format!("{}{}{}", left_separator, text, right_separator)
let tab_styled_text = format!("{} {} {}", left_separator, text, right_separator)
.black()
.bold()
.on_green();
let tab_text_len = text.chars().count() + 2; // 2 for the left and right separators
let tab_text_len = text.chars().count() + 4; // 2 for the left and right separators, 2 for the text padding
LinePart {
part: format!("{}", tab_styled_text),
len: tab_text_len,
}
}
pub fn tab(text: String, is_active_tab: bool, is_furthest_to_the_left: bool) -> LinePart {
if is_active_tab {
active_tab(text, is_furthest_to_the_left)
pub fn tab_style(text: String, is_active_tab: bool, position: usize) -> LinePart {
let tab_text;
if text.is_empty() {
tab_text = format!("Tab #{}", position + 1);
} else {
non_active_tab(text, is_furthest_to_the_left)
tab_text = text;
}
if is_active_tab {
active_tab(tab_text, position == 0)
} else {
non_active_tab(tab_text, position == 0)
}
}
pub fn nameless_tab(index: usize, is_active_tab: bool) -> LinePart {
let tab_text = format!(" Tab #{} ", index + 1);
tab(tab_text, is_active_tab, index == 0)
}

View File

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::{fs::File, io::prelude::*};
use crate::common::wasm_vm::EventType;
use crate::panes::PositionAndSize;
fn split_space_to_parts_vertically(
@ -180,6 +181,8 @@ pub struct Layout {
pub plugin: Option<PathBuf>,
#[serde(default)]
pub expansion_boundary: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub events: Vec<EventType>,
}
impl Layout {

View File

@ -2,11 +2,13 @@
//! as well as how they should be resized
use crate::common::{AppInstruction, SenderWithContext};
use crate::layout::Layout;
use crate::panes::{PaneId, PositionAndSize, TerminalPane};
use crate::pty_bus::{PtyInstruction, VteEvent};
use crate::wasm_vm::{PluginInputType, PluginInstruction};
use crate::{boundaries::Boundaries, panes::PluginPane};
use crate::{layout::Layout, wasm_vm::PluginInstruction};
use crate::{os_input_output::OsApi, utils::shared::pad_to_size};
use serde::{Deserialize, Serialize};
use std::os::unix::io::RawFd;
use std::{
cmp::Reverse,
@ -51,6 +53,7 @@ fn split_horizontally_with_gap(rect: &PositionAndSize) -> (PositionAndSize, Posi
pub struct Tab {
pub index: usize,
pub position: usize,
pub name: String,
panes: BTreeMap<PaneId, Box<dyn Pane>>,
panes_to_hide: HashSet<PaneId>,
active_terminal: Option<PaneId>,
@ -64,6 +67,14 @@ pub struct Tab {
expansion_boundary: Option<PositionAndSize>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct TabData {
/* subset of fields to publish to plugins */
pub position: usize,
pub name: String,
pub active: bool,
}
// FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication
pub trait Pane {
fn x(&self) -> usize;
@ -170,6 +181,7 @@ impl Tab {
pub fn new(
index: usize,
position: usize,
name: String,
full_screen_ws: &PositionAndSize,
mut os_api: Box<dyn OsApi>,
send_pty_instructions: SenderWithContext<PtyInstruction>,
@ -195,6 +207,7 @@ impl Tab {
index,
position,
panes,
name,
max_panes,
panes_to_hide: HashSet::new(),
active_terminal: pane_id,
@ -249,7 +262,11 @@ impl Tab {
if let Some(plugin) = &layout.plugin {
let (pid_tx, pid_rx) = channel();
self.send_plugin_instructions
.send(PluginInstruction::Load(pid_tx, plugin.clone()))
.send(PluginInstruction::Load(
pid_tx,
plugin.clone(),
layout.events.clone(),
))
.unwrap();
let pid = pid_rx.recv().unwrap();
let new_plugin = PluginPane::new(
@ -546,7 +563,10 @@ impl Tab {
}
Some(PaneId::Plugin(pid)) => {
self.send_plugin_instructions
.send(PluginInstruction::Input(pid, input_bytes))
.send(PluginInstruction::Input(
PluginInputType::Normal(pid),
input_bytes,
))
.unwrap();
}
_ => {}

View File

@ -198,6 +198,7 @@ pub enum ScreenContext {
SwitchTabPrev,
CloseTab,
GoToTab,
UpdateTabName,
}
impl From<&ScreenInstruction> for ScreenContext {
@ -236,6 +237,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::SwitchTabPrev => ScreenContext::SwitchTabPrev,
ScreenInstruction::CloseTab => ScreenContext::CloseTab,
ScreenInstruction::GoToTab(_) => ScreenContext::GoToTab,
ScreenInstruction::UpdateTabName(_) => ScreenContext::UpdateTabName,
}
}
}

View File

@ -48,4 +48,6 @@ pub enum Action {
/// Close the current tab.
CloseTab,
GoToTab(u32),
TabNameInput(Vec<u8>),
SaveTabName,
}

View File

@ -7,7 +7,7 @@ use crate::errors::ContextType;
use crate::os_input_output::OsApi;
use crate::pty_bus::PtyInstruction;
use crate::screen::ScreenInstruction;
use crate::wasm_vm::PluginInstruction;
use crate::wasm_vm::{EventType, PluginInputType, PluginInstruction};
use crate::CommandIsExecuting;
use serde::{Deserialize, Serialize};
@ -232,6 +232,28 @@ impl InputHandler {
.send(ScreenInstruction::GoToTab(i))
.unwrap();
}
Action::TabNameInput(c) => {
self.send_plugin_instructions
.send(PluginInstruction::Input(
PluginInputType::Event(EventType::Tab),
c.clone(),
))
.unwrap();
self.send_screen_instructions
.send(ScreenInstruction::UpdateTabName(c))
.unwrap();
}
Action::SaveTabName => {
self.send_plugin_instructions
.send(PluginInstruction::Input(
PluginInputType::Event(EventType::Tab),
vec![b'\n'],
))
.unwrap();
self.send_screen_instructions
.send(ScreenInstruction::UpdateTabName(vec![b'\n']))
.unwrap();
}
Action::NoOp => {}
}
@ -265,6 +287,7 @@ pub enum InputMode {
Tab,
/// `Scroll` mode allows scrolling up and down within a pane.
Scroll,
RenameTab,
}
/// Represents the contents of the help message that is printed in the status bar,
@ -310,10 +333,14 @@ pub fn get_help(mode: InputMode) -> Help {
keybinds.push(("←↓↑→".to_string(), "Move focus".to_string()));
keybinds.push(("n".to_string(), "New".to_string()));
keybinds.push(("x".to_string(), "Close".to_string()));
keybinds.push(("r".to_string(), "Rename".to_string()));
}
InputMode::Scroll => {
keybinds.push(("↓↑".to_string(), "Scroll".to_string()));
}
InputMode::RenameTab => {
keybinds.push(("Enter".to_string(), "when done".to_string()));
}
}
keybinds.push(("ESC".to_string(), "BACK".to_string()));
keybinds.push(("q".to_string(), "QUIT".to_string()));

View File

@ -128,6 +128,13 @@ fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
defaults.insert(Key::Char('n'), vec![Action::NewTab]);
defaults.insert(Key::Char('x'), vec![Action::CloseTab]);
defaults.insert(
Key::Char('r'),
vec![
Action::SwitchToMode(InputMode::RenameTab),
Action::TabNameInput(vec![0]),
],
);
defaults.insert(Key::Char('q'), vec![Action::Quit]);
defaults.insert(
Key::Ctrl('g'),
@ -155,6 +162,23 @@ fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
);
defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]);
}
InputMode::RenameTab => {
defaults.insert(
Key::Char('\n'),
vec![Action::SaveTabName, Action::SwitchToMode(InputMode::Tab)],
);
defaults.insert(
Key::Ctrl('g'),
vec![Action::SwitchToMode(InputMode::Normal)],
);
defaults.insert(
Key::Esc,
vec![
Action::TabNameInput(vec![0x1b]),
Action::SwitchToMode(InputMode::Tab),
],
);
}
}
Ok(defaults)
@ -178,6 +202,7 @@ pub fn key_to_actions(
};
match *mode {
InputMode::Normal => mode_keybind_or_action(Action::Write(input)),
InputMode::RenameTab => mode_keybind_or_action(Action::TabNameInput(input)),
_ => mode_keybind_or_action(Action::NoOp),
}
}

View File

@ -34,7 +34,9 @@ use os_input_output::OsApi;
use pty_bus::{PtyBus, PtyInstruction};
use screen::{Screen, ScreenInstruction};
use utils::consts::{ZELLIJ_IPC_PIPE, ZELLIJ_ROOT_PLUGIN_DIR};
use wasm_vm::{wasi_stdout, wasi_write_string, zellij_imports, PluginInstruction};
use wasm_vm::{
wasi_stdout, wasi_write_string, zellij_imports, EventType, PluginInputType, PluginInstruction,
};
#[derive(Serialize, Deserialize, Debug)]
pub enum ApiCommand {
@ -419,6 +421,9 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
ScreenInstruction::GoToTab(tab_index) => {
screen.go_to_tab(tab_index as usize)
}
ScreenInstruction::UpdateTabName(c) => {
screen.update_active_tab_name(c);
}
ScreenInstruction::Quit => {
break;
}
@ -438,6 +443,11 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
let store = Store::default();
let mut plugin_id = 0;
let mut plugin_map = HashMap::new();
let handler_map: HashMap<EventType, String> =
[(EventType::Tab, "handle_tab_rename_keypress".to_string())]
.iter()
.cloned()
.collect();
move || loop {
let (event, mut err_ctx) = receive_plugin_instructions
@ -448,7 +458,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
send_pty_instructions.update(err_ctx);
send_app_instructions.update(err_ctx);
match event {
PluginInstruction::Load(pid_tx, path) => {
PluginInstruction::Load(pid_tx, path, events) => {
let project_dirs =
ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap();
let plugin_dir = project_dirs.data_dir().join("plugins/");
@ -490,6 +500,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
send_screen_instructions: send_screen_instructions.clone(),
send_app_instructions: send_app_instructions.clone(),
wasi_env,
events,
};
let zellij = zellij_imports(&store, &plugin_env);
@ -514,32 +525,58 @@ 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() {
PluginInstruction::UpdateTabs(mut tabs) => {
for (instance, plugin_env) in plugin_map.values() {
if !plugin_env.events.contains(&EventType::Tab) {
continue;
}
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();
tabs.sort_by(|a, b| a.position.cmp(&b.position));
wasi_write_string(
&plugin_env.wasi_env,
&serde_json::to_string(&tabs).unwrap(),
);
handler.call(&[]).unwrap();
}
}
// FIXME: Deduplicate this with the callback below!
PluginInstruction::Input(pid, input_bytes) => {
let (instance, plugin_env) = plugin_map.get(&pid).unwrap();
let handle_key = instance.exports.get_function("handle_key").unwrap();
for key in input_bytes.keys() {
if let Ok(key) = key {
wasi_write_string(
&plugin_env.wasi_env,
&serde_json::to_string(&key).unwrap(),
);
handle_key.call(&[]).unwrap();
PluginInstruction::Input(input_type, input_bytes) => {
match input_type {
PluginInputType::Normal(pid) => {
let (instance, plugin_env) = plugin_map.get(&pid).unwrap();
let handle_key =
instance.exports.get_function("handle_key").unwrap();
for key in input_bytes.keys() {
if let Ok(key) = key {
wasi_write_string(
&plugin_env.wasi_env,
&serde_json::to_string(&key).unwrap(),
);
handle_key.call(&[]).unwrap();
}
}
}
PluginInputType::Event(event) => {
for (instance, plugin_env) in plugin_map.values() {
if !plugin_env.events.contains(&event) {
continue;
}
let handle_key = instance
.exports
.get_function(handler_map.get(&event).unwrap())
.unwrap();
for key in input_bytes.keys() {
if let Ok(key) = key {
wasi_write_string(
&plugin_env.wasi_env,
&serde_json::to_string(&key).unwrap(),
);
handle_key.call(&[]).unwrap();
}
}
}
}
}
drop(send_screen_instructions.send(ScreenInstruction::Render));
}
PluginInstruction::GlobalInput(input_bytes) => {

View File

@ -2,13 +2,14 @@
use std::collections::BTreeMap;
use std::os::unix::io::RawFd;
use std::str;
use std::sync::mpsc::Receiver;
use super::{AppInstruction, SenderWithContext};
use crate::os_input_output::OsApi;
use crate::panes::PositionAndSize;
use crate::pty_bus::{PtyInstruction, VteEvent};
use crate::tab::Tab;
use crate::tab::{Tab, TabData};
use crate::{errors::ErrorContext, wasm_vm::PluginInstruction};
use crate::{layout::Layout, panes::PaneId};
@ -46,6 +47,7 @@ pub enum ScreenInstruction {
SwitchTabPrev,
CloseTab,
GoToTab(u32),
UpdateTabName(Vec<u8>),
}
/// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes).
@ -69,6 +71,7 @@ pub struct Screen {
active_tab_index: Option<usize>,
/// The [`OsApi`] this [`Screen`] uses.
os_api: Box<dyn OsApi>,
tabname_buf: String,
}
impl Screen {
@ -92,6 +95,7 @@ impl Screen {
active_tab_index: None,
tabs: BTreeMap::new(),
os_api,
tabname_buf: String::new(),
}
}
@ -103,6 +107,7 @@ impl Screen {
let tab = Tab::new(
tab_index,
position,
String::new(),
&self.full_screen_ws,
self.os_api.clone(),
self.send_pty_instructions.clone(),
@ -246,6 +251,7 @@ impl Screen {
let mut tab = Tab::new(
tab_index,
position,
String::new(),
&self.full_screen_ws,
self.os_api.clone(),
self.send_pty_instructions.clone(),
@ -261,13 +267,36 @@ impl Screen {
}
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();
let mut tab_data = vec![];
let active_tab_index = self.active_tab_index.unwrap();
for tab in self.tabs.values() {
tab_data.push(TabData {
position: tab.position,
name: tab.name.clone(),
active: active_tab_index == tab.index,
});
}
self.send_plugin_instructions
.send(PluginInstruction::UpdateTabs(tab_data))
.unwrap();
}
pub fn update_active_tab_name(&mut self, buf: Vec<u8>) {
let s = str::from_utf8(&buf).unwrap();
match s {
"\0" => {
self.tabname_buf = String::new();
}
"\n" => {
let new_name = self.tabname_buf.clone();
let active_tab = self.get_active_tab_mut().unwrap();
active_tab.name = new_name;
self.update_tabs();
self.render();
}
c => {
self.tabname_buf.push_str(c);
}
}
}
}

View File

@ -1,24 +1,36 @@
use crate::tab::TabData;
use serde::{Deserialize, Serialize};
use std::{
path::PathBuf,
sync::mpsc::{channel, Sender},
};
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
use wasmer_wasi::WasiEnv;
// use crate::utils::logging::debug_log_to_file;
use super::{
input::handler::get_help, pty_bus::PtyInstruction, screen::ScreenInstruction, AppInstruction,
PaneId, SenderWithContext,
};
#[derive(Clone, Debug, PartialEq, Hash, Eq, Serialize, Deserialize)]
pub enum EventType {
Tab,
}
#[derive(Clone, Debug)]
pub enum PluginInputType {
Normal(u32),
Event(EventType),
}
#[derive(Clone, Debug)]
pub enum PluginInstruction {
Load(Sender<u32>, PathBuf),
Load(Sender<u32>, PathBuf, Vec<EventType>),
Draw(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols
Input(u32, Vec<u8>), // plugin id, input bytes
Input(PluginInputType, Vec<u8>), // plugin id, input bytes
GlobalInput(Vec<u8>), // input bytes
Unload(u32),
UpdateTabs(usize, usize), // num tabs, active tab
UpdateTabs(Vec<TabData>), // num tabs, active tab
Quit,
}
@ -29,6 +41,7 @@ pub struct PluginEnv {
pub send_app_instructions: SenderWithContext<AppInstruction>,
pub send_pty_instructions: SenderWithContext<PtyInstruction>, // FIXME: This should be a big bundle of all of the channels
pub wasi_env: WasiEnv,
pub events: Vec<EventType>,
}
// Plugin API ---------------------------------------------------------------------------------------------------------

View File

@ -7,7 +7,8 @@ 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) {}
fn update_tabs(&mut self) {}
fn handle_tab_rename_keypress(&mut self, key: Key) {}
}
#[macro_export]
@ -45,11 +46,18 @@ macro_rules! register_tile {
}
#[no_mangle]
pub fn update_tabs(active_tab_index: i32, num_active_tabs: i32) {
pub fn update_tabs() {
STATE.with(|state| {
state.borrow_mut().update_tabs();
})
}
#[no_mangle]
pub fn handle_tab_rename_keypress() {
STATE.with(|state| {
state
.borrow_mut()
.update_tabs(active_tab_index as usize, num_active_tabs as usize);
.handle_tab_rename_keypress($crate::get_key());
})
}
};

View File

@ -38,10 +38,19 @@ pub enum InputMode {
Resize,
Pane,
Tab,
RenameTab,
Scroll,
Exiting,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TabData {
/* subset of fields to publish to plugins */
pub position: usize,
pub name: String,
pub active: bool,
}
impl Default for InputMode {
fn default() -> InputMode {
InputMode::Normal
@ -76,6 +85,10 @@ pub fn get_help() -> Help {
deserialize_from_stdin().unwrap_or_default()
}
pub fn get_tabs() -> Vec<TabData> {
deserialize_from_stdin().unwrap_or_default()
}
fn deserialize_from_stdin<T: DeserializeOwned>() -> Option<T> {
let mut json = String::new();
io::stdin().read_line(&mut json).unwrap();