2026-01-17 17:09:42 -05:00
|
|
|
use crate::tui::component::{Action, ClideComponent};
|
2026-01-18 10:09:28 -05:00
|
|
|
use crate::tui::editor::Editor;
|
2026-01-17 17:09:42 -05:00
|
|
|
use crate::tui::explorer::Explorer;
|
2026-01-17 14:04:02 -05:00
|
|
|
use ratatui::buffer::Buffer;
|
2026-01-18 10:09:28 -05:00
|
|
|
use ratatui::crossterm::event;
|
|
|
|
|
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
|
|
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
2026-01-17 14:04:02 -05:00
|
|
|
use ratatui::prelude::{Color, Style, Widget};
|
|
|
|
|
use ratatui::widgets::{Block, Borders, Padding, Paragraph, Tabs, Wrap};
|
|
|
|
|
use ratatui::{DefaultTerminal, symbols};
|
2026-01-18 10:09:28 -05:00
|
|
|
use std::time::Duration;
|
2026-01-17 15:07:05 -05:00
|
|
|
|
2026-01-17 14:04:02 -05:00
|
|
|
pub struct App<'a> {
|
2026-01-17 15:07:05 -05:00
|
|
|
explorer: Explorer<'a>,
|
2026-01-18 10:09:28 -05:00
|
|
|
editor: Editor,
|
2026-01-17 14:04:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> App<'a> {
|
|
|
|
|
pub(crate) fn new(root_path: &'a std::path::Path) -> Self {
|
2026-01-17 15:07:05 -05:00
|
|
|
Self {
|
|
|
|
|
explorer: Explorer::new(root_path),
|
2026-01-18 10:09:28 -05:00
|
|
|
editor: Editor::new(),
|
2026-01-17 15:07:05 -05:00
|
|
|
}
|
2026-01-17 14:04:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-18 10:09:28 -05:00
|
|
|
fn get_event(&mut self) -> Option<Event> {
|
|
|
|
|
if !event::poll(Duration::from_millis(250)).expect("event poll failed") {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event::read().ok()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 14:04:02 -05:00
|
|
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> anyhow::Result<()> {
|
|
|
|
|
loop {
|
2026-01-17 17:09:42 -05:00
|
|
|
terminal.draw(|f| {
|
2026-01-18 10:09:28 -05:00
|
|
|
f.render_widget(&mut self, f.area());
|
2026-01-17 17:09:42 -05:00
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// TODO: Handle events based on which component is active.
|
2026-01-18 10:09:28 -05:00
|
|
|
if let Some(event) = self.get_event() {
|
|
|
|
|
self.editor
|
|
|
|
|
.event_handler
|
|
|
|
|
.on_event(event.clone(), &mut self.editor.state);
|
|
|
|
|
|
|
|
|
|
match event {
|
|
|
|
|
Event::FocusGained => {}
|
|
|
|
|
Event::FocusLost => {}
|
|
|
|
|
Event::Key(key_event) => {
|
|
|
|
|
// Handle main application key events.
|
|
|
|
|
match self.handle_key_events(key_event) {
|
|
|
|
|
Action::Noop => {}
|
|
|
|
|
Action::Quit => break,
|
|
|
|
|
Action::Pass => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(_) => {}
|
|
|
|
|
Event::Paste(_) => {}
|
|
|
|
|
Event::Resize(_, _) => {}
|
|
|
|
|
}
|
2026-01-17 14:04:02 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 17:09:42 -05:00
|
|
|
fn draw_status(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
|
// TODO: Status bar should have drop down menus
|
2026-01-17 14:04:02 -05:00
|
|
|
Tabs::new(["File", "Edit", "View", "Help"])
|
|
|
|
|
.style(Style::default())
|
|
|
|
|
.block(Block::default().borders(Borders::ALL))
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 17:09:42 -05:00
|
|
|
fn draw_tabs(&self, area: Rect, buf: &mut Buffer) {
|
2026-01-17 14:04:02 -05:00
|
|
|
// TODO: Tabs should be opened from file explorer
|
|
|
|
|
Tabs::new(["file.md", "file.cpp"])
|
|
|
|
|
.divider(symbols::DOT)
|
|
|
|
|
.block(
|
|
|
|
|
Block::default()
|
|
|
|
|
.borders(Borders::NONE)
|
|
|
|
|
.padding(Padding::new(0, 0, 0, 0)),
|
|
|
|
|
)
|
|
|
|
|
.highlight_style(Style::default().fg(Color::LightRed))
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 17:09:42 -05:00
|
|
|
fn draw_terminal(&self, area: Rect, buf: &mut Buffer) {
|
2026-01-17 14:04:02 -05:00
|
|
|
// TODO: Title should be detected shell name
|
|
|
|
|
// TODO: Contents should be shell output
|
2026-01-17 15:07:05 -05:00
|
|
|
Paragraph::new("shaun@pc:~/Code/clide$ ")
|
2026-01-17 14:04:02 -05:00
|
|
|
.style(Style::default())
|
|
|
|
|
.block(
|
|
|
|
|
Block::default()
|
|
|
|
|
.title("Bash")
|
|
|
|
|
.title_style(Style::default().fg(Color::DarkGray))
|
|
|
|
|
.borders(Borders::ALL),
|
|
|
|
|
)
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Separate complex components into their own widgets.
|
2026-01-18 10:09:28 -05:00
|
|
|
impl<'a> Widget for &mut App<'a> {
|
2026-01-17 14:04:02 -05:00
|
|
|
fn render(self, area: Rect, buf: &mut Buffer)
|
|
|
|
|
where
|
|
|
|
|
Self: Sized,
|
|
|
|
|
{
|
|
|
|
|
let vertical = Layout::default()
|
|
|
|
|
.direction(Direction::Vertical)
|
|
|
|
|
.constraints([
|
|
|
|
|
Constraint::Length(3), // status bar
|
|
|
|
|
Constraint::Percentage(70), // horizontal layout
|
|
|
|
|
Constraint::Percentage(30), // terminal
|
|
|
|
|
])
|
|
|
|
|
.split(area);
|
|
|
|
|
|
|
|
|
|
let horizontal = Layout::default()
|
|
|
|
|
.direction(Direction::Horizontal)
|
|
|
|
|
.constraints([
|
|
|
|
|
Constraint::Max(30), // File explorer with a max width of 30 characters.
|
|
|
|
|
Constraint::Fill(1), // Editor fills the remaining space.
|
|
|
|
|
])
|
|
|
|
|
.split(vertical[1]);
|
|
|
|
|
|
|
|
|
|
let editor_layout = Layout::default()
|
|
|
|
|
.direction(Direction::Vertical)
|
|
|
|
|
.constraints([
|
|
|
|
|
Constraint::Length(1), // Editor tabs.
|
2026-01-17 17:09:42 -05:00
|
|
|
Constraint::Fill(1), // Editor contents.
|
2026-01-17 14:04:02 -05:00
|
|
|
])
|
|
|
|
|
.split(horizontal[1]);
|
|
|
|
|
|
|
|
|
|
self.draw_status(vertical[0], buf);
|
|
|
|
|
self.draw_terminal(vertical[2], buf);
|
|
|
|
|
|
2026-01-17 17:09:42 -05:00
|
|
|
self.explorer.render(horizontal[0], buf);
|
2026-01-17 14:04:02 -05:00
|
|
|
|
|
|
|
|
self.draw_tabs(editor_layout[0], buf);
|
2026-01-18 10:09:28 -05:00
|
|
|
self.editor.render(editor_layout[1], buf);
|
2026-01-17 14:04:02 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 17:09:42 -05:00
|
|
|
|
2026-01-18 10:09:28 -05:00
|
|
|
impl<'a> ClideComponent for App<'a> {
|
|
|
|
|
fn handle_key_events(&mut self, key: KeyEvent) -> Action {
|
|
|
|
|
match key {
|
|
|
|
|
KeyEvent {
|
|
|
|
|
code: KeyCode::Char('c'),
|
|
|
|
|
modifiers: KeyModifiers::CONTROL,
|
|
|
|
|
kind: KeyEventKind::Press,
|
|
|
|
|
state: _state,
|
|
|
|
|
} => Action::Quit,
|
|
|
|
|
key_event => {
|
|
|
|
|
// Pass the key event to each component that can handle it.
|
|
|
|
|
self.explorer.handle_key_events(key_event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|