use super::{
keybindings::{KeyAction, KeyHandler},
modes::{EditorMode, Mode},
widgets::{HelpPanel, StatusBar, TreeNode, TreeState},
};
use anyhow::Result;
use crossterm::event::{Event, KeyEvent};
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
Frame, Terminal,
};
#[derive(Debug, Clone)]
pub struct TuiConfig {
pub title: String,
pub show_help: bool,
}
impl Default for TuiConfig {
fn default() -> Self {
Self {
title: "Meta TUI".to_string(),
show_help: false,
}
}
}
pub struct TuiAppState {
pub mode: EditorMode,
pub tree_state: TreeState,
pub show_help: bool,
pub status_message: String,
pub modified: bool,
pub should_quit: bool,
pub selected_path: Vec<usize>,
}
impl Default for TuiAppState {
fn default() -> Self {
Self {
mode: EditorMode::new(),
tree_state: TreeState::new(),
show_help: false,
status_message: String::new(),
modified: false,
should_quit: false,
selected_path: Vec::new(),
}
}
}
impl TuiAppState {
pub fn new() -> Self {
Self::default()
}
pub fn set_status(&mut self, msg: impl Into<String>) {
self.status_message = msg.into();
}
pub fn clear_status(&mut self) {
self.status_message.clear();
}
}
pub trait TuiApp {
fn state(&self) -> &TuiAppState;
fn state_mut(&mut self) -> &mut TuiAppState;
fn get_tree_roots(&self) -> &[TreeNode];
fn get_tree_roots_mut(&mut self) -> &mut Vec<TreeNode>;
fn handle_action(&mut self, action: KeyAction) -> Result<()>;
fn save(&mut self) -> Result<()>;
fn can_quit(&self) -> bool {
!self.state().modified
}
fn handle_key(&mut self, key: KeyEvent) -> Result<bool> {
let action = {
let state = self.state();
KeyHandler::handle(key, state.mode.mode)
};
match action {
KeyAction::None => {}
KeyAction::EnterNormalMode => {
let state = self.state_mut();
state.mode.switch_to(Mode::Normal);
state.clear_status();
}
KeyAction::EnterInsertMode => {
let state = self.state_mut();
state.mode.switch_to(Mode::Insert);
state.set_status("-- INSERT --");
}
KeyAction::EnterVisualMode => {
let state = self.state_mut();
state.mode.switch_to(Mode::Visual);
state.mode.visual_start = Some((state.tree_state.selected, 0));
state.set_status("-- VISUAL --");
}
KeyAction::EnterCommandMode => {
let state = self.state_mut();
state.mode.switch_to(Mode::Command);
state.clear_status();
}
KeyAction::InsertChar(c) if self.state().mode.is_command() => {
self.state_mut().mode.push_command_char(c);
}
KeyAction::Backspace if self.state().mode.is_command() => {
self.state_mut().mode.pop_command_char();
}
KeyAction::ExecuteCommand(_) => {
let cmd_action = {
let state = self.state_mut();
let cmd = state.mode.command().to_string();
let cmd_action = KeyHandler::parse_command(&cmd);
state.mode.switch_to(Mode::Normal);
state.mode.clear_command();
cmd_action
};
return self.handle_key_action(cmd_action);
}
KeyAction::MoveUp => {
self.state_mut().tree_state.select_previous();
}
KeyAction::MoveDown => {
let visible_count = {
let roots = self.get_tree_roots();
roots.iter().flat_map(|r| r.flatten(true)).count()
};
self.state_mut().tree_state.select_next(visible_count);
}
KeyAction::MoveToTop => {
self.state_mut().tree_state.select_first();
}
KeyAction::MoveToBottom => {
let visible_count = {
let roots = self.get_tree_roots();
roots.iter().flat_map(|r| r.flatten(true)).count()
};
self.state_mut().tree_state.select_last(visible_count);
}
KeyAction::MovePageUp => {
let state = self.state_mut();
for _ in 0..10 {
state.tree_state.select_previous();
}
}
KeyAction::MovePageDown => {
let visible_count = {
let roots = self.get_tree_roots();
roots.iter().flat_map(|r| r.flatten(true)).count()
};
let state = self.state_mut();
for _ in 0..10 {
state.tree_state.select_next(visible_count);
}
}
KeyAction::ToggleNode => {
let selected_idx = self.state().tree_state.selected;
let roots = self.get_tree_roots_mut();
let visible: Vec<_> = roots.iter_mut().flat_map(|r| r.flatten_mut()).collect();
if let Some(&node_ptr) = visible.get(selected_idx) {
unsafe {
(*node_ptr).toggle();
}
}
}
KeyAction::ExpandNode => {
let selected_idx = self.state().tree_state.selected;
let roots = self.get_tree_roots_mut();
let visible: Vec<_> = roots.iter_mut().flat_map(|r| r.flatten_mut()).collect();
if let Some(&node_ptr) = visible.get(selected_idx) {
unsafe {
(*node_ptr).expand();
}
}
}
KeyAction::CollapseNode => {
let selected_idx = self.state().tree_state.selected;
let roots = self.get_tree_roots_mut();
let visible: Vec<_> = roots.iter_mut().flat_map(|r| r.flatten_mut()).collect();
if let Some(&node_ptr) = visible.get(selected_idx) {
unsafe {
(*node_ptr).collapse();
}
}
}
KeyAction::Save => {
self.save()?;
let state = self.state_mut();
state.modified = false;
state.set_status("Saved!");
}
KeyAction::Quit => {
if !self.can_quit() {
self.state_mut().set_status(
"Error: Unsaved changes. Use :q! to force quit or :wq to save and quit",
);
} else {
self.state_mut().should_quit = true;
}
}
KeyAction::ForceQuit => {
self.state_mut().should_quit = true;
}
KeyAction::SaveAndQuit => {
self.save()?;
let state = self.state_mut();
state.modified = false;
state.should_quit = true;
}
KeyAction::ShowHelp => {
let show_help = self.state().show_help;
self.state_mut().show_help = !show_help;
}
_ => {
self.handle_action(action)?;
}
}
Ok(!self.state().should_quit)
}
fn handle_key_action(&mut self, action: KeyAction) -> Result<bool> {
match action {
KeyAction::Save => {
self.save()?;
self.state_mut().modified = false;
self.state_mut().set_status("Saved!");
}
KeyAction::Quit => {
if !self.can_quit() {
self.state_mut().set_status(
"Error: Unsaved changes. Use :q! to force quit or :wq to save and quit",
);
} else {
self.state_mut().should_quit = true;
}
}
KeyAction::ForceQuit => {
self.state_mut().should_quit = true;
}
KeyAction::SaveAndQuit => {
self.save()?;
self.state_mut().modified = false;
self.state_mut().should_quit = true;
}
_ => {}
}
Ok(!self.state().should_quit)
}
fn render(&mut self, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1), Constraint::Length(1), ])
.split(frame.area());
if self.state().show_help {
let help = HelpPanel::new(self.state().mode.mode);
frame.render_widget(help, chunks[0]);
} else {
self.render_content(frame, chunks[0]);
}
let status = StatusBar::new(self.state().mode.mode)
.message(&self.state().status_message)
.command(self.state().mode.command())
.modified(self.state().modified);
frame.render_widget(status, chunks[1]);
}
fn render_content(&mut self, frame: &mut Frame, area: Rect);
fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
loop {
terminal.draw(|f| self.render(f))?;
if let Some(Event::Key(key)) = super::poll_event()? {
let should_continue = self.handle_key(key)?;
if !should_continue {
break;
}
}
}
Ok(())
}
}