[tui] Add About page within Help MenuBar.
This commit is contained in:
parent
76fe09f39b
commit
ae9f787c81
10
README.md
10
README.md
@ -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.
|
||||||
|
|||||||
@ -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
139
src/tui/about.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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'),
|
||||||
|
|||||||
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user