TUI #1
15
src/tui.rs
15
src/tui.rs
@ -7,7 +7,7 @@ mod logger;
|
||||
mod menu_bar;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::{LevelFilter, debug, info};
|
||||
use log::{LevelFilter, debug, info, trace};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::crossterm::event::{
|
||||
@ -28,10 +28,15 @@ pub struct Tui {
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn id() -> &'static str {
|
||||
"Tui"
|
||||
}
|
||||
|
||||
pub fn new(root_path: std::path::PathBuf) -> Result<Self> {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
init_logger(LevelFilter::Trace)?;
|
||||
set_default_level(LevelFilter::Trace);
|
||||
debug!(target:"Tui", "Logging initialized");
|
||||
debug!(target:Self::id(), "Logging initialized");
|
||||
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push("clide.log");
|
||||
@ -43,7 +48,7 @@ impl Tui {
|
||||
.output_file(false)
|
||||
.output_separator(':');
|
||||
set_log_file(file_options);
|
||||
debug!(target:"Tui", "Logging to file: {dir:?}");
|
||||
debug!(target:Self::id(), "Logging to file: {dir:?}");
|
||||
|
||||
Ok(Self {
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
|
||||
@ -52,7 +57,7 @@ impl Tui {
|
||||
}
|
||||
|
||||
pub fn start(self) -> Result<()> {
|
||||
info!(target:"Tui", "Starting the TUI editor at {:?}", self.root_path);
|
||||
info!(target:Self::id(), "Starting the TUI editor at {:?}", self.root_path);
|
||||
ratatui::crossterm::execute!(
|
||||
stdout(),
|
||||
EnterAlternateScreen,
|
||||
@ -69,7 +74,7 @@ impl Tui {
|
||||
}
|
||||
|
||||
fn stop() -> Result<()> {
|
||||
info!(target:"Tui", "Stopping the TUI editor");
|
||||
info!(target:Self::id(), "Stopping the TUI editor");
|
||||
disable_raw_mode()?;
|
||||
ratatui::crossterm::execute!(
|
||||
stdout(),
|
||||
|
||||
@ -6,7 +6,7 @@ use crate::tui::logger::Logger;
|
||||
use crate::tui::menu_bar::MenuBar;
|
||||
use AppComponent::AppMenuBar;
|
||||
use anyhow::{Context, Result};
|
||||
use log::error;
|
||||
use log::{error, info, trace, warn};
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event;
|
||||
@ -22,7 +22,7 @@ use std::time::Duration;
|
||||
// TODO: Need a way to dynamically run Widget::render on all widgets.
|
||||
// TODO: + Need a way to map Rect to Component::id() to position each widget?
|
||||
// TODO: Need a good way to dynamically run Component methods on all widgets.
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum AppComponent {
|
||||
AppEditor,
|
||||
AppExplorer,
|
||||
@ -44,6 +44,7 @@ impl<'a> App<'a> {
|
||||
}
|
||||
|
||||
pub fn new(root_path: PathBuf) -> Result<Self> {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
let app = Self {
|
||||
editor_tabs: EditorTab::new(&root_path),
|
||||
explorer: Explorer::new(&root_path)?,
|
||||
@ -56,6 +57,7 @@ impl<'a> App<'a> {
|
||||
|
||||
/// Logic that should be executed once on application startup.
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
trace!(target:Self::id(), "Starting App");
|
||||
let root_path = self.explorer.root_path.clone();
|
||||
let editor = self
|
||||
.editor_tabs
|
||||
@ -72,10 +74,8 @@ impl<'a> App<'a> {
|
||||
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
self.start()?;
|
||||
trace!(target:Self::id(), "Entering App run loop");
|
||||
loop {
|
||||
self.refresh_editor_contents()
|
||||
.context("Failed to refresh editor contents.")?;
|
||||
|
||||
terminal.draw(|f| {
|
||||
f.render_widget(&mut self, f.area());
|
||||
})?;
|
||||
@ -122,6 +122,7 @@ impl<'a> App<'a> {
|
||||
}
|
||||
|
||||
fn change_focus(&mut self, focus: AppComponent) {
|
||||
info!(target:Self::id(), "Changing widget focus to {:?}", focus);
|
||||
match focus {
|
||||
AppEditor => match self.editor_tabs.current_editor_mut() {
|
||||
None => {
|
||||
@ -138,6 +139,7 @@ impl<'a> App<'a> {
|
||||
|
||||
/// 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.
|
||||
@ -242,8 +244,8 @@ impl<'a> Component for App<'a> {
|
||||
Action::Quit | Action::Handled => Ok(action),
|
||||
Action::Save => match editor.save() {
|
||||
Ok(_) => Ok(Action::Handled),
|
||||
Err(_) => {
|
||||
error!(target:Self::id(), "Failed to save editor contents");
|
||||
Err(e) => {
|
||||
error!(target:Self::id(), "Failed to save editor contents: {e}");
|
||||
Ok(Action::Noop)
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
use anyhow::Result;
|
||||
use log::trace;
|
||||
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
|
||||
|
||||
pub enum Action {
|
||||
@ -55,7 +56,12 @@ pub struct ComponentState {
|
||||
}
|
||||
|
||||
impl ComponentState {
|
||||
pub fn id() -> &'static str {
|
||||
"ComponentState"
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
Self {
|
||||
focus: Focus::Active,
|
||||
help_text: String::new(),
|
||||
|
||||
@ -3,6 +3,7 @@ use anyhow::{Context, Result, bail};
|
||||
use edtui::{
|
||||
EditorEventHandler, EditorState, EditorTheme, EditorView, LineNumbers, Lines, SyntaxHighlighter,
|
||||
};
|
||||
use log::{error, trace};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
@ -25,6 +26,7 @@ impl Editor {
|
||||
|
||||
// TODO: You shouldnt be able to construct the editor without a path?
|
||||
pub fn new() -> Self {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
Editor {
|
||||
state: EditorState::default(),
|
||||
event_handler: EditorEventHandler::default(),
|
||||
@ -38,6 +40,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn set_contents(&mut self, path: &std::path::PathBuf) -> Result<()> {
|
||||
trace!(target:Self::id(), "Setting Editor contents from path {:?}", path);
|
||||
if let Ok(contents) = std::fs::read_to_string(path) {
|
||||
let lines: Vec<_> = contents
|
||||
.lines()
|
||||
@ -53,8 +56,10 @@ impl Editor {
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
if let Some(path) = &self.file_path {
|
||||
trace!(target:Self::id(), "Saving Editor contents {:?}", path);
|
||||
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");
|
||||
bail!("File not saved. No file path set.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::tui::component::{Action, Component};
|
||||
use crate::tui::editor::Editor;
|
||||
use anyhow::{Context, Result};
|
||||
use log::trace;
|
||||
use log::{trace, warn};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use ratatui::layout::Rect;
|
||||
@ -24,7 +24,7 @@ impl EditorTab {
|
||||
}
|
||||
|
||||
pub fn new(path: &std::path::PathBuf) -> Self {
|
||||
trace!(target:Self::id(), "Building EditorTab with path '{path:?}'");
|
||||
trace!(target:Self::id(), "Building EditorTab with path {path:?}");
|
||||
let tab_order = vec![path.to_string_lossy().to_string()];
|
||||
Self {
|
||||
editors: HashMap::from([(tab_order.first().unwrap().to_owned(), Editor::new())]),
|
||||
@ -34,14 +34,18 @@ impl EditorTab {
|
||||
}
|
||||
|
||||
pub fn next_editor(&mut self) {
|
||||
self.current_editor = (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);
|
||||
self.current_editor = next;
|
||||
}
|
||||
|
||||
pub fn prev_editor(&mut self) {
|
||||
self.current_editor = self
|
||||
let prev = self
|
||||
.current_editor
|
||||
.checked_sub(1)
|
||||
.unwrap_or(self.tab_order.len() - 1);
|
||||
trace!(target:Self::id(), "Moving from {} to previous editor tab at {}", self.current_editor, prev);
|
||||
self.current_editor = prev;
|
||||
}
|
||||
|
||||
pub fn current_editor(&self) -> Option<&Editor> {
|
||||
@ -53,10 +57,12 @@ impl EditorTab {
|
||||
}
|
||||
|
||||
pub fn open_tab(&mut self, path: &std::path::PathBuf) -> Result<()> {
|
||||
trace!(target:Self::id(), "Opening new EditorTab with path {:?}", path);
|
||||
if self
|
||||
.editors
|
||||
.contains_key(&path.to_string_lossy().to_string())
|
||||
{
|
||||
warn!(target:Self::id(), "EditorTab already opened with this file");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use log::trace;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
|
||||
use ratatui::layout::{Alignment, Position, Rect};
|
||||
@ -24,6 +25,7 @@ impl<'a> Explorer<'a> {
|
||||
}
|
||||
|
||||
pub fn new(path: &PathBuf) -> Result<Self> {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
let explorer = Explorer {
|
||||
root_path: path.to_owned(),
|
||||
tree_items: Self::build_tree_from_path(path.to_owned())?,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use crate::tui::component::{Action, Component, ComponentState, Focus};
|
||||
use log::trace;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent};
|
||||
use ratatui::layout::Rect;
|
||||
@ -19,6 +20,7 @@ impl Logger {
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
let state = TuiWidgetState::new();
|
||||
state.transition(TuiWidgetEvent::HideKey);
|
||||
Self {
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::tui::component::{Action, Component, ComponentState};
|
||||
use crate::tui::menu_bar::MenuBarItemOption::{
|
||||
About, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,
|
||||
};
|
||||
use log::trace;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::layout::Rect;
|
||||
@ -80,8 +81,13 @@ pub struct MenuBar {
|
||||
}
|
||||
|
||||
impl MenuBar {
|
||||
pub fn id() -> &'static str {
|
||||
"MenuBar"
|
||||
}
|
||||
|
||||
const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection";
|
||||
pub fn new() -> Self {
|
||||
trace!(target:Self::id(), "Building {}", Self::id());
|
||||
Self {
|
||||
selected: MenuBarItem::File,
|
||||
opened: None,
|
||||
@ -136,12 +142,14 @@ impl MenuBar {
|
||||
|
||||
fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect {
|
||||
// TODO: X offset for item option? It's fine as-is, but it might look nicer.
|
||||
Rect {
|
||||
let rect = Rect {
|
||||
x: anchor.x,
|
||||
y: anchor.y + anchor.height,
|
||||
width: width.min(area.width),
|
||||
height,
|
||||
}
|
||||
};
|
||||
trace!(target:Self::id(), "Building Rect under MenuBar popup {}", rect);
|
||||
rect
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user