tuimux 0.1.8

A fast Rust TUI for everything tmux, with full CRUD support.
Documentation
use crate::app::App;
use crate::app::actions::Action;
use crate::app::state::{self, ConfirmAction, InputAction, Modal};
use crate::tmux::interface::{
    attach_to_session, attach_to_window, close_session, close_window, create_session, create_window, rename_session,
    rename_window,
};
use crossterm::event::KeyCode;

impl App {
    pub(super) fn handle_action(&mut self, action: Option<Action>) -> bool {
        let Some(action) = action else {
            return false;
        };

        match action {
            Action::Quit => return true,
            Action::StartFilter => self.with_tree_focus(state::State::start_filter),
            Action::Refresh => self.refresh_sessions(),
            Action::Export => self.export_sessions(),
            Action::Restore => self.restore_sessions(),
            Action::MoveUp => self.with_tree_focus(state::State::move_up),
            Action::MoveDown => self.with_tree_focus(state::State::move_down),
            Action::Select => self.with_tree_focus(state::State::select),
            Action::Expand => self.with_tree_focus(state::State::expand_selected_session),
            Action::Collapse => self.with_tree_focus(state::State::collapse_selected_session),
            Action::ToggleExpand => self.with_tree_focus(state::State::toggle_expand),
            Action::Attach => {
                if self.perform_attach() {
                    return true;
                }
            }
            Action::CreateSession | Action::CreateWindow | Action::Rename | Action::Close => self.start_action(action),
        }

        false
    }

    pub(super) fn handle_modal_key(&mut self, code: KeyCode) -> bool {
        let Some(modal) = self.state.modal.clone() else {
            return false;
        };

        match modal {
            Modal::Input { .. } => {
                self.handle_input_modal_key(modal, code);
                true
            }
            Modal::Confirm { .. } => {
                self.handle_confirm_modal_key(modal, code);
                true
            }
        }
    }

    pub(super) fn handle_filter_key(&mut self, code: KeyCode) -> bool {
        if !self.state.filter_mode {
            return false;
        }

        match code {
            KeyCode::Esc => {
                self.state.clear_filter();
                self.state.stop_filter();
            }
            KeyCode::Enter => self.state.stop_filter(),
            KeyCode::Backspace => self.state.pop_filter_char(),
            KeyCode::Char(character) => self.state.append_filter_char(character),
            _ => {}
        }

        true
    }

    fn with_tree_focus(&mut self, callback: impl FnOnce(&mut state::State)) {
        if matches!(self.state.focus, state::FocusRegion::Tree) {
            callback(&mut self.state);
            self.state.preview.clear();
            self.state.preview_is_error = false;
            self.request_preview_refresh();
        }
    }

    fn handle_input_modal_key(&mut self, modal: Modal, code: KeyCode) {
        let Modal::Input { title, mut value, action } = modal else {
            return;
        };

        match code {
            KeyCode::Esc => self.state.modal = None,
            KeyCode::Backspace => {
                value.pop();
                self.state.modal = Some(Modal::Input { title, value, action });
            }
            KeyCode::Enter => {
                self.state.modal = None;
                self.submit_input_action(action, value.trim());
            }
            KeyCode::Char(character) => {
                value.push(character);
                self.state.modal = Some(Modal::Input { title, value, action });
            }
            _ => self.state.modal = Some(Modal::Input { title, value, action }),
        }
    }

    fn handle_confirm_modal_key(&mut self, modal: Modal, code: KeyCode) {
        let Modal::Confirm { title, prompt, action } = modal else {
            return;
        };

        match code {
            KeyCode::Esc | KeyCode::Char('n') => self.state.modal = None,
            KeyCode::Enter | KeyCode::Char('y') => {
                self.state.modal = None;
                self.submit_confirm_action(action);
            }
            _ => self.state.modal = Some(Modal::Confirm { title, prompt, action }),
        }
    }

    fn start_action(&mut self, action: Action) {
        match action {
            Action::CreateSession => {
                self.state.modal = Some(Modal::Input {
                    title: String::from("Create session"),
                    value: String::new(),
                    action: InputAction::CreateSession,
                });
            }
            Action::CreateWindow => {
                let Some(session_name) = self.state.selected_session_name() else {
                    self.set_error_status("No session selected");
                    return;
                };

                self.state.modal = Some(Modal::Input {
                    title: format!("Create window in {session_name}"),
                    value: String::new(),
                    action: InputAction::CreateWindow { session_name: session_name.to_string() },
                });
            }
            Action::Rename => self.open_rename_modal(),
            Action::Close => self.open_close_modal(),
            _ => {}
        }
    }

    fn perform_attach(&mut self) -> bool {
        let Some(session_name) = self.state.selected_session_name() else {
            self.set_error_status("No session selected");
            return false;
        };

        let result = if let Some(window_index) = self.state.selected_window_index() {
            attach_to_window(session_name, window_index)
        } else {
            attach_to_session(session_name)
        };

        match result {
            Ok(()) => true,
            Err(error) => {
                self.set_error_status(&format!("Attach failed: {error}"));
                false
            }
        }
    }

    fn open_rename_modal(&mut self) {
        if let Some(window_index) = self.state.selected_window_index() {
            let Some(session_name) = self.state.selected_session_name() else {
                self.set_error_status("No session selected");
                return;
            };

            self.state.modal = Some(Modal::Input {
                title: format!("Rename window {session_name}:{window_index}"),
                value: String::new(),
                action: InputAction::RenameWindow {
                    session_name: session_name.to_string(),
                    window_index: window_index.to_string(),
                },
            });
            return;
        }

        let Some(session_name) = self.state.selected_session_name() else {
            self.set_error_status("No session selected");
            return;
        };

        self.state.modal = Some(Modal::Input {
            title: format!("Rename session {session_name}"),
            value: String::new(),
            action: InputAction::RenameSession { session_name: session_name.to_string() },
        });
    }

    fn open_close_modal(&mut self) {
        if let Some(window_index) = self.state.selected_window_index() {
            let Some(session_name) = self.state.selected_session_name() else {
                self.set_error_status("No session selected");
                return;
            };

            self.state.modal = Some(Modal::Confirm {
                title: format!("Close window {session_name}:{window_index}"),
                prompt: String::from("Press y/Enter to confirm, n/Esc to cancel"),
                action: ConfirmAction::CloseWindow {
                    session_name: session_name.to_string(),
                    window_index: window_index.to_string(),
                },
            });
            return;
        }

        let Some(session_name) = self.state.selected_session_name() else {
            self.set_error_status("No session selected");
            return;
        };

        self.state.modal = Some(Modal::Confirm {
            title: format!("Close session {session_name}"),
            prompt: String::from("Press y/Enter to confirm, n/Esc to cancel"),
            action: ConfirmAction::CloseSession { session_name: session_name.to_string() },
        });
    }

    fn submit_input_action(&mut self, action: InputAction, value: &str) {
        if value.is_empty() {
            self.set_error_status("Input cannot be empty");
            return;
        }

        let result = match action {
            InputAction::CreateSession => {
                let result = create_session(value);
                if result.is_ok() {
                    self.refresh_sessions();
                    self.state.select_session_by_name(value);
                    self.set_status("Session created");
                }
                result
            }
            InputAction::CreateWindow { session_name } => {
                let result = create_window(&session_name, value);
                if result.is_ok() {
                    self.refresh_sessions();
                    self.state.select_session_by_name(&session_name);
                    self.set_status("Window created");
                }
                result
            }
            InputAction::RenameSession { session_name } => {
                let result = rename_session(&session_name, value);
                if result.is_ok() {
                    self.refresh_sessions();
                    self.state.select_session_by_name(value);
                    self.set_status("Session renamed");
                }
                result
            }
            InputAction::RenameWindow { session_name, window_index } => {
                let result = rename_window(&session_name, &window_index, value);
                if result.is_ok() {
                    self.refresh_sessions();
                    self.state.select_window_by_identity(&session_name, &window_index);
                    self.set_status("Window renamed");
                }
                result
            }
        };

        if let Err(error) = result {
            self.set_error_status(&format!("Action failed: {error}"));
        }
    }

    fn submit_confirm_action(&mut self, action: ConfirmAction) {
        let result = match action {
            ConfirmAction::CloseSession { session_name } => {
                let result = close_session(&session_name);
                if result.is_ok() {
                    self.refresh_sessions();
                    self.set_status("Session closed");
                }
                result
            }
            ConfirmAction::CloseWindow { session_name, window_index } => {
                let result = close_window(&session_name, &window_index);
                if result.is_ok() {
                    self.refresh_sessions();
                    self.state.select_session_by_name(&session_name);
                    self.set_status("Window closed");
                }
                result
            }
            ConfirmAction::OverwriteSessionExport => {
                self.perform_export();
                Ok(())
            }
            ConfirmAction::RunSessionRestore => {
                self.perform_restore_sessions();
                Ok(())
            }
        };

        if let Err(error) = result {
            self.set_error_status(&format!("Action failed: {error}"));
        }
    }

    pub(super) fn set_status(&mut self, message: &str) {
        self.state.status = Some(state::StatusLine { message: message.to_string(), is_error: false });
    }

    pub(super) fn set_error_status(&mut self, message: &str) {
        self.state.status = Some(state::StatusLine { message: message.to_string(), is_error: true });
    }
}