TUI #1

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

View File

@ -1,15 +1,11 @@
# CLIDE # CLIDE
CLIDE is a barebones but extendable IDE written in Rust using the Qt UI framework that supports both full and headless Linux environments. CLIDE is an extendable command-line driven development environment 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 GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate.
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. 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.) 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. Once you've created your plugin, you can submit a pull request to add a link to the git repository for 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. 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. In the future, we may provide a minimal installation option that only includes dependencies for the headless TUI.

View File

@ -1,3 +1,4 @@
mod about;
mod app; mod app;
mod component; mod component;
mod editor; mod editor;

139
src/tui/about.rs Normal file
View File

@ -0,0 +1,139 @@
use log::trace;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap};
pub struct About {}
impl About {
#[allow(unused)]
pub fn id() -> &'static str {
"About"
}
pub fn new() -> Self {
// trace!(target:Self::id(), "Building {}", Self::id());
Self {}
}
}
impl Widget for About {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
Clear::default().render(area, buf);
// Split main area
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Fill(2), // image column
Constraint::Fill(1), // image column
Constraint::Fill(2), // text column
])
.split(area);
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Fill(3),
Constraint::Fill(1),
])
.split(chunks[1]);
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Fill(1),
Constraint::Fill(3),
Constraint::Fill(1),
])
.split(chunks[2]);
// ---------- IMAGE ----------
let kilroy_art = [
" * ",
" |.===. ",
" {}o o{} ",
"-----------------------ooO--(_)--Ooo---------------------------",
"# #",
"# CLIDE WAS HERE #",
"# #",
"# https://git.shaunreed.com/shaunred/clide #",
"# https://shaunreed.com/shaunred/clide #",
"# #",
];
let kilroy_lines: Vec<Line> = kilroy_art
.iter()
.map(|l| Line::from(Span::raw(*l)))
.collect();
Paragraph::new(kilroy_lines)
.block(
Block::default()
.borders(Borders::NONE)
.padding(Padding::bottom(0)),
)
.wrap(Wrap { trim: false })
.centered()
.render(top_chunks[1], buf);
// ---------- TEXT ----------
let about_text = vec![
Line::from(vec![Span::styled(
"clide\n",
Style::default().add_modifier(Modifier::BOLD),
)])
.centered(),
Line::from(""),
Line::from(vec![
Span::styled("Author: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("Shaun Reed"),
])
.left_aligned(),
Line::from(vec![
Span::styled("Email: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("shaunrd0@gmail.com"),
])
.left_aligned(),
Line::from(vec![
Span::styled("URL: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("https://git.shaunreed.com/shaunrd0/clide"),
])
.left_aligned(),
Line::from(vec![
Span::styled("Blog: ", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("https://shaunreed.com"),
])
.left_aligned(),
Line::from(""),
Line::from(vec![Span::styled(
"Description\n",
Style::default().add_modifier(Modifier::BOLD),
)])
.left_aligned(),
Line::from(concat!(
"CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. ",
"The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. ",
))
.style(Style::default())
.left_aligned(),
];
Block::bordered().render(area, buf);
let paragraph = Paragraph::new(about_text)
.block(
Block::default()
.title("About")
.borders(Borders::ALL)
.padding(Padding::top(0)),
)
.wrap(Wrap { trim: true });
paragraph.render(bottom_chunks[1], buf);
}
}

View File

@ -1,5 +1,6 @@
use crate::tui::about::About;
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger}; use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState, Visible, VisibleState}; use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState};
use crate::tui::editor_tab::EditorTab; use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
@ -36,6 +37,7 @@ pub struct App<'a> {
logger: Logger, logger: Logger,
menu_bar: MenuBar, menu_bar: MenuBar,
last_active: AppComponent, last_active: AppComponent,
about: bool,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
@ -46,11 +48,12 @@ impl<'a> App<'a> {
pub fn new(root_path: PathBuf) -> Result<Self> { pub fn new(root_path: PathBuf) -> Result<Self> {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::id(), "Building {}", Self::id());
let app = Self { let app = Self {
editor_tabs: EditorTab::new(&root_path), editor_tabs: EditorTab::new(&root_path.join("src/tui/app.rs")),
explorer: Explorer::new(&root_path)?, explorer: Explorer::new(&root_path)?,
logger: Logger::new(), logger: Logger::new(),
menu_bar: MenuBar::new(), menu_bar: MenuBar::new(),
last_active: AppEditor, last_active: AppEditor,
about: false,
}; };
Ok(app) Ok(app)
} }
@ -187,7 +190,7 @@ impl<'a> Widget for &mut App<'a> {
Self: Sized, Self: Sized,
{ {
let vertical_constraints = match self.logger.component_state.vis { let vertical_constraints = match self.logger.component_state.vis {
Visible::Visible => { Visibility::Visible => {
vec![ vec![
Constraint::Length(3), // top status bar Constraint::Length(3), // top status bar
Constraint::Percentage(70), // horizontal layout Constraint::Percentage(70), // horizontal layout
@ -195,7 +198,7 @@ impl<'a> Widget for &mut App<'a> {
Constraint::Length(3), // bottom status bar Constraint::Length(3), // bottom status bar
] ]
} }
Visible::Hidden => { Visibility::Hidden => {
vec![ vec![
Constraint::Length(3), // top status bar Constraint::Length(3), // top status bar
Constraint::Fill(1), // horizontal layout Constraint::Fill(1), // horizontal layout
@ -209,13 +212,13 @@ impl<'a> Widget for &mut App<'a> {
.split(area); .split(area);
let horizontal_constraints = match self.explorer.component_state.vis { let horizontal_constraints = match self.explorer.component_state.vis {
Visible::Visible => { Visibility::Visible => {
vec![ vec![
Constraint::Max(30), // File explorer with a max width of 30 characters. Constraint::Max(30), // File explorer with a max width of 30 characters.
Constraint::Fill(1), // Editor fills the remaining space. Constraint::Fill(1), // Editor fills the remaining space.
] ]
} }
Visible::Hidden => { Visibility::Hidden => {
vec![ vec![
Constraint::Fill(1), // Editor fills the remaining space. Constraint::Fill(1), // Editor fills the remaining space.
] ]
@ -228,7 +231,7 @@ impl<'a> Widget for &mut App<'a> {
.constraints(horizontal_constraints) .constraints(horizontal_constraints)
.split(vertical[1]); .split(vertical[1]);
match self.explorer.component_state.vis { match self.explorer.component_state.vis {
Visible::Visible => { Visibility::Visible => {
let editor_layout = Layout::default() let editor_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
@ -240,7 +243,7 @@ impl<'a> Widget for &mut App<'a> {
.render(editor_layout[0], editor_layout[1], buf); .render(editor_layout[0], editor_layout[1], buf);
self.explorer.render(horizontal[0], buf); self.explorer.render(horizontal[0], buf);
} }
Visible::Hidden => { Visibility::Hidden => {
let editor_layout = Layout::default() let editor_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
@ -255,18 +258,23 @@ impl<'a> Widget for &mut App<'a> {
match self.logger.component_state.vis { match self.logger.component_state.vis {
// Index 1 of vertical is rendered with the horizontal layout above. // Index 1 of vertical is rendered with the horizontal layout above.
Visible::Visible => { Visibility::Visible => {
self.logger.render(vertical[2], buf); self.logger.render(vertical[2], buf);
self.draw_bottom_status(vertical[3], buf); self.draw_bottom_status(vertical[3], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus. // The title bar is rendered last to overlay any popups created for drop-down menus.
self.menu_bar.render(vertical[0], buf); self.menu_bar.render(vertical[0], buf);
} }
Visible::Hidden => { Visibility::Hidden => {
self.draw_bottom_status(vertical[2], buf); self.draw_bottom_status(vertical[2], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus. // The title bar is rendered last to overlay any popups created for drop-down menus.
self.menu_bar.render(vertical[0], buf); self.menu_bar.render(vertical[0], buf);
} }
} }
if self.about {
let about_area = area.centered(Constraint::Percentage(50), Constraint::Percentage(45));
About::new().render(about_area, buf);
}
} }
} }
@ -351,12 +359,27 @@ impl<'a> Component for App<'a> {
self.explorer.component_state.togget_visible(); self.explorer.component_state.togget_visible();
Ok(Action::Handled) Ok(Action::Handled)
} }
Action::ShowHideAbout => {
self.about = !self.about;
Ok(Action::Handled)
}
_ => Ok(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) -> Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
match key.code {
// If the ESC key is pressed with the About page open, hide it.
KeyCode::Esc | KeyCode::Char('q') => {
if self.about {
self.about = false;
return Ok(Action::Handled);
}
}
_ => {}
}
match key { match key {
KeyEvent { KeyEvent {
code: KeyCode::Char('q'), code: KeyCode::Char('q'),

View File

@ -27,7 +27,7 @@ pub enum Action {
ReloadFile, ReloadFile,
ShowHideExplorer, ShowHideExplorer,
ShowHideLogger, ShowHideLogger,
About, ShowHideAbout,
CloseTab, CloseTab,
} }
@ -60,7 +60,7 @@ pub trait Component {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ComponentState { pub struct ComponentState {
pub(crate) focus: Focus, pub(crate) focus: Focus,
pub(crate) vis: Visible, pub(crate) vis: Visibility,
pub(crate) help_text: String, pub(crate) help_text: String,
} }
@ -73,7 +73,7 @@ impl ComponentState {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::id(), "Building {}", Self::id());
Self { Self {
focus: Active, focus: Active,
vis: Visible::Visible, vis: Visibility::Visible,
help_text: String::new(), help_text: String::new(),
} }
} }
@ -111,7 +111,7 @@ impl FocusState for ComponentState {
fn with_focus(self, focus: Focus) -> Self { fn with_focus(self, focus: Focus) -> Self {
Self { Self {
focus, focus,
vis: Visible::Visible, vis: Visibility::Visible,
help_text: self.help_text, help_text: self.help_text,
} }
} }
@ -133,20 +133,20 @@ impl FocusState for ComponentState {
} }
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum Visible { pub enum Visibility {
#[default] #[default]
Visible, Visible,
Hidden, Hidden,
} }
pub trait VisibleState { pub trait VisibleState {
fn with_visible(self, vis: Visible) -> Self; fn with_visible(self, vis: Visibility) -> Self;
fn set_visible(&mut self, vis: Visible); fn set_visible(&mut self, vis: Visibility);
fn togget_visible(&mut self); fn togget_visible(&mut self);
} }
impl VisibleState for ComponentState { impl VisibleState for ComponentState {
fn with_visible(self, vis: Visible) -> Self { fn with_visible(self, vis: Visibility) -> Self {
Self { Self {
focus: self.focus, focus: self.focus,
vis, vis,
@ -154,14 +154,14 @@ impl VisibleState for ComponentState {
} }
} }
fn set_visible(&mut self, vis: Visible) { fn set_visible(&mut self, vis: Visibility) {
self.vis = vis; self.vis = vis;
} }
fn togget_visible(&mut self) { fn togget_visible(&mut self) {
match self.vis { match self.vis {
Visible::Visible => self.set_visible(Visible::Hidden), Visibility::Visible => self.set_visible(Visibility::Hidden),
Visible::Hidden => self.set_visible(Visible::Visible), Visibility::Hidden => self.set_visible(Visibility::Visible),
} }
} }
} }

View File

@ -199,7 +199,7 @@ impl Component for MenuBar {
Reload => Ok(Action::ReloadFile), Reload => Ok(Action::ReloadFile),
ShowHideExplorer => Ok(Action::ShowHideExplorer), ShowHideExplorer => Ok(Action::ShowHideExplorer),
ShowHideLogger => Ok(Action::ShowHideLogger), ShowHideLogger => Ok(Action::ShowHideLogger),
About => Ok(Action::About), About => Ok(Action::ShowHideAbout),
CloseTab => Ok(Action::CloseTab), CloseTab => Ok(Action::CloseTab),
}; };
} }