@@ -3,10 +3,14 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Padding, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{components::GraphExplorer, models::EditMsg, state::SharedState, Msg};
|
||||
use crate::{commands::IntoCommand, components::GraphExplorer, state::SharedState, Msg};
|
||||
|
||||
use self::dialog::{CreateItem, CreateItemState};
|
||||
use self::{
|
||||
command_bar::{CommandBar, CommandBarState},
|
||||
dialog::{CreateItem, CreateItemState},
|
||||
};
|
||||
|
||||
mod command_bar;
|
||||
pub mod dialog;
|
||||
|
||||
pub enum Dialog {
|
||||
@@ -16,6 +20,7 @@ pub enum Dialog {
|
||||
pub enum Mode {
|
||||
View,
|
||||
Insert,
|
||||
Command,
|
||||
}
|
||||
|
||||
pub struct App<'a> {
|
||||
@@ -23,6 +28,7 @@ pub struct App<'a> {
|
||||
|
||||
pub mode: Mode,
|
||||
dialog: Option<Dialog>,
|
||||
command: Option<CommandBarState>,
|
||||
|
||||
graph_explorer: GraphExplorer<'a>,
|
||||
}
|
||||
@@ -32,12 +38,13 @@ impl<'a> App<'a> {
|
||||
Self {
|
||||
mode: Mode::View,
|
||||
dialog: None,
|
||||
command: None,
|
||||
state,
|
||||
graph_explorer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: Msg) -> anyhow::Result<()> {
|
||||
pub fn update(&mut self, msg: Msg) -> anyhow::Result<impl IntoCommand> {
|
||||
tracing::trace!("handling msg: {:?}", msg);
|
||||
|
||||
match msg {
|
||||
@@ -47,17 +54,31 @@ impl<'a> App<'a> {
|
||||
Msg::MoveUp => self.graph_explorer.move_up()?,
|
||||
Msg::OpenCreateItemDialog => self.open_dialog(),
|
||||
Msg::EnterInsertMode => self.mode = Mode::Insert,
|
||||
Msg::EnterCommandMode => self.mode = Mode::View,
|
||||
Msg::EnterViewMode => self.mode = Mode::View,
|
||||
Msg::EnterCommandMode => {
|
||||
self.command = Some(CommandBarState::default());
|
||||
self.mode = Mode::Command
|
||||
}
|
||||
Msg::SubmitCommand => {
|
||||
tracing::info!("submitting command");
|
||||
|
||||
self.command = None;
|
||||
|
||||
return Ok(Msg::EnterViewMode.into_command());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(dialog) = &mut self.dialog {
|
||||
if let Some(command) = &mut self.command {
|
||||
let cmd = command.update(&msg)?;
|
||||
return Ok(cmd.into_command());
|
||||
} else if let Some(dialog) = &mut self.dialog {
|
||||
match dialog {
|
||||
Dialog::CreateItem { state } => state.update(&msg)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(().into_command())
|
||||
}
|
||||
|
||||
fn open_dialog(&mut self) {
|
||||
@@ -106,17 +127,29 @@ pub fn render_app(frame: &mut Frame, state: &mut App) {
|
||||
|
||||
frame.render_widget(heading.block(block_heading), chunks[0]);
|
||||
|
||||
let powerbar = match &state.mode {
|
||||
Mode::View => Line::raw("-- VIEW --"),
|
||||
Mode::Insert => Line::raw("-- EDIT --"),
|
||||
};
|
||||
let powerbar_block = Block::default()
|
||||
.borders(Borders::empty())
|
||||
.padding(Padding::new(1, 1, 0, 0));
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![powerbar]).block(powerbar_block),
|
||||
chunks[2],
|
||||
);
|
||||
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];
|
||||
|
||||
|
53
crates/hyperlog-tui/src/app/command_bar.rs
Normal file
53
crates/hyperlog-tui/src/app/command_bar.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use ratatui::widgets::{Paragraph, StatefulWidget, Widget};
|
||||
|
||||
use crate::{
|
||||
commands::IntoCommand,
|
||||
models::{EditMsg, Msg},
|
||||
};
|
||||
|
||||
use super::dialog::BufferState;
|
||||
|
||||
pub struct CommandBarState {
|
||||
contents: BufferState,
|
||||
}
|
||||
|
||||
impl Default for CommandBarState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
contents: BufferState::Focused {
|
||||
content: ropey::Rope::default(),
|
||||
position: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandBar {}
|
||||
|
||||
impl CommandBarState {
|
||||
pub fn update(&mut self, msg: &Msg) -> anyhow::Result<impl IntoCommand> {
|
||||
if let Msg::Edit(e) = msg {
|
||||
self.contents.update(e)?;
|
||||
|
||||
if let EditMsg::InsertNewLine = e {
|
||||
return Ok(Msg::SubmitCommand.into_command());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(().into_command())
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for CommandBar {
|
||||
type State = CommandBarState;
|
||||
|
||||
fn render(
|
||||
self,
|
||||
area: ratatui::prelude::Rect,
|
||||
buf: &mut ratatui::prelude::Buffer,
|
||||
state: &mut Self::State,
|
||||
) {
|
||||
Paragraph::new(format!(":{}", state.contents.string())).render(area, buf);
|
||||
}
|
||||
}
|
@@ -1,5 +1,3 @@
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::models::{EditMsg, Msg};
|
||||
@@ -25,13 +23,13 @@ impl Default for BufferState {
|
||||
}
|
||||
|
||||
impl BufferState {
|
||||
fn update(&mut self, msg: &EditMsg) -> anyhow::Result<()> {
|
||||
pub fn update(&mut self, msg: &EditMsg) -> anyhow::Result<()> {
|
||||
if let BufferState::Focused { content, position } = self {
|
||||
let pos = *position;
|
||||
|
||||
match msg {
|
||||
EditMsg::Delete => {
|
||||
if pos > 0 && content.len_chars() > pos {
|
||||
if pos > 0 && pos <= content.len_chars() {
|
||||
content.remove((pos - 1)..pos);
|
||||
*position = position.saturating_sub(1);
|
||||
}
|
||||
@@ -41,8 +39,8 @@ impl BufferState {
|
||||
content.remove((pos)..pos + 1);
|
||||
}
|
||||
}
|
||||
EditMsg::InsertNewLine => todo!(),
|
||||
EditMsg::InsertTab => todo!(),
|
||||
EditMsg::InsertNewLine => {}
|
||||
EditMsg::InsertTab => {}
|
||||
EditMsg::InsertChar(c) => {
|
||||
content.try_insert_char(pos, *c)?;
|
||||
*position = position.saturating_add(1);
|
||||
@@ -51,7 +49,7 @@ impl BufferState {
|
||||
*position = position.saturating_sub(1);
|
||||
}
|
||||
EditMsg::MoveRight => {
|
||||
if pos + 1 < content.len_chars() {
|
||||
if pos < content.len_chars() {
|
||||
*position = pos.saturating_add(1);
|
||||
}
|
||||
}
|
||||
@@ -60,10 +58,17 @@ impl BufferState {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn string(&self) -> String {
|
||||
match self {
|
||||
BufferState::Focused { content, .. } => content.to_string(),
|
||||
BufferState::Static { content, .. } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputBuffer {
|
||||
state: BufferState,
|
||||
pub state: BufferState,
|
||||
}
|
||||
|
||||
impl InputBuffer {
|
||||
@@ -111,7 +116,8 @@ impl InputBuffer {
|
||||
pub fn update(&mut self, msg: &Msg) -> anyhow::Result<()> {
|
||||
match msg {
|
||||
Msg::EnterInsertMode => self.to_focused(),
|
||||
Msg::EnterCommandMode => self.to_static(),
|
||||
Msg::EnterCommandMode => self.to_focused(),
|
||||
Msg::EnterViewMode => self.to_static(),
|
||||
Msg::Edit(c) => {
|
||||
self.state.update(c)?;
|
||||
}
|
||||
@@ -120,13 +126,12 @@ impl InputBuffer {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for InputBuffer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
pub fn string(&self) -> String {
|
||||
match &self.state {
|
||||
BufferState::Focused { ref content, .. } => content.to_string(),
|
||||
BufferState::Static { content, .. } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +160,13 @@ impl<'a> StatefulWidget for InputField<'a> {
|
||||
let block = Block::bordered().title(self.title);
|
||||
|
||||
match &state.state {
|
||||
BufferState::Focused { content, .. } => {
|
||||
BufferState::Focused { content, position } => {
|
||||
Paragraph::new(content.to_string().as_str())
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
|
||||
buf.get_mut(area.x + 1 + *position as u16, area.y + 1)
|
||||
.set_style(Style::new().bg(Color::Magenta).fg(Color::Black));
|
||||
}
|
||||
BufferState::Static { content, .. } => {
|
||||
Paragraph::new(content.as_str())
|
||||
|
31
crates/hyperlog-tui/src/commands.rs
Normal file
31
crates/hyperlog-tui/src/commands.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::models::Msg;
|
||||
|
||||
pub trait IntoCommand {
|
||||
fn into_command(self) -> Command;
|
||||
}
|
||||
|
||||
impl IntoCommand for () {
|
||||
fn into_command(self) -> Command {
|
||||
Command::new(|| None)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCommand for Command {
|
||||
fn into_command(self) -> Command {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Command {
|
||||
func: Box<dyn FnOnce() -> Option<Msg>>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new<T: FnOnce() -> Option<Msg> + 'static>(f: T) -> Self {
|
||||
Self { func: Box::new(f) }
|
||||
}
|
||||
|
||||
pub fn execute(self) -> Option<Msg> {
|
||||
self.func.call_once(())
|
||||
}
|
||||
}
|
@@ -151,8 +151,10 @@ impl RenderGraph for MovementGraph {
|
||||
}) {
|
||||
Some((true, rest)) => {
|
||||
if rest.is_empty() {
|
||||
lines
|
||||
.push(Line::raw(format!("- {}", item.name)).style(Style::new().bold()));
|
||||
lines.push(
|
||||
Line::raw(format!("- {}", item.name))
|
||||
.style(Style::new().bold().white()),
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
Line::raw(format!("- {}", item.name))
|
||||
@@ -203,7 +205,10 @@ impl RenderGraph for MovementGraph {
|
||||
Some((true, rest)) => {
|
||||
let mut line = Vec::new();
|
||||
if rest.is_empty() {
|
||||
line.push(Span::raw(format!("- {}", item.name)).style(Style::new().bold()));
|
||||
line.push(
|
||||
Span::raw(format!("- {}", item.name))
|
||||
.style(Style::new().bold().white()),
|
||||
);
|
||||
} else {
|
||||
line.push(
|
||||
Span::raw(format!("- {}", item.name))
|
||||
|
@@ -1,7 +1,10 @@
|
||||
#![feature(fn_traits)]
|
||||
|
||||
use std::{io::Stdout, time::Duration};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use app::{render_app, App};
|
||||
use commands::IntoCommand;
|
||||
use components::GraphExplorer;
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use hyperlog_core::state::State;
|
||||
@@ -13,6 +16,7 @@ use crate::{state::SharedState, terminal::TerminalInstance};
|
||||
pub mod models;
|
||||
|
||||
pub(crate) mod app;
|
||||
pub(crate) mod commands;
|
||||
pub(crate) mod components;
|
||||
pub(crate) mod state;
|
||||
|
||||
@@ -67,32 +71,24 @@ fn update(
|
||||
) -> Result<UpdateConclusion> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
match &app.mode {
|
||||
let mut cmd = match &app.mode {
|
||||
app::Mode::View => match key.code {
|
||||
KeyCode::Char('q') => return Ok(UpdateConclusion::new(true)),
|
||||
KeyCode::Char('l') => {
|
||||
app.update(Msg::MoveRight)?;
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.update(Msg::MoveLeft)?;
|
||||
}
|
||||
KeyCode::Char('j') => {
|
||||
app.update(Msg::MoveDown)?;
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.update(Msg::MoveUp)?;
|
||||
}
|
||||
KeyCode::Char('l') => app.update(Msg::MoveRight)?,
|
||||
KeyCode::Char('h') => app.update(Msg::MoveLeft)?,
|
||||
KeyCode::Char('j') => app.update(Msg::MoveDown)?,
|
||||
KeyCode::Char('k') => app.update(Msg::MoveUp)?,
|
||||
KeyCode::Char('a') => {
|
||||
// TODO: batch commands
|
||||
app.update(Msg::OpenCreateItemDialog)?;
|
||||
app.update(Msg::EnterInsertMode)?;
|
||||
app.update(Msg::EnterInsertMode)?
|
||||
}
|
||||
KeyCode::Char('i') => {
|
||||
app.update(Msg::EnterInsertMode)?;
|
||||
}
|
||||
_ => {}
|
||||
KeyCode::Char('i') => app.update(Msg::EnterInsertMode)?,
|
||||
KeyCode::Char(':') => app.update(Msg::EnterCommandMode)?,
|
||||
_ => return Ok(UpdateConclusion(false)),
|
||||
},
|
||||
|
||||
app::Mode::Insert => match key.code {
|
||||
app::Mode::Command | app::Mode::Insert => match key.code {
|
||||
KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?,
|
||||
KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?,
|
||||
KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?,
|
||||
@@ -100,9 +96,19 @@ fn update(
|
||||
KeyCode::Char(c) => app.update(Msg::Edit(EditMsg::InsertChar(c)))?,
|
||||
KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?,
|
||||
KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?,
|
||||
KeyCode::Esc => app.update(Msg::EnterCommandMode)?,
|
||||
_ => {}
|
||||
KeyCode::Esc => app.update(Msg::EnterViewMode)?,
|
||||
_ => return Ok(UpdateConclusion(false)),
|
||||
},
|
||||
};
|
||||
|
||||
loop {
|
||||
let msg = cmd.into_command().execute();
|
||||
match msg {
|
||||
Some(msg) => {
|
||||
cmd = app.update(msg)?;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
use crate::commands::{Command, IntoCommand};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Msg {
|
||||
MoveRight,
|
||||
@@ -7,10 +9,20 @@ pub enum Msg {
|
||||
OpenCreateItemDialog,
|
||||
|
||||
EnterInsertMode,
|
||||
EnterViewMode,
|
||||
EnterCommandMode,
|
||||
|
||||
SubmitCommand,
|
||||
|
||||
Edit(EditMsg),
|
||||
}
|
||||
|
||||
impl IntoCommand for Msg {
|
||||
fn into_command(self) -> crate::commands::Command {
|
||||
Command::new(|| Some(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditMsg {
|
||||
Delete,
|
||||
|
Reference in New Issue
Block a user