use hyperlog_core::log::GraphItem; use itertools::Itertools; use ratatui::{ prelude::*, widgets::{Block, Borders, Padding, Paragraph}, }; use crate::{ command_parser::CommandParser, commands::{batch::BatchCommand, update_item::UpdateItemCommandExt, Command, IntoCommand}, components::graph_explorer::GraphExplorer, editor, models::IOEvent, state::SharedState, Msg, }; use self::{ command_bar::{CommandBar, CommandBarState}, dialog::{ create_item::{CreateItem, CreateItemState}, edit_item::{EditItem, EditItemState}, }, }; mod command_bar; pub mod dialog; pub enum Dialog { CreateItem { state: CreateItemState }, EditItem { state: EditItemState }, } impl Dialog { pub fn get_command(&self) -> Option { match self { Dialog::CreateItem { state } => state.get_command().map(|c| c.into_command()), Dialog::EditItem { state } => state.get_command().map(|c| c.into_command()), } } } pub enum Mode { View, Insert, Command, } pub enum AppFocus { Dialog, Graph, } pub struct App<'a> { root: String, state: SharedState, pub mode: Mode, dialog: Option, command: Option, graph_explorer: GraphExplorer<'a>, focus: AppFocus, } impl<'a> App<'a> { pub fn new( root: impl Into, state: SharedState, graph_explorer: GraphExplorer<'a>, ) -> Self { Self { state, root: root.into(), dialog: None, command: None, graph_explorer, mode: Mode::View, focus: AppFocus::Graph, } } pub fn update(&mut self, msg: Msg) -> anyhow::Result { tracing::trace!("handling msg: {:?}", msg); let mut batch = BatchCommand::default(); match &msg { Msg::ItemCreated(IOEvent::Success(())) | Msg::ItemUpdated(IOEvent::Success(())) | Msg::SectionCreated(IOEvent::Success(())) | Msg::ItemToggled(IOEvent::Success(())) | Msg::Archive(IOEvent::Success(())) => { batch.with(self.graph_explorer.new_update_graph()); } Msg::MoveRight => self.graph_explorer.move_right()?, Msg::MoveLeft => self.graph_explorer.move_left()?, Msg::MoveDown => self.graph_explorer.move_down()?, Msg::MoveUp => self.graph_explorer.move_up()?, Msg::OpenCreateItemDialog => self.open_dialog(), Msg::OpenCreateItemDialogBelow => self.open_dialog_below(), Msg::OpenEditor { item } => { if let Some(cmd) = self.open_editor(item) { batch.with(cmd); } } Msg::OpenEditItemDialog { item } => self.open_edit_item_dialog(item), Msg::EnterInsertMode => self.mode = Mode::Insert, Msg::EnterViewMode => self.mode = Mode::View, Msg::EnterCommandMode => { self.command = Some(CommandBarState::default()); self.mode = Mode::Command } Msg::Interact => match self.focus { AppFocus::Dialog => {} AppFocus::Graph => { let cmd = self.graph_explorer.interact()?; batch.with(cmd); } }, Msg::SubmitCommand { command } => { tracing::info!("submitting command"); if let Some(command) = CommandParser::parse(command) { match self.focus { AppFocus::Dialog => { if command.is_write() { if let Some(dialog) = &self.dialog { if let Some(output) = dialog.get_command() { batch.with(output.into_command()); } } } if command.is_quit() { self.focus = AppFocus::Graph; self.dialog = None; } } AppFocus::Graph => { if let Some(cmd) = self.graph_explorer.execute_command(&command)? { self.command = None; batch.with(cmd); } if command.is_quit() { batch.with(Msg::QuitApp.into_command()); } } } } self.command = None; batch.with(Msg::EnterViewMode.into_command()); } _ => {} } let cmd = self.graph_explorer.inner.update(&msg); if let Some(cmd) = cmd { batch.with(cmd); } if let Some(command) = &mut self.command { let cmd = command.update(&msg)?; batch.with(cmd); } else if let Some(dialog) = &mut self.dialog { match dialog { Dialog::CreateItem { state } => state.update(&msg)?, Dialog::EditItem { state } => state.update(&msg)?, } } Ok(batch.into_command()) } fn open_dialog(&mut self) { if self.dialog.is_none() { let root = self.root.clone(); let path = self.graph_explorer.get_current_path(); self.focus = AppFocus::Dialog; self.dialog = Some(Dialog::CreateItem { state: CreateItemState::new(&self.state, root, path), }); } } fn open_dialog_below(&mut self) { if self.dialog.is_none() { let root = self.root.clone(); let path = self.graph_explorer.get_current_path(); if let Some((_, rest)) = path.split_last() { let path = rest.to_vec(); self.focus = AppFocus::Dialog; self.dialog = Some(Dialog::CreateItem { state: CreateItemState::new(&self.state, root, path), }); } } } fn open_edit_item_dialog(&mut self, item: &GraphItem) { if self.dialog.is_none() { let root = self.root.clone(); let path = self.graph_explorer.get_current_path(); self.dialog = Some(Dialog::EditItem { state: EditItemState::new(&self.state, root, path, item), }); self.command = None; self.focus = AppFocus::Dialog; self.mode = Mode::Insert; } } fn open_editor(&self, item: &GraphItem) -> Option { match editor::EditorSession::new(item).execute() { Ok(None) => { tracing::info!("editor returned without changes, skipping"); } Ok(Some(item)) => { if let GraphItem::Item { title, description, state, } = item { return Some( self.state.update_item_command().command( &self.root, &self .graph_explorer .get_current_path() .iter() .map(|s| s.as_str()) .collect_vec(), &title, &description, state, ), ); } } Err(e) => { tracing::error!("failed to run editor with: {}", e); } } None } } impl<'a> Widget for &mut App<'a> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { StatefulWidget::render( GraphExplorer::new(self.root.clone(), self.state.clone()), area, buf, &mut self.graph_explorer.inner, ) } } pub fn render_app(frame: &mut Frame, state: &mut App) { let chunks = Layout::vertical(vec![ Constraint::Length(2), Constraint::Min(0), Constraint::Length(1), ]) .split(frame.size()); let mut heading_parts = vec![Span::styled("hyperlog", Style::default()).fg(Color::Green)]; if let Some(dialog) = &state.dialog { heading_parts.push(Span::raw(" ~ ")); match dialog { Dialog::CreateItem { .. } => heading_parts.push(Span::raw("create item")), Dialog::EditItem { .. } => heading_parts.push(Span::raw("edit item")), } } let heading = Paragraph::new(text::Line::from(heading_parts)); let block_heading = Block::default().borders(Borders::BOTTOM); frame.render_widget(heading.block(block_heading), chunks[0]); match &state.mode { Mode::View => { let line = Line::raw("-- VIEW --"); let powerbar_block = Block::default() .borders(Borders::empty()) .padding(Padding::new(1, 1, 0, 0)); frame.render_widget(Paragraph::new(vec![line]).block(powerbar_block), chunks[2]); } Mode::Insert => { let line = Line::raw("-- EDIT --"); let powerbar_block = Block::default() .borders(Borders::empty()) .padding(Padding::new(1, 1, 0, 0)); frame.render_widget(Paragraph::new(vec![line]).block(powerbar_block), chunks[2]); } Mode::Command => { if let Some(command) = &mut state.command { frame.render_stateful_widget(CommandBar::default(), chunks[2], command); } } } let Rect { width, height, .. } = chunks[1]; let height = height as usize; let width = width as usize; let mut lines = Vec::new(); for y in 0..height { if !y % 2 == 0 { lines.push(text::Line::default()); } else { lines.push(text::Line::raw(" ~ ".repeat(width / 3))); } } let _background = Paragraph::new(lines); let _bg_block = Block::default() .fg(Color::DarkGray) .bold() .padding(Padding { left: 4, right: 4, top: 2, bottom: 2, }); if let Some(dialog) = state.dialog.as_mut() { match dialog { Dialog::CreateItem { state } => { frame.render_stateful_widget(&mut CreateItem::default(), chunks[1], state) } Dialog::EditItem { state } => { frame.render_stateful_widget(&mut EditItem::default(), chunks[1], state) } } return; } frame.render_widget(state, chunks[1]); }