340 lines
12 KiB
Rust
340 lines
12 KiB
Rust
mod active_component;
|
|
mod pages;
|
|
mod tips;
|
|
use zellij_tile::prelude::*;
|
|
|
|
use pages::Page;
|
|
use rand::prelude::*;
|
|
use rand::rng;
|
|
use std::cell::RefCell;
|
|
use std::collections::BTreeMap;
|
|
use std::rc::Rc;
|
|
use tips::MAX_TIP_INDEX;
|
|
|
|
use crate::active_component::ActiveComponent;
|
|
use crate::pages::{ComponentLine, TextOrCustomRender};
|
|
|
|
const UI_ROWS: usize = 20;
|
|
const UI_COLUMNS: usize = 90;
|
|
|
|
#[derive(Debug)]
|
|
struct App {
|
|
active_page: Page,
|
|
link_executable: Rc<RefCell<String>>,
|
|
zellij_version: Rc<RefCell<String>>,
|
|
base_mode: Rc<RefCell<InputMode>>,
|
|
tab_rows: usize,
|
|
tab_columns: usize,
|
|
own_plugin_id: Option<u32>,
|
|
is_release_notes: bool,
|
|
is_startup_tip: bool,
|
|
tip_index: usize,
|
|
waiting_for_config_to_be_written: bool,
|
|
error: Option<String>,
|
|
}
|
|
|
|
impl Default for App {
|
|
fn default() -> Self {
|
|
let link_executable = Rc::new(RefCell::new("".to_owned()));
|
|
let zellij_version = Rc::new(RefCell::new("".to_owned()));
|
|
let base_mode = Rc::new(RefCell::new(Default::default()));
|
|
App {
|
|
active_page: Page::new_main_screen(
|
|
link_executable.clone(),
|
|
"".to_owned(),
|
|
base_mode.clone(),
|
|
false,
|
|
),
|
|
link_executable,
|
|
zellij_version,
|
|
base_mode,
|
|
tab_rows: 0,
|
|
tab_columns: 0,
|
|
own_plugin_id: None,
|
|
is_release_notes: false,
|
|
is_startup_tip: false,
|
|
tip_index: 0,
|
|
waiting_for_config_to_be_written: false,
|
|
error: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
register_plugin!(App);
|
|
|
|
impl ZellijPlugin for App {
|
|
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
|
self.is_release_notes = configuration
|
|
.get("is_release_notes")
|
|
.map(|v| v == "true")
|
|
.unwrap_or(false);
|
|
self.is_startup_tip = configuration
|
|
.get("is_startup_tip")
|
|
.map(|v| v == "true")
|
|
.unwrap_or(false);
|
|
subscribe(&[
|
|
EventType::Key,
|
|
EventType::Mouse,
|
|
EventType::ModeUpdate,
|
|
EventType::RunCommandResult,
|
|
EventType::TabUpdate,
|
|
EventType::FailedToWriteConfigToDisk,
|
|
EventType::ConfigWasWrittenToDisk,
|
|
]);
|
|
let own_plugin_id = get_plugin_ids().plugin_id;
|
|
self.own_plugin_id = Some(own_plugin_id);
|
|
*self.zellij_version.borrow_mut() = get_zellij_version();
|
|
self.change_own_title();
|
|
self.query_link_executable();
|
|
self.active_page = if self.is_startup_tip {
|
|
let mut rng = rng();
|
|
self.tip_index = rng.random_range(0..=MAX_TIP_INDEX);
|
|
Page::new_tip_screen(
|
|
self.link_executable.clone(),
|
|
self.base_mode.clone(),
|
|
self.tip_index,
|
|
)
|
|
} else {
|
|
Page::new_main_screen(
|
|
self.link_executable.clone(),
|
|
self.zellij_version.borrow().clone(),
|
|
self.base_mode.clone(),
|
|
self.is_release_notes,
|
|
)
|
|
};
|
|
}
|
|
fn update(&mut self, event: Event) -> bool {
|
|
let mut should_render = false;
|
|
match event {
|
|
Event::FailedToWriteConfigToDisk(file_path) => {
|
|
if self.waiting_for_config_to_be_written {
|
|
let error = match file_path {
|
|
Some(file_path) => {
|
|
format!("Failed to write config to disk at: {}", file_path)
|
|
},
|
|
None => format!("Failed to write config to disk."),
|
|
};
|
|
eprintln!("{}", error);
|
|
self.error = Some(error);
|
|
should_render = true;
|
|
}
|
|
},
|
|
Event::ConfigWasWrittenToDisk => {
|
|
if self.waiting_for_config_to_be_written {
|
|
close_self();
|
|
}
|
|
},
|
|
Event::TabUpdate(tab_info) => {
|
|
self.center_own_pane(tab_info);
|
|
},
|
|
Event::Mouse(mouse_event) => {
|
|
should_render = self.handle_mouse_event(mouse_event);
|
|
},
|
|
Event::ModeUpdate(mode_info) => {
|
|
if let Some(base_mode) = mode_info.base_mode {
|
|
should_render = self.update_base_mode(base_mode);
|
|
}
|
|
},
|
|
Event::RunCommandResult(exit_code, _stdout, _stderr, context) => {
|
|
let is_xdg_open = context.get("xdg_open_cli").is_some();
|
|
let is_open = context.get("open_cli").is_some();
|
|
if is_xdg_open {
|
|
if exit_code == Some(0) {
|
|
self.update_link_executable("xdg-open".to_owned());
|
|
}
|
|
} else if is_open {
|
|
if exit_code == Some(0) {
|
|
self.update_link_executable("open".to_owned());
|
|
}
|
|
}
|
|
},
|
|
Event::Key(key) => {
|
|
if let Some(_error) = self.error.take() {
|
|
// dismiss error on any key
|
|
should_render = true;
|
|
} else {
|
|
should_render = self.handle_key(key);
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
should_render
|
|
}
|
|
fn render(&mut self, rows: usize, cols: usize) {
|
|
if let Some(error) = &self.error {
|
|
self.render_error(rows, cols, error.to_owned())
|
|
} else {
|
|
self.active_page.render(rows, cols, &self.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
pub fn change_own_title(&mut self) {
|
|
if let Some(own_plugin_id) = self.own_plugin_id {
|
|
if self.is_release_notes {
|
|
rename_plugin_pane(
|
|
own_plugin_id,
|
|
format!("Release Notes {}", self.zellij_version.borrow()),
|
|
);
|
|
} else {
|
|
rename_plugin_pane(own_plugin_id, "About Zellij");
|
|
}
|
|
}
|
|
}
|
|
pub fn query_link_executable(&self) {
|
|
let mut xdg_open_context = BTreeMap::new();
|
|
xdg_open_context.insert("xdg_open_cli".to_owned(), String::new());
|
|
run_command(&["xdg-open", "--help"], xdg_open_context);
|
|
let mut open_context = BTreeMap::new();
|
|
open_context.insert("open_cli".to_owned(), String::new());
|
|
run_command(&["open", "--help"], open_context);
|
|
}
|
|
pub fn update_link_executable(&mut self, new_link_executable: String) {
|
|
*self.link_executable.borrow_mut() = new_link_executable;
|
|
}
|
|
pub fn update_base_mode(&mut self, new_base_mode: InputMode) -> bool {
|
|
let mut should_render = false;
|
|
if *self.base_mode.borrow() != new_base_mode {
|
|
should_render = true;
|
|
}
|
|
*self.base_mode.borrow_mut() = new_base_mode;
|
|
should_render
|
|
}
|
|
pub fn handle_mouse_event(&mut self, mouse_event: Mouse) -> bool {
|
|
let mut should_render = false;
|
|
match mouse_event {
|
|
Mouse::LeftClick(line, column) => {
|
|
if let Some(new_page) = self
|
|
.active_page
|
|
.handle_mouse_left_click(column, line as usize)
|
|
{
|
|
self.active_page = new_page;
|
|
should_render = true;
|
|
}
|
|
},
|
|
Mouse::Hover(line, column) => {
|
|
should_render = self.active_page.handle_mouse_hover(column, line as usize);
|
|
},
|
|
_ => {},
|
|
}
|
|
should_render
|
|
}
|
|
pub fn handle_key(&mut self, key: KeyWithModifier) -> bool {
|
|
let mut should_render = false;
|
|
if key.bare_key == BareKey::Up && key.has_no_modifiers() && self.is_startup_tip {
|
|
self.previous_tip();
|
|
should_render = true;
|
|
} else if key.bare_key == BareKey::Down && key.has_no_modifiers() && self.is_startup_tip {
|
|
self.next_tip();
|
|
should_render = true;
|
|
} else if key.bare_key == BareKey::Enter && key.has_no_modifiers() {
|
|
if let Some(new_page) = self.active_page.handle_selection() {
|
|
self.active_page = new_page;
|
|
should_render = true;
|
|
}
|
|
} else if key.bare_key == BareKey::Char('c')
|
|
&& key.has_modifiers(&[KeyModifier::Ctrl])
|
|
&& self.is_startup_tip
|
|
{
|
|
self.waiting_for_config_to_be_written = true;
|
|
let save_configuration = true;
|
|
reconfigure("show_startup_tips false".to_owned(), save_configuration);
|
|
} else if key.bare_key == BareKey::Esc && key.has_no_modifiers() {
|
|
if self.active_page.is_main_screen {
|
|
close_self();
|
|
} else {
|
|
self.active_page = Page::new_main_screen(
|
|
self.link_executable.clone(),
|
|
self.zellij_version.borrow().clone(),
|
|
self.base_mode.clone(),
|
|
self.is_release_notes,
|
|
);
|
|
should_render = true;
|
|
}
|
|
} else if key.bare_key == BareKey::Char('?')
|
|
&& !self.is_release_notes
|
|
&& !self.is_startup_tip
|
|
{
|
|
self.is_startup_tip = true;
|
|
self.active_page = Page::new_tip_screen(
|
|
self.link_executable.clone(),
|
|
self.base_mode.clone(),
|
|
self.tip_index,
|
|
);
|
|
should_render = true;
|
|
} else {
|
|
should_render = self.active_page.handle_key(key);
|
|
}
|
|
should_render
|
|
}
|
|
fn render_error(&self, rows: usize, cols: usize, error: String) {
|
|
let mut error_page = Page::new()
|
|
.main_screen()
|
|
.with_title(Text::new(format!("{}", error)).color_range(3, ..))
|
|
.with_paragraph(vec![
|
|
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
|
|
Text::new("Unable to permanently dismiss tips."),
|
|
))]),
|
|
ComponentLine::new(vec![ActiveComponent::new(TextOrCustomRender::Text(
|
|
Text::new("You can do so manually by adding the following to your config:"),
|
|
))]),
|
|
])
|
|
.with_paragraph(vec![ComponentLine::new(vec![ActiveComponent::new(
|
|
TextOrCustomRender::Text(Text::new("show_startup_tips false").color_range(0, ..)),
|
|
)])])
|
|
.with_help(Box::new(|_hovering_over_link, _menu_item_is_selected| {
|
|
Text::new("<ESC> - dismiss").color_range(1, ..=4)
|
|
}));
|
|
error_page.render(rows, cols, &None)
|
|
}
|
|
fn center_own_pane(&mut self, tab_info: Vec<TabInfo>) {
|
|
// we only take the size of the first tab because at the time of writing this is
|
|
// identical to all tabs, but this might not always be the case...
|
|
if let Some(first_tab) = tab_info.get(0) {
|
|
let prev_tab_columns = self.tab_columns;
|
|
let prev_tab_rows = self.tab_rows;
|
|
self.tab_columns = first_tab.display_area_columns;
|
|
self.tab_rows = first_tab.display_area_rows;
|
|
if self.tab_columns != prev_tab_columns || self.tab_rows != prev_tab_rows {
|
|
let desired_x_coords = self.tab_columns.saturating_sub(UI_COLUMNS) / 2;
|
|
let desired_y_coords = self.tab_rows.saturating_sub(UI_ROWS) / 2;
|
|
change_floating_panes_coordinates(vec![(
|
|
PaneId::Plugin(self.own_plugin_id.unwrap()),
|
|
FloatingPaneCoordinates::new(
|
|
Some(desired_x_coords.to_string()),
|
|
Some(desired_y_coords.to_string()),
|
|
Some(UI_COLUMNS.to_string()),
|
|
Some(UI_ROWS.to_string()),
|
|
None,
|
|
)
|
|
.unwrap(),
|
|
)]);
|
|
}
|
|
}
|
|
}
|
|
fn previous_tip(&mut self) {
|
|
if self.tip_index == 0 {
|
|
self.tip_index = MAX_TIP_INDEX;
|
|
} else {
|
|
self.tip_index = self.tip_index.saturating_sub(1);
|
|
}
|
|
self.active_page = Page::new_tip_screen(
|
|
self.link_executable.clone(),
|
|
self.base_mode.clone(),
|
|
self.tip_index,
|
|
);
|
|
}
|
|
fn next_tip(&mut self) {
|
|
if self.tip_index == MAX_TIP_INDEX {
|
|
self.tip_index = 0;
|
|
} else {
|
|
self.tip_index += 1;
|
|
}
|
|
self.active_page = Page::new_tip_screen(
|
|
self.link_executable.clone(),
|
|
self.base_mode.clone(),
|
|
self.tip_index,
|
|
);
|
|
}
|
|
}
|