From fac6ea6bcdcaf1d30c27020738d30ab4aa54c87f Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Sat, 17 Jan 2026 14:04:02 -0500 Subject: [PATCH] Create App struct for TUI. --- README.md | 21 ++++++- src/main.rs | 10 ++-- src/tui.rs | 39 ++----------- src/tui/app.rs | 146 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 42 deletions(-) create mode 100644 src/tui/app.rs diff --git a/README.md b/README.md index ad8d381..43c1f54 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ # CLIDE -CLIDE is an IDE written in Rust that supports both full and headless Linux environments. +CLIDE is a barebones but extendable IDE written in Rust using the Qt UI framework that supports both full and headless Linux environments. +The core application will provide you with a text editor that can be extended with plugins written in Rust. + +The UI is written in QML and compiled to C++ using `cxx`, which is then linked into the Rust application. + +It's up to you to build your own development environment for your tools. +This project is intended to be a light-weight core application with no language-specific tools or features. +To add tools for your purposes, create a plugin that implements the `ClidePlugin` trait. (This is currently under development and not yet available.) +Once you've created your plugin, you can submit a pull request to add your plugin to the final section in this README if you'd like to contribute. +If this section becomes too large, we may explore other options to distribute plugins. The following packages must be installed before the application will build. +In the future, we may provide a minimal installation option that only includes dependencies for the headless TUI. ```bash sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick qml6-module-qtquick-dialogs qt6-svg-dev @@ -22,8 +32,8 @@ The [Qt Installer](https://www.qt.io/download-qt-installer) will provide the lat If using RustRover be sure to set your QML binaries path in the settings menu. If Qt was installed to its default directory this will be `$HOME/Qt/6.8.3/gcc_64/bin/`. -Viewing documentation in the web browser is possible, but you will end up in a mess of tabs. -Using Qt Assistant is recommended. It comes with Qt6 when installed. Run the following command to start it. +Viewing documentation in the web browser is possible, but using Qt Assistant is recommended. +It comes with Qt6 when installed. Run the following command to start it. ```bash nohup $HOME/Qt/6.8.3/gcc_64/bin/assistant > /dev/null 2>&1 & @@ -58,3 +68,8 @@ Some helpful links for reading up on QML if you're just getting started. * [All QML Controls Types](https://doc.qt.io/qt-6/qtquick-controls-qmlmodule.html) * [KDAB CXX-Qt Book](https://kdab.github.io/cxx-qt/book/) * [github.com/KDAB/cxx-qt](https://github.com/KDAB/cxx-qt) + + +### Plugins + +TODO: Add a list of plugins here. The first example will be C++ with CMake functionality. diff --git a/src/main.rs b/src/main.rs index bcef94b..2d53646 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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> { 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.")), }; diff --git a/src/tui.rs b/src/tui.rs index 0710a42..3ba419d 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -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 { - 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()); -} - diff --git a/src/tui/app.rs b/src/tui/app.rs new file mode 100644 index 0000000..1d24d6a --- /dev/null +++ b/src/tui/app.rs @@ -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 { + 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); + } +}