This commit is contained in:
Shaun Reed 2026-01-28 20:34:00 -05:00
parent 00f9075d0f
commit a431d02a0e
12 changed files with 188 additions and 240 deletions

View File

@ -29,7 +29,8 @@ cd clide
cargo install --path . cargo install --path .
``` ```
After installation `clide` can be used directly After installation `clide` can be used directly.
A path can optionally be provided to open a specific directory with `clide /path/to/project`.
```bash ```bash
clide --help clide --help

View File

@ -1,3 +1,4 @@
use crate::AppContext;
use anyhow::Result; use anyhow::Result;
use cxx_qt_lib::QString; use cxx_qt_lib::QString;
use log::trace; use log::trace;
@ -5,8 +6,8 @@ use log::trace;
pub mod colors; pub mod colors;
pub mod filesystem; pub mod filesystem;
pub fn run(root_path: std::path::PathBuf) -> Result<()> { pub fn run(app_context: AppContext) -> Result<()> {
trace!(target:"gui::run()", "Starting the GUI editor at {root_path:?}"); trace!(target:"gui::run()", "Starting the GUI editor at {:?}", app_context.path);
use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl}; use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QUrl};

View File

@ -1,5 +1,4 @@
use crate::tui::Tui; use anyhow::{Context, Result, anyhow};
use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use log::{info, trace}; use log::{info, trace};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
@ -25,41 +24,67 @@ struct Cli {
pub gui: bool, pub gui: bool,
} }
fn main() -> Result<()> { impl Cli {
let args = Cli::parse(); fn run_mode(&self) -> Result<RunMode> {
let mut modes = Vec::new();
self.tui.then(|| modes.push(RunMode::Tui));
self.gui.then(|| modes.push(RunMode::GuiAttached));
match &modes[..] {
[] => Ok(RunMode::Tui),
[mode] => Ok(*mode),
multiple => Err(anyhow!(
"More than one run mode found {multiple:?} please select one."
)),
}
}
}
let root_path = match args.path { pub struct AppContext {
pub path: std::path::PathBuf,
pub run_mode: RunMode,
}
impl AppContext {
fn new(cli: Cli) -> Result<Self> {
let path = match &cli.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)?, Some(path) => std::path::absolute(path)?,
// If no path was provided, use the current directory. // If no path was provided, use the current directory.
None => std::env::current_dir().unwrap_or( None => std::env::current_dir().context("Failed to obtain current directory")?,
// If we can't find the CWD, attempt to open the home directory.
dirs::home_dir().context("Failed to obtain home directory")?,
),
}; };
info!(target:"main()", "Root path detected: {root_path:?}"); info!(target:"main()", "Root path detected: {path:?}");
match args.gui { Ok(Self {
true => { path,
trace!(target:"main()", "Starting GUI"); run_mode: cli.run_mode()?,
gui::run(root_path) })
} }
false => match args.tui {
// Open the TUI editor if requested, otherwise use the QML GUI by default.
true => {
trace!(target:"main()", "Starting TUI");
Ok(Tui::new(root_path)?.start()?)
} }
false => {
#[derive(Copy, Clone, Debug, Default)]
pub enum RunMode {
#[default]
Gui,
GuiAttached,
Tui,
}
fn main() -> Result<()> {
let args = Cli::parse();
let app_context = AppContext::new(args)?;
match app_context.run_mode {
RunMode::GuiAttached => gui::run(app_context),
RunMode::Tui => tui::run(app_context),
RunMode::Gui => {
trace!(target:"main()", "Starting GUI in a new process"); trace!(target:"main()", "Starting GUI in a new process");
Command::new(std::env::current_exe()?) Command::new(std::env::current_exe()?)
.args(&["--gui", root_path.to_str().unwrap()]) .args(&["--gui", app_context.path.to_str().unwrap()])
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.stdin(Stdio::null()) .stdin(Stdio::null())
.spawn()?; .spawn()
Ok(()) .context("Failed to start GUI")
} .map(|_| ())
}, }
} }
} }

View File

@ -22,22 +22,26 @@ use std::io::{Stdout, stdout};
use tui_logger::{ use tui_logger::{
TuiLoggerFile, TuiLoggerLevelOutput, init_logger, set_default_level, set_log_file, TuiLoggerFile, TuiLoggerLevelOutput, init_logger, set_default_level, set_log_file,
}; };
use crate::AppContext;
pub struct Tui { struct Tui {
terminal: Terminal<CrosstermBackend<Stdout>>, terminal: Terminal<CrosstermBackend<Stdout>>,
root_path: std::path::PathBuf, root_path: std::path::PathBuf,
} }
impl Tui { pub fn run(app_context: AppContext) -> Result<()> {
pub fn id() -> &'static str { trace!(target:Tui::ID, "Starting TUI");
"Tui" Tui::new(app_context)?.start()
} }
pub fn new(root_path: std::path::PathBuf) -> Result<Self> { impl Tui {
trace!(target:Self::id(), "Building {}", Self::id()); pub const ID: &str = "Tui";
fn new(app_context: AppContext) -> Result<Self> {
trace!(target:Self::ID, "Building {}", Self::ID);
init_logger(LevelFilter::Trace)?; init_logger(LevelFilter::Trace)?;
set_default_level(LevelFilter::Trace); set_default_level(LevelFilter::Trace);
debug!(target:Self::id(), "Logging initialized"); debug!(target:Self::ID, "Logging initialized");
let mut dir = env::temp_dir(); let mut dir = env::temp_dir();
dir.push("clide.log"); dir.push("clide.log");
@ -49,16 +53,16 @@ impl Tui {
.output_file(false) .output_file(false)
.output_separator(':'); .output_separator(':');
set_log_file(file_options); set_log_file(file_options);
debug!(target:Self::id(), "Logging to file: {dir:?}"); debug!(target:Self::ID, "Logging to file: {dir:?}");
Ok(Self { Ok(Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))?, terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
root_path, root_path: app_context.path,
}) })
} }
pub fn start(self) -> Result<()> { fn start(self) -> Result<()> {
info!(target:Self::id(), "Starting the TUI editor at {:?}", self.root_path); info!(target:Self::ID, "Starting the TUI editor at {:?}", self.root_path);
ratatui::crossterm::execute!( ratatui::crossterm::execute!(
stdout(), stdout(),
EnterAlternateScreen, EnterAlternateScreen,
@ -75,7 +79,7 @@ impl Tui {
} }
fn stop() -> Result<()> { fn stop() -> Result<()> {
info!(target:Self::id(), "Stopping the TUI editor"); info!(target:Self::ID, "Stopping the TUI editor");
disable_raw_mode()?; disable_raw_mode()?;
ratatui::crossterm::execute!( ratatui::crossterm::execute!(
stdout(), stdout(),

View File

@ -1,6 +1,6 @@
use log::info;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap}; use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap};
@ -8,9 +8,7 @@ pub struct About {}
impl About { impl About {
#[allow(unused)] #[allow(unused)]
pub fn id() -> &'static str { pub const ID: &str = "About";
"About"
}
pub fn new() -> Self { pub fn new() -> Self {
// trace!(target:Self::id(), "Building {}", Self::id()); // trace!(target:Self::id(), "Building {}", Self::id());
@ -23,54 +21,51 @@ impl Widget for About {
where where
Self: Sized, Self: Sized,
{ {
Clear::default().render(area, buf); let kilroy_rect = Rect {
// Split main area x: area.x,
y: area.y + 8,
width: 85,
height: 35,
};
info!(target: About::ID, "Created rect: {kilroy_rect:?}");
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Fill(2), // image column Constraint::Fill(1), // Image Layout
Constraint::Fill(1), // image column Constraint::Fill(2), // Description
Constraint::Fill(2), // text column
]) ])
.split(area); .split(area);
let kilroy = [
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{} ", " {}o o{} ",
"-----------------------ooO--(_)--Ooo---------------------------", "-----------------------ooO--(_)--Ooo---------------------------",
"# #", " CLIDE WAS HERE ",
"# CLIDE WAS HERE #",
"# #",
"# https://git.shaunreed.com/shaunred/clide #",
"# https://shaunreed.com/shaunred/clide #",
"# #",
]; ];
let kilroy_lines: Vec<Line> = kilroy.iter().map(|l| Line::from(Span::raw(*l))).collect();
let kilroy_lines: Vec<Line> = kilroy_art let about_text = [
"Clide",
"",
"Author: Shaun Reed",
"Email: shaunrd0@gmail.com",
"URL: https://git.shaunreed.com/shaunrd0/clide",
"Blog: https://shaunreed.com",
"",
"Description:",
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.",
),
];
let about_lines: Vec<Line> = about_text
.iter() .iter()
.map(|l| Line::from(Span::raw(*l))) .map(|l| Line::from(Span::raw(*l)))
.collect(); .collect();
Clear::default().render(area, buf);
Paragraph::new(kilroy_lines) Paragraph::new(kilroy_lines)
.block( .block(
Block::default() Block::default()
@ -79,60 +74,15 @@ impl Widget for About {
) )
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.centered() .centered()
.render(top_chunks[1], buf); .render(kilroy_rect, buf);
Paragraph::new(about_lines)
// ---------- 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(
Block::default() Block::default()
.title("About") .title("About")
.borders(Borders::ALL) .borders(Borders::ALL)
.padding(Padding::top(0)), .padding(Padding::top(0)),
) )
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: false })
.render(chunks[1], buf);
paragraph.render(bottom_chunks[1], buf);
} }
} }

View File

@ -29,7 +29,7 @@ pub enum AppComponent {
} }
pub struct App<'a> { pub struct App<'a> {
editor_tabs: EditorTab, editor_tab: EditorTab,
explorer: Explorer<'a>, explorer: Explorer<'a>,
logger: Logger, logger: Logger,
menu_bar: MenuBar, menu_bar: MenuBar,
@ -38,14 +38,12 @@ pub struct App<'a> {
} }
impl<'a> App<'a> { impl<'a> App<'a> {
pub fn id() -> &'static str { pub const ID: &'static str = "App";
"App"
}
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(None), editor_tab: EditorTab::new(None),
explorer: Explorer::new(&root_path)?, explorer: Explorer::new(&root_path)?,
logger: Logger::new(), logger: Logger::new(),
menu_bar: MenuBar::new(), menu_bar: MenuBar::new(),
@ -57,13 +55,13 @@ impl<'a> App<'a> {
/// Logic that should be executed once on application startup. /// Logic that should be executed once on application startup.
pub fn start(&mut self) -> Result<()> { pub fn start(&mut self) -> Result<()> {
trace!(target:Self::id(), "Starting App"); trace!(target:Self::ID, "Starting App");
Ok(()) Ok(())
} }
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.start()?; self.start()?;
trace!(target:Self::id(), "Entering App run loop"); trace!(target:Self::ID, "Entering App run loop");
loop { loop {
terminal.draw(|f| { terminal.draw(|f| {
f.render_widget(&mut self, f.area()); f.render_widget(&mut self, f.area());
@ -85,11 +83,11 @@ impl<'a> App<'a> {
fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) { fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) {
// Determine help text from the most recently focused component. // Determine help text from the most recently focused component.
let help = match self.last_active { let help = match self.last_active {
AppEditor => match self.editor_tabs.current_editor() { AppEditor => match self.editor_tab.current_editor() {
Some(editor) => editor.component_state.help_text.clone(), Some(editor) => editor.component_state.help_text.clone(),
None => { None => {
if !self.editor_tabs.is_empty() { if !self.editor_tab.is_empty() {
error!(target:Self::id(), "Failed to get Editor while drawing bottom status bar"); error!(target:Self::ID, "Failed to get Editor while drawing bottom status bar");
} }
"Failed to get current Editor while getting widget help text".to_string() "Failed to get current Editor while getting widget help text".to_string()
} }
@ -113,26 +111,26 @@ impl<'a> App<'a> {
} }
fn clear_focus(&mut self) { fn clear_focus(&mut self) {
info!(target:Self::id(), "Clearing all widget focus"); info!(target:Self::ID, "Clearing all widget focus");
self.explorer.component_state.set_focus(Focus::Inactive); self.explorer.component_state.set_focus(Focus::Inactive);
self.explorer.component_state.set_focus(Focus::Inactive); self.explorer.component_state.set_focus(Focus::Inactive);
self.logger.component_state.set_focus(Focus::Inactive); self.logger.component_state.set_focus(Focus::Inactive);
self.menu_bar.component_state.set_focus(Focus::Inactive); self.menu_bar.component_state.set_focus(Focus::Inactive);
match self.editor_tabs.current_editor_mut() { match self.editor_tab.current_editor_mut() {
None => { None => {
error!(target:Self::id(), "Failed to get current Editor while clearing focus") error!(target:Self::ID, "Failed to get current Editor while clearing focus")
} }
Some(editor) => editor.component_state.set_focus(Focus::Inactive), Some(editor) => editor.component_state.set_focus(Focus::Inactive),
} }
} }
fn change_focus(&mut self, focus: AppComponent) { fn change_focus(&mut self, focus: AppComponent) {
info!(target:Self::id(), "Changing widget focus to {:?}", focus); info!(target:Self::ID, "Changing widget focus to {:?}", focus);
self.clear_focus(); self.clear_focus();
match focus { match focus {
AppEditor => match self.editor_tabs.current_editor_mut() { AppEditor => match self.editor_tab.current_editor_mut() {
None => { None => {
error!(target:Self::id(), "Failed to get current Editor while changing focus") error!(target:Self::ID, "Failed to get current Editor while changing focus")
} }
Some(editor) => editor.component_state.set_focus(Focus::Active), Some(editor) => editor.component_state.set_focus(Focus::Active),
}, },
@ -142,32 +140,6 @@ impl<'a> App<'a> {
} }
self.last_active = focus; self.last_active = focus;
} }
/// Refresh the contents of the editor to match the selected TreeItem in the file Explorer.
/// If the selected item is not a file, this does nothing.
#[allow(unused)]
fn refresh_editor_contents(&mut self) -> Result<()> {
// TODO: This may be useful for a preview mode of the selected file prior to opening a tab.
// Use the currently selected TreeItem or get an absolute path to this source file.
// let selected_pathbuf = match self.explorer.selected() {
// Ok(path) => PathBuf::from(path),
// Err(_) => PathBuf::from(std::path::absolute(file!())?.to_string_lossy().to_string()),
// };
// match self.editor_tabs.current_editor_mut() {
// None => bail!("Failed to get current Editor while refreshing editor contents"),
// Some(editor) => {
// let current_file_path = editor
// .file_path
// .clone()
// .context("Failed to get Editor current file_path")?;
// if selected_pathbuf == current_file_path || !selected_pathbuf.is_file() {
// return Ok(());
// }
// editor.set_contents(&selected_pathbuf)
// }
// }
Ok(())
}
} }
impl<'a> Widget for &mut App<'a> { impl<'a> Widget for &mut App<'a> {
@ -225,7 +197,7 @@ impl<'a> Widget for &mut App<'a> {
Constraint::Fill(1), // Editor contents. Constraint::Fill(1), // Editor contents.
]) ])
.split(horizontal[1]); .split(horizontal[1]);
self.editor_tabs self.editor_tab
.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);
} }
@ -237,7 +209,7 @@ impl<'a> Widget for &mut App<'a> {
Constraint::Fill(1), // Editor contents. Constraint::Fill(1), // Editor contents.
]) ])
.split(horizontal[0]); .split(horizontal[0]);
self.editor_tabs self.editor_tab
.render(editor_layout[0], editor_layout[1], buf); .render(editor_layout[0], editor_layout[1], buf);
} }
} }
@ -258,7 +230,7 @@ impl<'a> Widget for &mut App<'a> {
} }
if self.about { if self.about {
let about_area = area.centered(Constraint::Percentage(50), Constraint::Percentage(45)); let about_area = area.centered(Constraint::Percentage(40), Constraint::Percentage(60));
About::new().render(about_area, buf); About::new().render(about_area, buf);
} }
} }
@ -279,7 +251,7 @@ impl<'a> Component for App<'a> {
} }
// Handle events for all components. // Handle events for all components.
let action = match self.last_active { let action = match self.last_active {
AppEditor => self.editor_tabs.handle_event(event.clone())?, AppEditor => self.editor_tab.handle_event(event.clone())?,
AppExplorer => self.explorer.handle_event(event.clone())?, AppExplorer => self.explorer.handle_event(event.clone())?,
AppLogger => self.logger.handle_event(event.clone())?, AppLogger => self.logger.handle_event(event.clone())?,
AppMenuBar => self.menu_bar.handle_event(event.clone())?, AppMenuBar => self.menu_bar.handle_event(event.clone())?,
@ -288,7 +260,7 @@ impl<'a> Component for App<'a> {
// Components should always handle mouse events for click interaction. // Components should always handle mouse events for click interaction.
if let Some(mouse) = event.as_mouse_event() { if let Some(mouse) = event.as_mouse_event() {
if mouse.kind == MouseEventKind::Down(MouseButton::Left) { if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
if let Some(editor) = self.editor_tabs.current_editor_mut() { if let Some(editor) = self.editor_tab.current_editor_mut() {
editor.handle_mouse_events(mouse)?; editor.handle_mouse_events(mouse)?;
} }
self.explorer.handle_mouse_events(mouse)?; self.explorer.handle_mouse_events(mouse)?;
@ -299,15 +271,15 @@ impl<'a> Component for App<'a> {
// Handle actions returned from widgets that may need context on other widgets or app state. // Handle actions returned from widgets that may need context on other widgets or app state.
match action { match action {
Action::Quit | Action::Handled => Ok(action), Action::Quit | Action::Handled => Ok(action),
Action::Save => match self.editor_tabs.current_editor_mut() { Action::Save => match self.editor_tab.current_editor_mut() {
None => { None => {
error!(target:Self::id(), "Failed to get current editor while handling App Action::Save"); error!(target:Self::ID, "Failed to get current editor while handling App Action::Save");
Ok(Action::Noop) Ok(Action::Noop)
} }
Some(editor) => match editor.save() { Some(editor) => match editor.save() {
Ok(_) => Ok(Action::Handled), Ok(_) => Ok(Action::Handled),
Err(e) => { Err(e) => {
error!(target:Self::id(), "Failed to save editor contents: {e}"); error!(target:Self::ID, "Failed to save editor contents: {e}");
Ok(Action::Noop) Ok(Action::Noop)
} }
}, },
@ -315,34 +287,34 @@ impl<'a> Component for App<'a> {
Action::OpenTab => { Action::OpenTab => {
if let Ok(path) = self.explorer.selected() { if let Ok(path) = self.explorer.selected() {
let path_buf = PathBuf::from(path); let path_buf = PathBuf::from(path);
self.editor_tabs.open_tab(&path_buf)?; self.editor_tab.open_tab(&path_buf)?;
Ok(Action::Handled) Ok(Action::Handled)
} else { } else {
Ok(Action::Noop) Ok(Action::Noop)
} }
} }
Action::CloseTab => match self.editor_tabs.close_current_tab() { Action::CloseTab => match self.editor_tab.close_current_tab() {
Ok(_) => Ok(Action::Handled), Ok(_) => Ok(Action::Handled),
Err(_) => Ok(Action::Noop), Err(_) => Ok(Action::Noop),
}, },
Action::ReloadFile => { Action::ReloadFile => {
trace!(target:Self::id(), "Reloading file for current editor"); trace!(target:Self::ID, "Reloading file for current editor");
if let Some(editor) = self.editor_tabs.current_editor_mut() { if let Some(editor) = self.editor_tab.current_editor_mut() {
editor editor
.reload_contents() .reload_contents()
.map(|_| Action::Handled) .map(|_| Action::Handled)
.context("Failed to handle Action::ReloadFile") .context("Failed to handle Action::ReloadFile")
} else { } else {
error!(target:Self::id(), "Failed to get current editor while handling App Action::ReloadFile"); error!(target:Self::ID, "Failed to get current editor while handling App Action::ReloadFile");
Ok(Action::Noop) Ok(Action::Noop)
} }
} }
Action::ShowHideLogger => { Action::ShowHideLogger => {
self.logger.component_state.togget_visible(); self.logger.component_state.toggle_visible();
Ok(Action::Handled) Ok(Action::Handled)
} }
Action::ShowHideExplorer => { Action::ShowHideExplorer => {
self.explorer.component_state.togget_visible(); self.explorer.component_state.toggle_visible();
Ok(Action::Handled) Ok(Action::Handled)
} }
Action::ShowHideAbout => { Action::ShowHideAbout => {

View File

@ -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: Visibility::Visible, vis: self.vis,
help_text: self.help_text, help_text: self.help_text,
} }
} }
@ -142,7 +142,7 @@ pub enum Visibility {
pub trait VisibleState { pub trait VisibleState {
fn with_visible(self, vis: Visibility) -> Self; fn with_visible(self, vis: Visibility) -> Self;
fn set_visible(&mut self, vis: Visibility); fn set_visible(&mut self, vis: Visibility);
fn togget_visible(&mut self); fn toggle_visible(&mut self);
} }
impl VisibleState for ComponentState { impl VisibleState for ComponentState {
@ -158,7 +158,7 @@ impl VisibleState for ComponentState {
self.vis = vis; self.vis = vis;
} }
fn togget_visible(&mut self) { fn toggle_visible(&mut self) {
match self.vis { match self.vis {
Visibility::Visible => self.set_visible(Visibility::Hidden), Visibility::Visible => self.set_visible(Visibility::Hidden),
Visibility::Hidden => self.set_visible(Visibility::Visible), Visibility::Hidden => self.set_visible(Visibility::Visible),

View File

@ -20,12 +20,10 @@ pub struct Editor {
} }
impl Editor { impl Editor {
pub fn id() -> &'static str { pub const ID: &str = "Editor";
"Editor"
}
pub fn new(path: &std::path::PathBuf) -> Self { pub fn new(path: &std::path::PathBuf) -> Self {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::ID, "Building {}", Self::ID);
Editor { Editor {
state: EditorState::default(), state: EditorState::default(),
event_handler: EditorEventHandler::default(), event_handler: EditorEventHandler::default(),
@ -39,10 +37,10 @@ impl Editor {
} }
pub fn reload_contents(&mut self) -> Result<()> { pub fn reload_contents(&mut self) -> Result<()> {
trace!(target:Self::id(), "Reloading editor file contents {:?}", self.file_path); trace!(target:Self::ID, "Reloading editor file contents {:?}", self.file_path);
match self.file_path.clone() { match self.file_path.clone() {
None => { None => {
error!(target:Self::id(), "Failed to reload editor contents with None file_path"); error!(target:Self::ID, "Failed to reload editor contents with None file_path");
bail!("Failed to reload editor contents with None file_path") bail!("Failed to reload editor contents with None file_path")
} }
Some(path) => self.set_contents(&path), Some(path) => self.set_contents(&path),
@ -50,7 +48,7 @@ impl Editor {
} }
pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> { pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> {
trace!(target:Self::id(), "Setting Editor contents from path {:?}", path); trace!(target:Self::ID, "Setting Editor contents from path {:?}", path);
if let Ok(contents) = std::fs::read_to_string(path) { if let Ok(contents) = std::fs::read_to_string(path) {
let lines: Vec<_> = contents let lines: Vec<_> = contents
.lines() .lines()
@ -66,10 +64,10 @@ impl Editor {
pub fn save(&self) -> Result<()> { pub fn save(&self) -> Result<()> {
if let Some(path) = &self.file_path { if let Some(path) = &self.file_path {
trace!(target:Self::id(), "Saving Editor contents {:?}", path); trace!(target:Self::ID, "Saving Editor contents {:?}", path);
return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into()); return std::fs::write(path, self.state.lines.to_string()).map_err(|e| e.into());
}; };
error!(target:Self::id(), "Failed saving Editor contents; file_path was None"); error!(target:Self::ID, "Failed saving Editor contents; file_path was None");
bail!("File not saved. No file path set.") bail!("File not saved. No file path set.")
} }
} }

View File

@ -19,12 +19,10 @@ pub struct EditorTab {
} }
impl EditorTab { impl EditorTab {
fn id() -> &'static str { pub const ID: &str = "EditorTab";
"EditorTab"
}
pub fn new(path: Option<&std::path::PathBuf>) -> Self { pub fn new(path: Option<&std::path::PathBuf>) -> Self {
trace!(target:Self::id(), "Building EditorTab with path {path:?}"); trace!(target:Self::ID, "Building EditorTab with path {path:?}");
match path { match path {
None => Self { None => Self {
editors: HashMap::new(), editors: HashMap::new(),
@ -47,7 +45,7 @@ impl EditorTab {
pub fn next_editor(&mut self) { pub fn next_editor(&mut self) {
let next = (self.current_editor + 1) % self.tab_order.len(); let next = (self.current_editor + 1) % self.tab_order.len();
trace!(target:Self::id(), "Moving from {} to next editor tab at {}", self.current_editor, next); trace!(target:Self::ID, "Moving from {} to next editor tab at {}", self.current_editor, next);
self.set_tab_focus(Focus::Active, next); self.set_tab_focus(Focus::Active, next);
self.current_editor = next; self.current_editor = next;
} }
@ -57,7 +55,7 @@ impl EditorTab {
.current_editor .current_editor
.checked_sub(1) .checked_sub(1)
.unwrap_or(self.tab_order.len() - 1); .unwrap_or(self.tab_order.len() - 1);
trace!(target:Self::id(), "Moving from {} to previous editor tab at {}", self.current_editor, prev); trace!(target:Self::ID, "Moving from {} to previous editor tab at {}", self.current_editor, prev);
self.set_tab_focus(Focus::Active, prev); self.set_tab_focus(Focus::Active, prev);
self.current_editor = prev; self.current_editor = prev;
} }
@ -66,7 +64,7 @@ impl EditorTab {
match self.tab_order.get(index) { match self.tab_order.get(index) {
None => { None => {
if !self.tab_order.is_empty() { if !self.tab_order.is_empty() {
error!(target:Self::id(), "Failed to get editor tab key with invalid index {index}"); error!(target:Self::ID, "Failed to get editor tab key with invalid index {index}");
} }
None None
} }
@ -84,16 +82,16 @@ impl EditorTab {
} }
pub fn set_current_tab_focus(&mut self, focus: Focus) { pub fn set_current_tab_focus(&mut self, focus: Focus) {
trace!(target:Self::id(), "Setting current tab {} focus to {:?}", self.current_editor, focus); trace!(target:Self::ID, "Setting current tab {} focus to {:?}", self.current_editor, focus);
self.set_tab_focus(focus, self.current_editor) self.set_tab_focus(focus, self.current_editor)
} }
pub fn set_tab_focus(&mut self, focus: Focus, index: usize) { pub fn set_tab_focus(&mut self, focus: Focus, index: usize) {
trace!(target:Self::id(), "Setting tab {} focus to {:?}", index, focus); trace!(target:Self::ID, "Setting tab {} focus to {:?}", index, focus);
if focus == Focus::Active && index != self.current_editor { if focus == Focus::Active && index != self.current_editor {
// If we are setting another tab to active, disable the current one. // If we are setting another tab to active, disable the current one.
trace!( trace!(
target:Self::id(), target:Self::ID,
"New tab {} focus set to Active; Setting current tab {} to Inactive", "New tab {} focus set to Active; Setting current tab {} to Inactive",
index, index,
self.current_editor self.current_editor
@ -102,12 +100,12 @@ impl EditorTab {
} }
match self.get_editor_key(index) { match self.get_editor_key(index) {
None => { None => {
error!(target:Self::id(), "Failed setting tab focus for invalid key {index}"); error!(target:Self::ID, "Failed setting tab focus for invalid key {index}");
} }
Some(key) => match self.editors.get_mut(&key) { Some(key) => match self.editors.get_mut(&key) {
None => { None => {
error!( error!(
target:Self::id(), target:Self::ID,
"Failed to update tab focus at index {} with invalid key: {}", "Failed to update tab focus at index {} with invalid key: {}",
self.current_editor, self.current_editor,
self.tab_order[self.current_editor] self.tab_order[self.current_editor]
@ -119,12 +117,12 @@ impl EditorTab {
} }
pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> { pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> {
trace!(target:Self::id(), "Opening new EditorTab with path {:?}", path); trace!(target:Self::ID, "Opening new EditorTab with path {:?}", path);
if self if self
.editors .editors
.contains_key(&path.to_string_lossy().to_string()) .contains_key(&path.to_string_lossy().to_string())
{ {
warn!(target:Self::id(), "EditorTab already opened with this file"); warn!(target:Self::ID, "EditorTab already opened with this file");
return Ok(()); return Ok(());
} }
@ -151,12 +149,12 @@ impl EditorTab {
.to_owned(); .to_owned();
match self.editors.remove(&key) { match self.editors.remove(&key) {
None => { None => {
error!(target:Self::id(), "Failed to remove editor tab {key} with invalid index {index}") error!(target:Self::ID, "Failed to remove editor tab {key} with invalid index {index}")
} }
Some(_) => { Some(_) => {
self.prev_editor(); self.prev_editor();
self.tab_order.remove(index); self.tab_order.remove(index);
info!(target:Self::id(), "Closed editor tab {key} at index {index}") info!(target:Self::ID, "Closed editor tab {key} at index {index}")
} }
} }
Ok(()) Ok(())

View File

@ -20,12 +20,10 @@ pub struct Explorer<'a> {
} }
impl<'a> Explorer<'a> { impl<'a> Explorer<'a> {
pub fn id() -> &'static str { pub const ID: &'static str = "Explorer";
"Explorer"
}
pub fn new(path: &PathBuf) -> Result<Self> { pub fn new(path: &PathBuf) -> Result<Self> {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::ID, "Building {}", Self::ID);
let explorer = Explorer { let explorer = Explorer {
root_path: path.to_owned(), root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path.to_owned())?, tree_items: Self::build_tree_from_path(path.to_owned())?,

View File

@ -15,12 +15,10 @@ pub struct Logger {
} }
impl Logger { impl Logger {
pub fn id() -> &'static str { pub const ID: &str = "Logger";
"Logger"
}
pub fn new() -> Self { pub fn new() -> Self {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::ID, "Building {}", Self::ID);
let state = TuiWidgetState::new(); let state = TuiWidgetState::new();
state.transition(TuiWidgetEvent::HideKey); state.transition(TuiWidgetEvent::HideKey);
Self { Self {

View File

@ -2,6 +2,7 @@ use crate::tui::component::{Action, Component, ComponentState, FocusState};
use crate::tui::menu_bar::MenuBarItemOption::{ use crate::tui::menu_bar::MenuBarItemOption::{
About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger, About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
}; };
use anyhow::Context;
use log::trace; use log::trace;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent}; use ratatui::crossterm::event::{KeyCode, KeyEvent};
@ -83,13 +84,11 @@ pub struct MenuBar {
} }
impl MenuBar { impl MenuBar {
pub fn id() -> &'static str { pub const ID: &str = "MenuBar";
"MenuBar"
}
const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection"; const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection";
pub fn new() -> Self { pub fn new() -> Self {
trace!(target:Self::id(), "Building {}", Self::id()); trace!(target:Self::ID, "Building {}", Self::ID);
Self { Self {
selected: MenuBarItem::File, selected: MenuBarItem::File,
opened: None, opened: None,
@ -154,7 +153,7 @@ impl MenuBar {
height, height,
}; };
// TODO: X offset for item option? It's fine as-is, but it might look nicer. // TODO: X offset for item option? It's fine as-is, but it might look nicer.
// trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect); // trace!(target:Self::ID, "Building Rect under MenuBar popup {}", rect);
rect rect
} }
} }
@ -192,7 +191,11 @@ impl Component for MenuBar {
} }
KeyCode::Enter => { KeyCode::Enter => {
if let Some(selected) = self.list_state.selected() { if let Some(selected) = self.list_state.selected() {
let selection = self.selected.options()[selected]; let selection = self
.selected
.options()
.get(selected)
.context("Failed to get selected MenuBar option")?;
return match selection { return match selection {
Save => Ok(Action::Save), Save => Ok(Action::Save),
Exit => Ok(Action::Quit), Exit => Ok(Action::Quit),