zellij-sheets 0.1.0

Terminal-based spreadsheet viewer powered by Zellij
Documentation
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use zellij_sheets::{ui, ui::UiRenderer, SheetsConfig, SheetsState};
use zellij_tile::prelude::*;

#[derive(Default)]
pub struct PluginState {
    sheets: SheetsState,
    input_path: Option<PathBuf>,
    status: Option<String>,
}

impl PluginState {
    fn initialize_from_config(&mut self, configuration: BTreeMap<String, String>) {
        self.sheets = SheetsState::new(Arc::new(SheetsConfig::default()));
        self.input_path = configuration
            .get("input")
            .or_else(|| configuration.get("input_path"))
            .map(PathBuf::from);

        self.status = match &self.input_path {
            Some(path) => Some(format!("Waiting for permission to open {}", path.display())),
            None => {
                Some("Set plugin config `input` or `input_path` to a spreadsheet file.".to_string())
            }
        };
    }

    fn load_input(&mut self) {
        let Some(input_path) = self.input_path.clone() else {
            return;
        };

        let host_path = to_host_path(&input_path);
        match self.sheets.load_file(host_path) {
            Ok(()) => {
                self.status = None;
            }
            Err(error) => {
                self.status = Some(format!(
                    "Failed to load {}: {}",
                    input_path.display(),
                    error
                ));
            }
        }
    }

    fn handle_key(&mut self, key: KeyWithModifier) -> bool {
        match key.bare_key {
            BareKey::Down => {
                self.sheets.select_down();
                true
            }
            BareKey::Up => {
                self.sheets.select_up();
                true
            }
            BareKey::PageDown => {
                self.sheets.page_down();
                true
            }
            BareKey::PageUp => {
                self.sheets.page_up();
                true
            }
            BareKey::Home => {
                self.sheets.go_to_top();
                true
            }
            BareKey::End => {
                self.sheets.go_to_bottom();
                true
            }
            BareKey::Char('q') if key.has_no_modifiers() => {
                close_self();
                false
            }
            BareKey::Char('c') if key.has_only_modifiers(&[KeyModifier::Ctrl]) => {
                close_self();
                false
            }
            _ => false,
        }
    }
}

impl ZellijPlugin for PluginState {
    fn load(&mut self, configuration: BTreeMap<String, String>) {
        subscribe(&[EventType::Key, EventType::PermissionRequestResult]);
        set_selectable(true);
        self.initialize_from_config(configuration);

        if self.input_path.is_some() {
            request_permission(&[
                PermissionType::ChangeApplicationState,
                PermissionType::FullHdAccess,
            ]);
        }
    }

    fn update(&mut self, event: Event) -> bool {
        match event {
            Event::PermissionRequestResult(PermissionStatus::Granted) => {
                change_host_folder(PathBuf::from("/"));
                self.load_input();
                true
            }
            Event::PermissionRequestResult(PermissionStatus::Denied) => {
                self.status =
                    Some("Permission denied. This plugin needs hard-drive access.".to_string());
                true
            }
            Event::Key(key) => self.handle_key(key),
            _ => false,
        }
    }

    fn render(&mut self, rows: usize, cols: usize) {
        self.sheets.resize(cols, rows);

        if let Some(status) = &self.status {
            println!("Zellij Sheets");
            println!();
            println!("{}", status);
            println!();
            println!("Use plugin config: input=\"/absolute/path/to/file.csv\"");
            return;
        }

        let renderer = UiRenderer::new();
        let rendered = renderer
            .draw_ui(&self.sheets)
            .unwrap_or_else(|e| format!("Error: {}", e));
        for line in rendered.lines() {
            println!("{line}");
        }
    }
}

fn to_host_path(path: &Path) -> PathBuf {
    if path.starts_with("/host") {
        return path.to_path_buf();
    }

    let relative = path.strip_prefix("/").unwrap_or(path);
    Path::new("/host").join(relative)
}