TUI #1

Merged
shaunrd0 merged 73 commits from ui into master 2026-01-25 20:57:37 +00:00
6 changed files with 95 additions and 91 deletions
Showing only changes of commit 42a40fe7f3 - Show all commits

View File

@ -1,6 +1,10 @@
use anyhow::Result;
use anyhow::{Context, Result};
use clap::Parser;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use std::io::stdout;
use std::process::{Command, Stdio};
use crate::tui::Tui;
pub mod gui;
pub mod tui;
@ -30,16 +34,17 @@ fn main() -> Result<()> {
// If the CLI was provided a directory, convert it to absolute.
Some(path) => std::path::absolute(path)?,
// If no path was provided, use the current directory.
None => std::env::current_dir().unwrap_or_else(|_|
// If we can't find the CWD, attempt to open the home directory.
dirs::home_dir().expect("Failed to access filesystem.")),
None => std::env::current_dir().unwrap_or(
// If we can't find the CWD, attempt to open the home directory.
dirs::home_dir().context("Failed to obtain home directory")?,
),
};
match args.gui {
true => gui::run(root_path),
false => match args.tui {
// Open the TUI editor if requested, otherwise use the QML GUI by default.
true => Ok(tui::Tui::new(root_path).start()?),
true => Ok(Tui::new(root_path)?.start()?),
false => {
// Relaunch the CLIDE GUI in a separate process.
Command::new(std::env::current_exe()?)

View File

@ -20,12 +20,11 @@ pub struct Tui {
}
impl Tui {
pub fn new(root_path: std::path::PathBuf) -> Self {
Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))
.expect("Failed to initialize terminal"),
pub fn new(root_path: std::path::PathBuf) -> Result<Self> {
Ok(Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
root_path,
}
})
}
pub fn start(self) -> Result<()> {
@ -38,7 +37,7 @@ impl Tui {
)?;
enable_raw_mode()?;
let app_result = app::App::new(self.root_path)
let app_result = app::App::new(self.root_path)?
.run(self.terminal)
.context("Failed to start the TUI editor.");
Self::stop()?;

View File

@ -1,7 +1,7 @@
use crate::tui::component::{Action, Component};
use crate::tui::editor::Editor;
use crate::tui::explorer::Explorer;
use anyhow::{Result, anyhow};
use anyhow::{Context, Result, anyhow};
use ratatui::buffer::Buffer;
use ratatui::crossterm::event;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
@ -23,18 +23,21 @@ pub struct App<'a> {
}
impl<'a> App<'a> {
pub(crate) fn new(root_path: PathBuf) -> Self {
pub fn new(root_path: PathBuf) -> Result<Self> {
let mut app = Self {
components: vec![
AppComponents::AppExplorer(Explorer::new(&root_path)),
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
.context(format!(
"Failed to initialize editor contents to path: {}",
root_path.to_string_lossy()
))?;
Ok(app)
}
fn get_explorer(&self) -> Result<&Explorer<'a>> {
@ -77,27 +80,22 @@ impl<'a> App<'a> {
None
}
fn get_event(&mut self) -> Option<Event> {
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.
self.refresh_editor_contents()
.context("Failed to refresh editor contents.")?;
terminal.draw(|f| {
f.render_widget(&mut self, f.area());
})?;
if let Some(event) = self.get_event() {
match self.handle_event(event) {
// TODO: Handle events based on which component is active.
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
match self.handle_event(event::read()?)? {
Action::Quit => break,
Action::Handled => {}
_ => {
// panic!("Unhandled event: {:?}", event);
// anyhow::anyhow!("Unhandled event: {:?}", event);
}
}
}
@ -160,7 +158,7 @@ impl<'a> App<'a> {
};
let editor = self
.get_editor_mut()
.expect("Failed to get active editor while refreshing contents.");
.context("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(());
@ -208,8 +206,6 @@ impl<'a> Widget for &mut App<'a> {
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);
}
}
@ -227,12 +223,14 @@ impl<'a> Component for App<'a> {
/// (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) -> Result<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,
let res = self
.handle_key_events(key_event)
.context("Failed to handle key events for primary App Component.");
match res {
Ok(Action::Quit) | Ok(Action::Handled) => return res,
_ => {}
}
}
@ -240,29 +238,29 @@ impl<'a> Component for App<'a> {
// 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()),
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::Quit | Action::Handled => return Ok(action),
_ => {}
}
}
Action::Noop
Ok(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) -> Result<Action> {
match key {
KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: _state,
} => Action::Quit,
_ => Action::Noop,
} => Ok(Action::Quit),
_ => Ok(Action::Noop),
}
}
}

View File

@ -1,5 +1,6 @@
#![allow(dead_code, unused_variables)]
use anyhow::Result;
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
pub enum Action {
@ -25,22 +26,22 @@ pub trait Component {
/// This is used for lookup in a container of Components.
fn id(&self) -> &str;
fn handle_event(&mut self, event: Event) -> Action {
fn handle_event(&mut self, event: Event) -> Result<Action> {
match event {
Event::Key(key_event) => self.handle_key_events(key_event),
_ => Action::Noop,
_ => Ok(Action::Noop),
}
}
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
Action::Noop
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
Ok(Action::Noop)
}
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Action {
Action::Noop
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Action> {
Ok(Action::Noop)
}
fn update(&mut self, action: Action) -> Action {
Action::Noop
fn update(&mut self, action: Action) -> Result<Action> {
Ok(Action::Noop)
}
}

View File

@ -90,32 +90,32 @@ impl Component for Editor {
"editor"
}
fn handle_event(&mut self, event: Event) -> Action {
fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler.
match self.handle_key_events(key_event) {
Action::Handled => return Action::Handled,
match self.handle_key_events(key_event)? {
Action::Handled => return Ok(Action::Handled),
_ => {}
}
}
self.event_handler.on_event(event, &mut self.state);
Action::Pass
Ok(Action::Pass)
}
/// The events for the vim emulation should be handled by EditorEventHandler::on_event.
/// These events are custom to the clide application.
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
match key {
KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
..
} => {
self.save().expect("Failed to save file.");
Action::Handled
self.save().context("Failed to save file.")?;
Ok(Action::Handled)
}
// For other events not handled here, pass to the vim emulation handler.
_ => Action::Noop,
_ => Ok(Action::Noop),
}
}
}

View File

@ -1,5 +1,5 @@
use crate::tui::component::{Action, Component};
use anyhow::Result;
use anyhow::{Context, Result};
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect};
@ -17,33 +17,36 @@ pub struct Explorer<'a> {
}
impl<'a> Explorer<'a> {
pub fn new(path: &std::path::PathBuf) -> Self {
pub fn new(path: &std::path::PathBuf) -> Result<Self> {
let explorer = Explorer {
root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path.to_owned()),
tree_items: Self::build_tree_from_path(path.to_owned())?,
tree_state: TreeState::default(),
};
explorer
Ok(explorer)
}
fn build_tree_from_path(path: std::path::PathBuf) -> TreeItem<'static, String> {
fn build_tree_from_path(path: std::path::PathBuf) -> Result<TreeItem<'static, String>> {
let mut children = vec![];
if let Ok(entries) = fs::read_dir(&path) {
let mut paths = entries
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()
.expect("");
.context(format!(
"Failed to build vector of paths under directory: {:?}",
path
))?;
paths.sort();
for path in paths {
if path.is_dir() {
children.push(Self::build_tree_from_path(path));
children.push(Self::build_tree_from_path(path)?);
} else {
if let Ok(path) = std::path::absolute(&path) {
let path_str = path.to_string_lossy().to_string();
children.push(TreeItem::new_leaf(
path_str,
path.file_name()
.expect("Failed to get file name from path.")
.context("Failed to get file name from path.")?
.to_string_lossy()
.to_string(),
));
@ -53,13 +56,10 @@ impl<'a> Explorer<'a> {
}
let abs = std::path::absolute(&path)
.expect(
format!(
"Failed to find absolute path for TreeItem: {}",
path.to_string_lossy().to_string()
)
.as_str(),
)
.context(format!(
"Failed to find absolute path for TreeItem: {:?}",
path
))?
.to_string_lossy()
.to_string();
TreeItem::new(
@ -70,13 +70,13 @@ impl<'a> Explorer<'a> {
.to_string(),
children,
)
.expect("Failed to build tree from path.")
.context("Failed to build tree from path.")
}
pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
pub fn render(&mut self, area: Rect, buf: &mut Buffer) -> Result<()> {
StatefulWidget::render(
Tree::new(&self.tree_items.children())
.expect("Failed to build tree.")
.context("Failed to build file Explorer Tree.")?
.style(Style::default())
.block(
Block::default()
@ -84,7 +84,7 @@ impl<'a> Explorer<'a> {
.title(
self.root_path
.file_name()
.expect("Failed to get file name from path.")
.context("Failed to get file name from path.")?
.to_string_lossy(),
)
.title_style(Style::default().fg(Color::Green))
@ -99,7 +99,8 @@ impl<'a> Explorer<'a> {
area,
buf,
&mut self.tree_state,
)
);
Ok(())
}
pub fn selected(&self) -> Result<String> {
@ -114,24 +115,24 @@ impl<'a> Component for Explorer<'a> {
fn id(&self) -> &str {
"explorer"
}
fn handle_event(&mut self, event: Event) -> Action {
fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler.
match self.handle_key_events(key_event) {
Action::Handled => return Action::Handled,
match self.handle_key_events(key_event)? {
Action::Handled => return Ok(Action::Handled),
_ => {}
}
}
if let Some(mouse_event) = event.as_mouse_event() {
match self.handle_mouse_events(mouse_event) {
Action::Handled => return Action::Handled,
match self.handle_mouse_events(mouse_event)? {
Action::Handled => return Ok(Action::Handled),
_ => {}
}
}
Action::Pass
Ok(Action::Pass)
}
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
let changed = match key.code {
KeyCode::Up => self.tree_state.key_up(),
KeyCode::Char('k') => self.tree_state.key_up(),
@ -145,12 +146,12 @@ impl<'a> Component for Explorer<'a> {
_ => false,
};
if changed {
return Action::Handled;
return Ok(Action::Handled);
}
Action::Noop
Ok(Action::Noop)
}
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Action {
fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Result<Action> {
let changed = match mouse.kind {
MouseEventKind::ScrollDown => self.tree_state.scroll_down(1),
MouseEventKind::ScrollUp => self.tree_state.scroll_up(1),
@ -160,8 +161,8 @@ impl<'a> Component for Explorer<'a> {
_ => false,
};
if changed {
return Action::Handled;
return Ok(Action::Handled);
}
Action::Noop
Ok(Action::Noop)
}
}