TUI #1
140
src/tui/app.rs
140
src/tui/app.rs
@ -1,6 +1,7 @@
|
||||
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};
|
||||
@ -11,23 +12,71 @@ use ratatui::{DefaultTerminal, symbols};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
pub enum AppComponents<'a> {
|
||||
AppEditor(Editor),
|
||||
AppExplorer(Explorer<'a>),
|
||||
AppComponent(Box<dyn Component>),
|
||||
}
|
||||
|
||||
pub struct App<'a> {
|
||||
explorer: Explorer<'a>,
|
||||
editor: Editor,
|
||||
components: Vec<AppComponents<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub(crate) fn new(root_path: PathBuf) -> Self {
|
||||
let mut app = Self {
|
||||
explorer: Explorer::new(&root_path),
|
||||
editor: Editor::new(),
|
||||
components: vec![
|
||||
AppComponents::AppExplorer(Explorer::new(&root_path)),
|
||||
AppComponents::AppEditor(Editor::new()),
|
||||
],
|
||||
};
|
||||
app.editor
|
||||
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<Event> {
|
||||
if !event::poll(Duration::from_millis(250)).expect("event poll failed") {
|
||||
return None;
|
||||
@ -36,7 +85,7 @@ impl<'a> App<'a> {
|
||||
event::read().ok()
|
||||
}
|
||||
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> {
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
// TODO: Handle events based on which component is active.
|
||||
terminal.draw(|f| {
|
||||
@ -66,15 +115,16 @@ impl<'a> App<'a> {
|
||||
|
||||
fn draw_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
// Determine the tab title from the current file (or use a fallback).
|
||||
let title = self
|
||||
.editor
|
||||
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())
|
||||
.unwrap_or("Untitled");
|
||||
}
|
||||
|
||||
Tabs::new(vec![title])
|
||||
Tabs::new(vec![title.unwrap_or("Unknown")])
|
||||
.divider(symbols::DOT)
|
||||
.block(
|
||||
Block::default()
|
||||
@ -102,17 +152,22 @@ impl<'a> App<'a> {
|
||||
|
||||
/// 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) {
|
||||
if let Some(current_file_path) = self.editor.file_path.clone() {
|
||||
if let Some(selected_path_string) = self.explorer.selected() {
|
||||
let selected_pathbuf = PathBuf::from(selected_path_string);
|
||||
if std::path::absolute(&selected_pathbuf).unwrap().is_file()
|
||||
&& selected_pathbuf != current_file_path
|
||||
{
|
||||
self.editor.set_contents(&selected_pathbuf.into()).ok();
|
||||
}
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,12 +204,13 @@ impl<'a> Widget for &mut App<'a> {
|
||||
|
||||
self.draw_status(vertical[0], buf);
|
||||
self.draw_terminal(vertical[2], buf);
|
||||
|
||||
self.explorer.render(horizontal[0], 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();
|
||||
self.editor.render(editor_layout[1], buf);
|
||||
self.refresh_editor_contents()
|
||||
.expect("Failed to refresh editor contents.");
|
||||
self.get_editor_mut().unwrap().render(editor_layout[1], buf);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,34 +225,35 @@ impl<'a> Component for App<'a> {
|
||||
///
|
||||
/// 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 => {
|
||||
// dbg!(format!("Handled event: {:?}", self.id()));
|
||||
return Action::Handled;
|
||||
}
|
||||
Action::Handled => return Action::Handled,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.explorer.handle_event(event.clone());
|
||||
self.editor.handle_event(event.clone());
|
||||
|
||||
// Handle events for all components.
|
||||
// for component in &mut self.components {
|
||||
// dbg!(format!("Handling event: {:?}", component.id()));
|
||||
// // Actions returned here abort the input handling iteration.
|
||||
// match component.handle_event(event.clone()) {
|
||||
// Action::Quit => return Action::Quit,
|
||||
// Action::Handled => return Action::Handled,
|
||||
// _ => continue,
|
||||
// }
|
||||
// }
|
||||
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 {
|
||||
@ -205,10 +262,7 @@ impl<'a> Component for App<'a> {
|
||||
kind: KeyEventKind::Press,
|
||||
state: _state,
|
||||
} => Action::Quit,
|
||||
key_event => {
|
||||
// Pass the key event to each component that can handle it.
|
||||
self.explorer.handle_key_events(key_event)
|
||||
}
|
||||
_ => Action::Noop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::tui::component::{Action, Component};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use edtui::{
|
||||
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
|
||||
};
|
||||
@ -40,9 +40,8 @@ impl Editor {
|
||||
.collect();
|
||||
self.file_path = Some(path.clone());
|
||||
self.state.lines = Lines::new(lines);
|
||||
return Ok(());
|
||||
}
|
||||
Err(anyhow::Error::msg("Failed to set editor file contents"))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
|
||||
@ -102,8 +102,11 @@ impl<'a> Explorer<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<&String> {
|
||||
self.tree_state.selected().last()
|
||||
pub fn selected(&self) -> Result<String> {
|
||||
if let Some(path) = self.tree_state.selected().last() {
|
||||
return Ok(std::path::absolute(path)?.to_str().unwrap().to_string());
|
||||
}
|
||||
Err(anyhow::anyhow!("Failed to get selected TreeItem"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user