use crate::tui::component::{Action, Component}; use crate::tui::editor::Editor; use crate::tui::explorer::Explorer; use anyhow::{Result, anyhow}; use ratatui::buffer::Buffer; use ratatui::crossterm::event; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::prelude::{Color, Style, Widget}; use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap}; use ratatui::{DefaultTerminal, symbols}; use std::path::PathBuf; use std::time::Duration; pub enum AppComponents<'a> { AppEditor(Editor), AppExplorer(Explorer<'a>), AppComponent(Box), } pub struct App<'a> { components: Vec>, } impl<'a> App<'a> { pub(crate) fn new(root_path: PathBuf) -> Self { let mut app = Self { components: vec![ AppComponents::AppExplorer(Explorer::new(&root_path)), AppComponents::AppEditor(Editor::new()), ], }; app.get_editor_mut() .unwrap() .set_contents(&root_path.join("src/tui/app.rs")) .expect("Failed to set editor contents."); app } fn get_explorer(&self) -> Result<&Explorer<'a>> { for component in &self.components { if let AppComponents::AppExplorer(explorer) = component { return Ok(explorer); } } Err(anyhow::anyhow!("Failed to find project explorer widget.")) } fn get_explorer_mut(&mut self) -> Result<&mut Explorer<'a>> { for component in &mut self.components { if let AppComponents::AppExplorer(explorer) = component { return Ok(explorer); } } Err(anyhow::anyhow!("Failed to find project explorer widget.")) } fn get_editor(&self) -> Option<&Editor> { for component in &self.components { if let AppComponents::AppEditor(editor) = component { return Some(editor); } } // There is no editor currently opened. None } fn get_editor_mut(&mut self) -> Option<&mut Editor> { for component in &mut self.components { if let AppComponents::AppEditor(editor) = component { return Some(editor); } } // There is no editor currently opened. None } fn get_event(&mut self) -> Option { if !event::poll(Duration::from_millis(250)).expect("event poll failed") { return None; } event::read().ok() } pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { loop { // TODO: Handle events based on which component is active. terminal.draw(|f| { f.render_widget(&mut self, f.area()); })?; if let Some(event) = self.get_event() { match self.handle_event(event) { Action::Quit => break, Action::Handled => {} _ => { // panic!("Unhandled event: {:?}", event); } } } } Ok(()) } fn draw_status(&self, area: Rect, buf: &mut Buffer) { // TODO: Status bar should have drop down menus Tabs::new(["File", "Edit", "View", "Help"]) .style(Style::default()) .block(Block::default().borders(Borders::ALL)) .render(area, buf); } fn draw_tabs(&self, area: Rect, buf: &mut Buffer) { // Determine the tab title from the current file (or use a fallback). let mut title: Option<&str> = None; if let Some(editor) = self.get_editor() { title = editor .file_path .as_ref() .and_then(|p| p.file_name()) .and_then(|s| s.to_str()) } Tabs::new(vec![title.unwrap_or("Unknown")]) .divider(symbols::DOT) .block( Block::default() .borders(Borders::NONE) .padding(Padding::new(0, 0, 0, 0)), ) .highlight_style(Style::default().fg(Color::LightRed)) .render(area, buf); } fn draw_terminal(&self, area: Rect, buf: &mut Buffer) { // TODO: Title should be detected shell name // TODO: Contents should be shell output Paragraph::new("shaun@pc:~/Code/clide$ ") .style(Style::default()) .block( Block::default() .title("Bash") .title_style(Style::default().fg(Color::DarkGray)) .borders(Borders::ALL), ) .wrap(Wrap { trim: false }) .render(area, buf); } /// Refresh the contents of the editor to match the selected TreeItem in the file Explorer. /// If the selected item is not a file, this does nothing. fn refresh_editor_contents(&mut self) -> Result<()> { // Use the currently selected TreeItem or get an absolute path to this source file. let selected_pathbuf = match self.get_explorer()?.selected() { Ok(path) => PathBuf::from(path), Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()), }; let editor = self .get_editor_mut() .expect("Failed to get active editor while refreshing contents."); if let Some(current_file_path) = editor.file_path.clone() { if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() { return Ok(()); } return editor.set_contents(&selected_pathbuf); } Err(anyhow!("Failed to refresh editor contents")) } } // TODO: Separate complex components into their own widgets. impl<'a> Widget for &mut App<'a> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { let vertical = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // status bar Constraint::Percentage(70), // horizontal layout Constraint::Percentage(30), // terminal ]) .split(area); let horizontal = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Max(30), // File explorer with a max width of 30 characters. Constraint::Fill(1), // Editor fills the remaining space. ]) .split(vertical[1]); let editor_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // Editor tabs. Constraint::Fill(1), // Editor contents. ]) .split(horizontal[1]); self.draw_status(vertical[0], buf); self.draw_terminal(vertical[2], buf); if let Ok(explorer) = self.get_explorer_mut() { explorer.render(horizontal[0], buf); } self.draw_tabs(editor_layout[0], buf); self.refresh_editor_contents() .expect("Failed to refresh editor contents."); self.get_editor_mut().unwrap().render(editor_layout[1], buf); } } impl<'a> Component for App<'a> { fn id(&self) -> &str { "app" } /// TODO: Get active widget with some Component trait function helper? /// trait Component { fn get_state() -> ComponentState; } /// if component.get_state() = ComponentState::Active { component.handle_event(); } /// /// App could then provide helpers for altering Component state based on TUI grouping.. /// (such as editor tabs, file explorer, status bars, etc..) /// /// Handles events for the App and delegates to attached Components. fn handle_event(&mut self, event: Event) -> Action { // Handle events in the primary application. if let Some(key_event) = event.as_key_event() { match self.handle_key_events(key_event) { Action::Quit => return Action::Quit, Action::Handled => return Action::Handled, _ => {} } } // Handle events for all components. for component in &mut self.components { let action = match component { AppComponents::AppEditor(editor) => editor.handle_event(event.clone()), AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone()), AppComponents::AppComponent(comp) => comp.handle_event(event.clone()), }; // Actions returned here abort the input handling iteration. match action { Action::Quit | Action::Handled => return action, _ => {} } } Action::Noop } /// Handles key events for the App Component only. fn handle_key_events(&mut self, key: KeyEvent) -> Action { match key { KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: _state, } => Action::Quit, _ => Action::Noop, } } }