[tui] Remove most usage of expect().

Still not quite sure what to do about some pieces in QML bindings for
the GUI.
This commit is contained in:
Shaun Reed 2026-01-20 17:19:13 -05:00
parent ce2949159c
commit 42a40fe7f3
6 changed files with 95 additions and 91 deletions

View File

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

View File

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

View File

@ -1,7 +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 anyhow::{Context, 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};
@ -23,18 +23,21 @@ pub struct App<'a> {
} }
impl<'a> 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 { let mut app = Self {
components: vec![ components: vec![
AppComponents::AppExplorer(Explorer::new(&root_path)), AppComponents::AppExplorer(Explorer::new(&root_path)?),
AppComponents::AppEditor(Editor::new()), AppComponents::AppEditor(Editor::new()),
], ],
}; };
app.get_editor_mut() app.get_editor_mut()
.unwrap() .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."); .context(format!(
app "Failed to initialize editor contents to path: {}",
root_path.to_string_lossy()
))?;
Ok(app)
} }
fn get_explorer(&self) -> Result<&Explorer<'a>> { fn get_explorer(&self) -> Result<&Explorer<'a>> {
@ -77,27 +80,22 @@ impl<'a> App<'a> {
None 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<()> { pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
loop { loop {
// TODO: Handle events based on which component is active. self.refresh_editor_contents()
.context("Failed to refresh editor contents.")?;
terminal.draw(|f| { terminal.draw(|f| {
f.render_widget(&mut self, f.area()); f.render_widget(&mut self, f.area());
})?; })?;
if let Some(event) = self.get_event() { // TODO: Handle events based on which component is active.
match self.handle_event(event) { if event::poll(Duration::from_millis(250)).context("event poll failed")? {
match self.handle_event(event::read()?)? {
Action::Quit => break, Action::Quit => break,
Action::Handled => {} Action::Handled => {}
_ => { _ => {
// panic!("Unhandled event: {:?}", event); // anyhow::anyhow!("Unhandled event: {:?}", event);
} }
} }
} }
@ -160,7 +158,7 @@ impl<'a> App<'a> {
}; };
let editor = self let editor = self
.get_editor_mut() .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 let Some(current_file_path) = editor.file_path.clone() {
if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() { if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() {
return Ok(()); return Ok(());
@ -208,8 +206,6 @@ impl<'a> Widget for &mut App<'a> {
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()
.expect("Failed to refresh editor contents.");
self.get_editor_mut().unwrap().render(editor_layout[1], buf); 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..) /// (such as editor tabs, file explorer, status bars, etc..)
/// ///
/// Handles events for the App and delegates to attached Components. /// 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. // 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) { let res = self
Action::Quit => return Action::Quit, .handle_key_events(key_event)
Action::Handled => return Action::Handled, .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. // Handle events for all components.
for component in &mut self.components { for component in &mut self.components {
let action = match component { let action = match component {
AppComponents::AppEditor(editor) => editor.handle_event(event.clone()), AppComponents::AppEditor(editor) => editor.handle_event(event.clone())?,
AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone()), AppComponents::AppExplorer(explorer) => explorer.handle_event(event.clone())?,
AppComponents::AppComponent(comp) => comp.handle_event(event.clone()), AppComponents::AppComponent(comp) => comp.handle_event(event.clone())?,
}; };
// Actions returned here abort the input handling iteration. // Actions returned here abort the input handling iteration.
match action { 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. /// 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 { match key {
KeyEvent { KeyEvent {
code: KeyCode::Char('c'), code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => Action::Quit, } => Ok(Action::Quit),
_ => Action::Noop, _ => Ok(Action::Noop),
} }
} }
} }

View File

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

View File

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