Create App struct for TUI.

This commit is contained in:
2026-01-17 14:04:02 -05:00
parent 7fe3e3e14d
commit fac6ea6bcd
4 changed files with 174 additions and 42 deletions

View File

@@ -8,8 +8,8 @@ pub mod gui;
pub mod tui;
/// Command line interface IDE with full GUI and headless modes.
/// If no flags are provided the GUI editor is launched in a separate process.
/// If no path is provided the current directory is used.
/// If no flags are provided, the GUI editor is launched in a separate process.
/// If no path is provided, the current directory is used.
#[derive(StructOpt, Debug)]
#[structopt(name = "clide", verbatim_doc_comment)]
struct Cli {
@@ -30,11 +30,11 @@ fn main() -> Result<(), Box<dyn Error>> {
let args = Cli::from_args();
let root_path = match args.path {
// 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)?,
// If no path was provided, use current directory.
// 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.
// If we can't find the CWD, attempt to open the home directory.
dirs::home_dir().expect("Failed to access filesystem.")),
};

View File

@@ -1,42 +1,13 @@
pub mod app;
use anyhow::{Context, Result};
use ratatui::{DefaultTerminal, Frame};
use std::error::Error;
use std::time::Duration;
use ratatui::crossterm::event;
use ratatui::crossterm::event::{Event, KeyCode};
use ratatui::widgets::Paragraph;
pub fn start(root_path: std::path::PathBuf) -> Result<()> {
println!("Starting the TUI editor at {:?}", root_path);
let terminal = ratatui::init();
let app_result = run(terminal, root_path).context("Failed to start the TUI editor.");
let app_result = app::App::new(&root_path)
.run(terminal)
.context("Failed to start the TUI editor.");
ratatui::restore();
app_result
}
pub fn run(
mut terminal: DefaultTerminal,
root_path: std::path::PathBuf,
) -> Result<()> {
loop {
terminal.draw(draw)?;
if should_quit()? {
break;
}
}
Ok(())
}
fn should_quit() -> Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}
fn draw(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.area());
}

146
src/tui/app.rs Normal file
View File

@@ -0,0 +1,146 @@
use anyhow::Context;
use ratatui::buffer::Buffer;
use ratatui::crossterm::event;
use ratatui::crossterm::event::{Event, KeyCode};
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::time::Duration;
#[derive(Debug, Clone, Copy)]
pub struct App<'a> {
root_path: &'a std::path::Path,
}
impl<'a> App<'a> {
pub(crate) fn new(root_path: &'a std::path::Path) -> Self {
Self { root_path }
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> {
loop {
terminal.draw(|f| f.render_widget(self, f.area()))?;
if self.should_quit()? {
break;
}
self.handle_events()?;
}
Ok(())
}
fn handle_events(&mut self) -> anyhow::Result<()> {
// Handle other keyboard events here, aside from quitting.
Ok(())
}
fn should_quit(self) -> anyhow::Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}
fn draw_status(self, area: Rect, buf: &mut Buffer) {
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) {
// TODO: Tabs should be opened from file explorer
Tabs::new(["file.md", "file.cpp"])
.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_editor(self, area: Rect, buf: &mut Buffer) {
// TODO: Title should be detected programming language name
// TODO: Content should be file contents
// TODO: Contents should use vim in rendered TTY
// TODO: Vimrc should be used
Paragraph::new("This is an example of the TUI interface (press 'q' to quit)")
.style(Style::default())
.block(
Block::default()
.title("Rust")
.title_style(Style::default().fg(Color::Yellow))
.borders(Borders::ALL)
.padding(Padding::new(0, 0, 0, 1)),
)
.wrap(Wrap { trim: false })
.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("Terminal placeholder")
.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);
}
fn draw_file_explorer(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("File explorer placeholder")
.style(Style::default())
.block(Block::default().borders(Borders::ALL))
.render(area, buf);
}
}
// TODO: Separate complex components into their own widgets.
impl<'a> Widget for 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),
])
.split(horizontal[1]);
self.draw_status(vertical[0], buf);
self.draw_terminal(vertical[2], buf);
self.draw_file_explorer(horizontal[0], buf);
self.draw_tabs(editor_layout[0], buf);
self.draw_editor(editor_layout[1], buf);
}
}