From ae9f787c81510eb4347f3d738c39e092aba87091 Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Sun, 25 Jan 2026 14:57:34 -0500 Subject: [PATCH] [tui] Add About page within Help MenuBar. --- README.md | 10 +--- src/tui.rs | 1 + src/tui/about.rs | 139 +++++++++++++++++++++++++++++++++++++++++++ src/tui/app.rs | 43 +++++++++---- src/tui/component.rs | 22 +++---- src/tui/menu_bar.rs | 2 +- 6 files changed, 188 insertions(+), 29 deletions(-) create mode 100644 src/tui/about.rs diff --git a/README.md b/README.md index 43c1f54..1335086 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ # CLIDE -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. +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. 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. +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. 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. diff --git a/src/tui.rs b/src/tui.rs index 3199e70..3a0127d 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,3 +1,4 @@ +mod about; mod app; mod component; mod editor; diff --git a/src/tui/about.rs b/src/tui/about.rs new file mode 100644 index 0000000..e7a7acc --- /dev/null +++ b/src/tui/about.rs @@ -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 = 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); + } +} diff --git a/src/tui/app.rs b/src/tui/app.rs index 7d5752e..04932c5 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,5 +1,6 @@ +use crate::tui::about::About; 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::explorer::Explorer; use crate::tui::logger::Logger; @@ -36,6 +37,7 @@ pub struct App<'a> { logger: Logger, menu_bar: MenuBar, last_active: AppComponent, + about: bool, } impl<'a> App<'a> { @@ -46,11 +48,12 @@ impl<'a> App<'a> { pub fn new(root_path: PathBuf) -> Result { trace!(target:Self::id(), "Building {}", Self::id()); 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)?, logger: Logger::new(), menu_bar: MenuBar::new(), last_active: AppEditor, + about: false, }; Ok(app) } @@ -187,7 +190,7 @@ impl<'a> Widget for &mut App<'a> { Self: Sized, { let vertical_constraints = match self.logger.component_state.vis { - Visible::Visible => { + Visibility::Visible => { vec![ Constraint::Length(3), // top status bar Constraint::Percentage(70), // horizontal layout @@ -195,7 +198,7 @@ impl<'a> Widget for &mut App<'a> { Constraint::Length(3), // bottom status bar ] } - Visible::Hidden => { + Visibility::Hidden => { vec![ Constraint::Length(3), // top status bar Constraint::Fill(1), // horizontal layout @@ -209,13 +212,13 @@ impl<'a> Widget for &mut App<'a> { .split(area); let horizontal_constraints = match self.explorer.component_state.vis { - Visible::Visible => { + Visibility::Visible => { vec![ Constraint::Max(30), // File explorer with a max width of 30 characters. Constraint::Fill(1), // Editor fills the remaining space. ] } - Visible::Hidden => { + Visibility::Hidden => { vec![ Constraint::Fill(1), // Editor fills the remaining space. ] @@ -228,7 +231,7 @@ impl<'a> Widget for &mut App<'a> { .constraints(horizontal_constraints) .split(vertical[1]); match self.explorer.component_state.vis { - Visible::Visible => { + Visibility::Visible => { let editor_layout = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -240,7 +243,7 @@ impl<'a> Widget for &mut App<'a> { .render(editor_layout[0], editor_layout[1], buf); self.explorer.render(horizontal[0], buf); } - Visible::Hidden => { + Visibility::Hidden => { let editor_layout = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -255,18 +258,23 @@ impl<'a> Widget for &mut App<'a> { match self.logger.component_state.vis { // Index 1 of vertical is rendered with the horizontal layout above. - Visible::Visible => { + Visibility::Visible => { self.logger.render(vertical[2], buf); self.draw_bottom_status(vertical[3], buf); // The title bar is rendered last to overlay any popups created for drop-down menus. self.menu_bar.render(vertical[0], buf); } - Visible::Hidden => { + Visibility::Hidden => { self.draw_bottom_status(vertical[2], buf); // The title bar is rendered last to overlay any popups created for drop-down menus. 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(); Ok(Action::Handled) } + Action::ShowHideAbout => { + self.about = !self.about; + Ok(Action::Handled) + } _ => Ok(Action::Noop), } } /// Handles key events for the App Component only. fn handle_key_events(&mut self, key: KeyEvent) -> Result { + 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 { KeyEvent { code: KeyCode::Char('q'), diff --git a/src/tui/component.rs b/src/tui/component.rs index fac8a97..5a119fe 100644 --- a/src/tui/component.rs +++ b/src/tui/component.rs @@ -27,7 +27,7 @@ pub enum Action { ReloadFile, ShowHideExplorer, ShowHideLogger, - About, + ShowHideAbout, CloseTab, } @@ -60,7 +60,7 @@ pub trait Component { #[derive(Debug, Clone, Default)] pub struct ComponentState { pub(crate) focus: Focus, - pub(crate) vis: Visible, + pub(crate) vis: Visibility, pub(crate) help_text: String, } @@ -73,7 +73,7 @@ impl ComponentState { trace!(target:Self::id(), "Building {}", Self::id()); Self { focus: Active, - vis: Visible::Visible, + vis: Visibility::Visible, help_text: String::new(), } } @@ -111,7 +111,7 @@ impl FocusState for ComponentState { fn with_focus(self, focus: Focus) -> Self { Self { focus, - vis: Visible::Visible, + vis: Visibility::Visible, help_text: self.help_text, } } @@ -133,20 +133,20 @@ impl FocusState for ComponentState { } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub enum Visible { +pub enum Visibility { #[default] Visible, Hidden, } pub trait VisibleState { - fn with_visible(self, vis: Visible) -> Self; - fn set_visible(&mut self, vis: Visible); + fn with_visible(self, vis: Visibility) -> Self; + fn set_visible(&mut self, vis: Visibility); fn togget_visible(&mut self); } impl VisibleState for ComponentState { - fn with_visible(self, vis: Visible) -> Self { + fn with_visible(self, vis: Visibility) -> Self { Self { focus: self.focus, 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; } fn togget_visible(&mut self) { match self.vis { - Visible::Visible => self.set_visible(Visible::Hidden), - Visible::Hidden => self.set_visible(Visible::Visible), + Visibility::Visible => self.set_visible(Visibility::Hidden), + Visibility::Hidden => self.set_visible(Visibility::Visible), } } } diff --git a/src/tui/menu_bar.rs b/src/tui/menu_bar.rs index ac4d3b2..bc11bb0 100644 --- a/src/tui/menu_bar.rs +++ b/src/tui/menu_bar.rs @@ -199,7 +199,7 @@ impl Component for MenuBar { Reload => Ok(Action::ReloadFile), ShowHideExplorer => Ok(Action::ShowHideExplorer), ShowHideLogger => Ok(Action::ShowHideLogger), - About => Ok(Action::About), + About => Ok(Action::ShowHideAbout), CloseTab => Ok(Action::CloseTab), }; }