feat: with input

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2024-05-07 23:21:13 +02:00
parent 74f91a6201
commit 26c430e173
8 changed files with 557 additions and 29 deletions

View File

@@ -18,6 +18,7 @@ ratatui = "0.26.2"
crossterm = { version = "0.27.0", features = ["event-stream"] }
directories = "5.0.1"
human-panic = "2.0.0"
ropey = "1.6.1"
[dev-dependencies]
similar-asserts = "1.5.0"

View File

@@ -3,17 +3,35 @@ use ratatui::{
widgets::{Block, Borders, Padding, Paragraph},
};
use crate::{components::GraphExplorer, state::SharedState, Msg};
use crate::{components::GraphExplorer, models::EditMsg, state::SharedState, Msg};
use self::dialog::{CreateItem, CreateItemState};
pub mod dialog;
pub enum Dialog {
CreateItem { state: CreateItemState },
}
pub enum Mode {
View,
Insert,
}
pub struct App<'a> {
state: SharedState,
pub mode: Mode,
dialog: Option<Dialog>,
graph_explorer: GraphExplorer<'a>,
}
impl<'a> App<'a> {
pub fn new(state: SharedState, graph_explorer: GraphExplorer<'a>) -> Self {
Self {
mode: Mode::View,
dialog: None,
state,
graph_explorer,
}
@@ -23,10 +41,30 @@ impl<'a> App<'a> {
tracing::trace!("handling msg: {:?}", msg);
match msg {
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::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::EnterInsertMode => self.mode = Mode::Insert,
Msg::EnterCommandMode => self.mode = Mode::View,
_ => {}
}
if let Some(dialog) = &mut self.dialog {
match dialog {
Dialog::CreateItem { state } => state.update(&msg)?,
}
}
Ok(())
}
fn open_dialog(&mut self) {
if self.dialog.is_none() {
self.dialog = Some(Dialog::CreateItem {
state: CreateItemState::default(),
});
}
}
}
@@ -46,16 +84,40 @@ impl<'a> Widget for &mut App<'a> {
}
pub fn render_app(frame: &mut Frame, state: &mut App) {
let chunks =
Layout::vertical(vec![Constraint::Length(2), Constraint::Min(0)]).split(frame.size());
let chunks = Layout::vertical(vec![
Constraint::Length(2),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
let heading = Paragraph::new(text::Line::from(
Span::styled("hyperlog", Style::default()).fg(Color::Green),
));
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")),
}
}
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]);
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],
);
let Rect { width, height, .. } = chunks[1];
let height = height as usize;
@@ -80,7 +142,16 @@ pub fn render_app(frame: &mut Frame, state: &mut App) {
top: 2,
bottom: 2,
});
//frame.render_widget(background.block(bg_block), chunks[1]);
frame.render_widget(state, chunks[1])
if let Some(dialog) = state.dialog.as_mut() {
match dialog {
Dialog::CreateItem { state } => {
frame.render_stateful_widget(&mut CreateItem::default(), chunks[1], state)
}
}
return;
}
frame.render_widget(state, chunks[1]);
}

View File

@@ -0,0 +1,242 @@
use std::{ops::Deref, rc::Rc};
use ratatui::{prelude::*, widgets::*};
use crate::models::{EditMsg, Msg};
pub enum BufferState {
Focused {
content: ropey::Rope,
position: usize,
},
Static {
content: String,
position: usize,
},
}
impl Default for BufferState {
fn default() -> Self {
Self::Static {
content: String::new(),
position: 0,
}
}
}
impl BufferState {
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 {
content.remove((pos - 1)..pos);
*position = position.saturating_sub(1);
}
}
EditMsg::DeleteNext => {
if pos > 0 && pos < content.len_chars() {
content.remove((pos)..pos + 1);
}
}
EditMsg::InsertNewLine => todo!(),
EditMsg::InsertTab => todo!(),
EditMsg::InsertChar(c) => {
content.try_insert_char(pos, *c)?;
*position = position.saturating_add(1);
}
EditMsg::MoveLeft => {
*position = position.saturating_sub(1);
}
EditMsg::MoveRight => {
if pos + 1 < content.len_chars() {
*position = pos.saturating_add(1);
}
}
}
}
Ok(())
}
}
pub struct InputBuffer {
state: BufferState,
}
impl InputBuffer {
fn to_focused(&mut self) {
match &mut self.state {
BufferState::Focused { .. } => {}
BufferState::Static { content, position } => {
self.state = BufferState::Focused {
content: ropey::Rope::from(content.as_str()),
position: *position,
}
}
}
}
fn to_static(&mut self) {
match &mut self.state {
BufferState::Focused { content, position } => {
self.state = BufferState::Static {
content: content.to_string(),
position: *position,
}
}
BufferState::Static { .. } => {}
}
}
pub fn toggle(&mut self) {
match &mut self.state {
BufferState::Focused { content, position } => {
self.state = BufferState::Static {
content: content.to_string(),
position: *position,
}
}
BufferState::Static { content, position } => {
self.state = BufferState::Focused {
content: ropey::Rope::from(content.as_str()),
position: *position,
}
}
}
}
pub fn update(&mut self, msg: &Msg) -> anyhow::Result<()> {
match msg {
Msg::EnterInsertMode => self.to_focused(),
Msg::EnterCommandMode => self.to_static(),
Msg::Edit(c) => {
self.state.update(c)?;
}
_ => {}
}
Ok(())
}
}
impl Widget for InputBuffer {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
}
}
impl Default for InputBuffer {
fn default() -> Self {
Self {
state: BufferState::default(),
}
}
}
pub struct InputField<'a> {
title: &'a str,
}
impl<'a> InputField<'a> {
pub fn new(title: &'a str) -> Self {
Self { title }
}
}
impl<'a> StatefulWidget for InputField<'a> {
type State = InputBuffer;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let block = Block::bordered().title(self.title);
match &state.state {
BufferState::Focused { content, .. } => {
Paragraph::new(content.to_string().as_str())
.block(block)
.render(area, buf);
}
BufferState::Static { content, .. } => {
Paragraph::new(content.as_str())
.block(block)
.render(area, buf);
}
}
}
}
enum CreateItemFocused {
Title,
Description,
}
impl Default for CreateItemFocused {
fn default() -> Self {
Self::Title
}
}
#[derive(Default)]
pub struct CreateItemState {
title: InputBuffer,
description: InputBuffer,
focused: CreateItemFocused,
}
impl CreateItemState {
pub fn update(&mut self, msg: &Msg) -> anyhow::Result<()> {
match &msg {
Msg::MoveDown | Msg::MoveUp => match self.focused {
CreateItemFocused::Title => self.focused = CreateItemFocused::Description,
CreateItemFocused::Description => self.focused = CreateItemFocused::Title,
},
_ => {}
}
match self.focused {
CreateItemFocused::Title => {
self.title.update(msg)?;
}
CreateItemFocused::Description => {
self.description.update(msg)?;
}
}
Ok(())
}
}
#[derive(Default)]
pub struct CreateItem {}
impl StatefulWidget for &mut CreateItem {
// fn render(self, area: Rect, buf: &mut Buffer)
// where
// Self: Sized,
// {
// //buf.reset();
// // let block = Block::bordered()
// // .title("create item")
// // .padding(Padding::proportional(1));
// }
type State = CreateItemState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let chunks =
Layout::vertical(vec![Constraint::Length(3), Constraint::Length(3)]).split(area);
InputField::new("title").render(chunks[0], buf, &mut state.title);
InputField::new("description").render(chunks[1], buf, &mut state.description);
// let title = Paragraph::new("something"); //.block(block);
// title.render(area, buf);
}
}

View File

@@ -237,7 +237,7 @@ impl<'a> StatefulWidget for GraphExplorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let Rect { height, .. } = area;
let height = height as usize;
let _height = height as usize;
if let Some(graph) = &state.graph {
let movement_graph: MovementGraph = graph.clone().into();

View File

@@ -5,7 +5,7 @@ use app::{render_app, App};
use components::GraphExplorer;
use crossterm::event::{self, Event, KeyCode};
use hyperlog_core::state::State;
use models::Msg;
use models::{EditMsg, Msg};
use ratatui::{backend::CrosstermBackend, Terminal};
use crate::{state::SharedState, terminal::TerminalInstance};
@@ -67,21 +67,42 @@ 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 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)?;
}
_ => {}
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('a') => {
app.update(Msg::OpenCreateItemDialog)?;
app.update(Msg::EnterInsertMode)?;
}
KeyCode::Char('i') => {
app.update(Msg::EnterInsertMode)?;
}
_ => {}
},
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))?,
KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?,
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)?,
_ => {}
},
}
}
}

View File

@@ -4,4 +4,20 @@ pub enum Msg {
MoveLeft,
MoveDown,
MoveUp,
OpenCreateItemDialog,
EnterInsertMode,
EnterCommandMode,
Edit(EditMsg),
}
#[derive(Debug)]
pub enum EditMsg {
Delete,
InsertNewLine,
InsertTab,
DeleteNext,
InsertChar(char),
MoveLeft,
MoveRight,
}