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::component::{Action, Component};
|
||||||
use crate::tui::editor::Editor;
|
use crate::tui::editor::Editor;
|
||||||
use crate::tui::explorer::Explorer;
|
use crate::tui::explorer::Explorer;
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::crossterm::event;
|
use ratatui::crossterm::event;
|
||||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
@ -11,23 +12,71 @@ use ratatui::{DefaultTerminal, symbols};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub enum AppComponents<'a> {
|
||||||
|
AppEditor(Editor),
|
||||||
|
AppExplorer(Explorer<'a>),
|
||||||
|
AppComponent(Box<dyn Component>),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App<'a> {
|
pub struct App<'a> {
|
||||||
explorer: Explorer<'a>,
|
components: Vec<AppComponents<'a>>,
|
||||||
editor: Editor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
impl<'a> App<'a> {
|
||||||
pub(crate) fn new(root_path: PathBuf) -> Self {
|
pub(crate) fn new(root_path: PathBuf) -> Self {
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
explorer: Explorer::new(&root_path),
|
components: vec![
|
||||||
editor: Editor::new(),
|
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"))
|
.set_contents(&root_path.join("src/tui/app.rs"))
|
||||||
.expect("Failed to set editor contents.");
|
.expect("Failed to set editor contents.");
|
||||||
app
|
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> {
|
fn get_event(&mut self) -> Option<Event> {
|
||||||
if !event::poll(Duration::from_millis(250)).expect("event poll failed") {
|
if !event::poll(Duration::from_millis(250)).expect("event poll failed") {
|
||||||
return None;
|
return None;
|
||||||
@ -36,7 +85,7 @@ impl<'a> App<'a> {
|
|||||||
event::read().ok()
|
event::read().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> {
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
// TODO: Handle events based on which component is active.
|
// TODO: Handle events based on which component is active.
|
||||||
terminal.draw(|f| {
|
terminal.draw(|f| {
|
||||||
@ -66,15 +115,16 @@ impl<'a> App<'a> {
|
|||||||
|
|
||||||
fn draw_tabs(&self, area: Rect, buf: &mut Buffer) {
|
fn draw_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||||
// Determine the tab title from the current file (or use a fallback).
|
// Determine the tab title from the current file (or use a fallback).
|
||||||
let title = self
|
let mut title: Option<&str> = None;
|
||||||
.editor
|
if let Some(editor) = self.get_editor() {
|
||||||
|
title = editor
|
||||||
.file_path
|
.file_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| p.file_name())
|
.and_then(|p| p.file_name())
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
.unwrap_or("Untitled");
|
}
|
||||||
|
|
||||||
Tabs::new(vec![title])
|
Tabs::new(vec![title.unwrap_or("Unknown")])
|
||||||
.divider(symbols::DOT)
|
.divider(symbols::DOT)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
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.
|
/// 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.
|
/// If the selected item is not a file, this does nothing.
|
||||||
fn refresh_editor_contents(&mut self) {
|
fn refresh_editor_contents(&mut self) -> Result<()> {
|
||||||
if let Some(current_file_path) = self.editor.file_path.clone() {
|
// Use the currently selected TreeItem or get an absolute path to this source file.
|
||||||
if let Some(selected_path_string) = self.explorer.selected() {
|
let selected_pathbuf = match self.get_explorer()?.selected() {
|
||||||
let selected_pathbuf = PathBuf::from(selected_path_string);
|
Ok(path) => PathBuf::from(path),
|
||||||
if std::path::absolute(&selected_pathbuf).unwrap().is_file()
|
Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()),
|
||||||
&& selected_pathbuf != current_file_path
|
};
|
||||||
{
|
let editor = self
|
||||||
self.editor.set_contents(&selected_pathbuf.into()).ok();
|
.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_status(vertical[0], buf);
|
||||||
self.draw_terminal(vertical[2], buf);
|
self.draw_terminal(vertical[2], buf);
|
||||||
|
if let Ok(explorer) = self.get_explorer_mut() {
|
||||||
self.explorer.render(horizontal[0], buf);
|
explorer.render(horizontal[0], buf);
|
||||||
|
}
|
||||||
self.draw_tabs(editor_layout[0], buf);
|
self.draw_tabs(editor_layout[0], buf);
|
||||||
self.refresh_editor_contents();
|
self.refresh_editor_contents()
|
||||||
self.editor.render(editor_layout[1], buf);
|
.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..
|
/// App could then provide helpers for altering Component state based on TUI grouping..
|
||||||
/// (such as editor tabs, file explorer, status bars, etc..)
|
/// (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 {
|
fn handle_event(&mut self, event: Event) -> Action {
|
||||||
// Handle events in the primary application.
|
// Handle events in the primary application.
|
||||||
if let Some(key_event) = event.as_key_event() {
|
if let Some(key_event) = event.as_key_event() {
|
||||||
match self.handle_key_events(key_event) {
|
match self.handle_key_events(key_event) {
|
||||||
Action::Quit => return Action::Quit,
|
Action::Quit => return Action::Quit,
|
||||||
Action::Handled => {
|
Action::Handled => return Action::Handled,
|
||||||
// dbg!(format!("Handled event: {:?}", self.id()));
|
|
||||||
return Action::Handled;
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.explorer.handle_event(event.clone());
|
|
||||||
self.editor.handle_event(event.clone());
|
|
||||||
|
|
||||||
// Handle events for all components.
|
// Handle events for all components.
|
||||||
// for component in &mut self.components {
|
for component in &mut self.components {
|
||||||
// dbg!(format!("Handling event: {:?}", component.id()));
|
let action = match component {
|
||||||
// // Actions returned here abort the input handling iteration.
|
AppComponents::AppEditor(editor) => editor.handle_event(event.clone()),
|
||||||
// match component.handle_event(event.clone()) {
|
AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone()),
|
||||||
// Action::Quit => return Action::Quit,
|
AppComponents::AppComponent(comp) => comp.handle_event(event.clone()),
|
||||||
// Action::Handled => return Action::Handled,
|
};
|
||||||
// _ => continue,
|
// Actions returned here abort the input handling iteration.
|
||||||
// }
|
match action {
|
||||||
// }
|
Action::Quit | Action::Handled => return action,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::Noop
|
Action::Noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles key events for the App Component only.
|
||||||
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
|
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
|
||||||
match key {
|
match key {
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
@ -205,10 +262,7 @@ impl<'a> Component for App<'a> {
|
|||||||
kind: KeyEventKind::Press,
|
kind: KeyEventKind::Press,
|
||||||
state: _state,
|
state: _state,
|
||||||
} => Action::Quit,
|
} => Action::Quit,
|
||||||
key_event => {
|
_ => Action::Noop,
|
||||||
// Pass the key event to each component that can handle it.
|
|
||||||
self.explorer.handle_key_events(key_event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::tui::component::{Action, Component};
|
use crate::tui::component::{Action, Component};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use edtui::{
|
use edtui::{
|
||||||
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
|
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
|
||||||
};
|
};
|
||||||
@ -40,9 +40,8 @@ impl Editor {
|
|||||||
.collect();
|
.collect();
|
||||||
self.file_path = Some(path.clone());
|
self.file_path = Some(path.clone());
|
||||||
self.state.lines = Lines::new(lines);
|
self.state.lines = Lines::new(lines);
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
Err(anyhow::Error::msg("Failed to set editor file contents"))
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<()> {
|
pub fn save(&self) -> Result<()> {
|
||||||
|
|||||||
@ -102,8 +102,11 @@ impl<'a> Explorer<'a> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected(&self) -> Option<&String> {
|
pub fn selected(&self) -> Result<String> {
|
||||||
self.tree_state.selected().last()
|
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