TUI #1

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

View File

@ -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.

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);
}
}