Primitive text entry

This commit is contained in:
Joshua Barretto 2025-06-06 00:01:20 +01:00
parent 4d4d6a3470
commit 8c0a033f3c
9 changed files with 66 additions and 31 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "zte2" name = "zte2"
version = "0.2.0" version = "0.2.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }

View file

@ -49,7 +49,7 @@ fn main() -> Result<(), Error> {
// Have the UI handle events // Have the UI handle events
if ui if ui
.handle(Event::from_raw(ev)) .handle(&mut state, Event::from_raw(ev))
.map_or(false, |r| r.should_end()) .map_or(false, |r| r.should_end())
{ {
return Ok(()); return Ok(());

View file

@ -1,10 +1,9 @@
use crate::{ use crate::{
theme, Action, Args, Color, Error, Event, theme,
ui::{self, Element as _, Resp}, ui::{self, Element as _, Resp},
Action, Args, Color, Error, Event,
}; };
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use slotmap::{new_key_type, HopSlotMap}; use slotmap::{HopSlotMap, new_key_type};
use std::{io, path::PathBuf}; use std::{io, path::PathBuf};
new_key_type! { new_key_type! {
@ -12,8 +11,10 @@ new_key_type! {
pub struct CursorId; pub struct CursorId;
} }
#[derive(Default)] #[derive(Copy, Clone, Default)]
pub struct Cursor {} pub struct Cursor {
pub pos: usize,
}
pub struct Buffer { pub struct Buffer {
pub path: PathBuf, pub path: PathBuf,
@ -25,6 +26,7 @@ impl Buffer {
pub fn new(path: PathBuf) -> Result<Self, Error> { pub fn new(path: PathBuf) -> Result<Self, Error> {
let chars = match std::fs::read_to_string(&path) { let chars = match std::fs::read_to_string(&path) {
Ok(s) => s.chars().collect(), Ok(s) => s.chars().collect(),
// If the file doesn't exist, create a new file
Err(err) if err.kind() == io::ErrorKind::NotFound => Vec::new(), Err(err) if err.kind() == io::ErrorKind::NotFound => Vec::new(),
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
@ -34,6 +36,15 @@ impl Buffer {
cursors: HopSlotMap::default(), cursors: HopSlotMap::default(),
}) })
} }
pub fn insert(&mut self, pos: usize, c: char) {
self.chars.insert(pos, c);
self.cursors.values_mut().for_each(|c| {
if c.pos >= pos {
c.pos += 1
}
});
}
} }
pub struct State { pub struct State {

View file

@ -1,11 +1,11 @@
use crate::{theme, Error}; use crate::{Error, theme};
pub use crossterm::{ pub use crossterm::{
cursor::SetCursorStyle as CursorStyle, event::Event as TerminalEvent, style::Color, cursor::SetCursorStyle as CursorStyle, event::Event as TerminalEvent, style::Color,
}; };
use crossterm::{ use crossterm::{
cursor, event, style, terminal, ExecutableCommand, QueueableCommand, SynchronizedUpdate, ExecutableCommand, QueueableCommand, SynchronizedUpdate, cursor, event, style, terminal,
}; };
use std::{ use std::{
borrow::Borrow, borrow::Borrow,

View file

@ -15,7 +15,7 @@ impl Input {
} }
impl Element for Input { impl Element for Input {
fn handle(&mut self, event: Event) -> Result<Resp, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
match event.to_action(|e| { match event.to_action(|e| {
e.to_char() e.to_char()
.map(Action::Char) .map(Action::Char)

View file

@ -13,8 +13,8 @@ pub use self::{
}; };
use crate::{ use crate::{
terminal::{Color, Rect},
Action, Dir, Event, State, Action, Dir, Event, State,
terminal::{Color, Rect},
}; };
pub enum CannotEnd {} pub enum CannotEnd {}
@ -59,7 +59,7 @@ pub trait Element<CanEnd = CannotEnd> {
/// ///
/// If handled, convert into a series of secondary actions. /// If handled, convert into a series of secondary actions.
/// If unhandled, return the original event to be handled by a lower element. /// If unhandled, return the original event to be handled by a lower element.
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event>; fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event>;
} }
pub trait Visual { pub trait Visual {

View file

@ -1,5 +1,8 @@
use super::*; use super::*;
use crate::state::{BufferId, Cursor, CursorId}; use crate::{
state::{BufferId, Cursor, CursorId},
terminal::CursorStyle,
};
#[derive(Clone)] #[derive(Clone)]
pub struct Doc { pub struct Doc {
@ -17,13 +20,24 @@ impl Doc {
} }
impl Element for Doc { impl Element for Doc {
fn handle(&mut self, event: Event) -> Result<Resp, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event);
};
let Some(cursor) = buffer.cursors.get(self.cursor) else {
return Err(event);
};
match event.to_action(|e| { match event.to_action(|e| {
e.to_char() e.to_char()
.map(Action::Char) .map(Action::Char)
.or_else(|| e.to_move().map(Action::Move)) .or_else(|| e.to_move().map(Action::Move))
.or_else(|| e.to_pane_move().map(Action::PaneMove)) .or_else(|| e.to_pane_move().map(Action::PaneMove))
}) { }) {
Some(Action::Char(c)) => {
buffer.insert(cursor.pos, c);
Ok(Resp::handled(None))
}
_ => Err(event), _ => Err(event),
} }
} }
@ -31,12 +45,22 @@ impl Element for Doc {
impl Visual for Doc { impl Visual for Doc {
fn render(&self, state: &State, frame: &mut Rect) { fn render(&self, state: &State, frame: &mut Rect) {
if let Some(buffer) = state.buffers.get(self.buffer) { let Some(buffer) = state.buffers.get(self.buffer) else {
for (i, line) in buffer.chars.split(|c| *c == '\n').enumerate() { return;
frame.text([0, i], line); };
let Some(cursor) = buffer.cursors.get(self.cursor) else {
return;
};
let mut n = 0;
for (i, line) in buffer.chars.split(|c| *c == '\n').enumerate() {
frame.text([0, i], line);
if (n..=n + line.len()).contains(&cursor.pos) {
frame.set_cursor([cursor.pos - n, i], CursorStyle::BlinkingBar);
} }
} else {
frame.text([0, 0], "[Error: no buffer]".chars()); n += line.len() + 1;
} }
} }
} }
@ -64,7 +88,7 @@ impl Panes {
} }
impl Element for Panes { impl Element for Panes {
fn handle(&mut self, event: Event) -> Result<Resp, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
match event.to_action(|e| e.to_pane_move().map(Action::PaneMove)) { match event.to_action(|e| e.to_pane_move().map(Action::PaneMove)) {
Some(Action::PaneMove(Dir::Left)) => { Some(Action::PaneMove(Dir::Left)) => {
self.selected = (self.selected + self.panes.len() - 1) % self.panes.len(); self.selected = (self.selected + self.panes.len() - 1) % self.panes.len();
@ -79,7 +103,7 @@ impl Element for Panes {
if let Some(pane) = self.panes.get_mut(self.selected) { if let Some(pane) = self.panes.get_mut(self.selected) {
// Pass to pane // Pass to pane
match pane { match pane {
Pane::Doc(doc) => doc.handle(event), Pane::Doc(doc) => doc.handle(state, event),
} }
} else { } else {
// No active pane, don't handle // No active pane, don't handle

View file

@ -41,7 +41,7 @@ impl Prompt {
} }
impl Element<CanEnd> for Prompt { impl Element<CanEnd> for Prompt {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| { match event.to_action(|e| {
if e.is_cancel() { if e.is_cancel() {
Some(Action::Cancel) Some(Action::Cancel)
@ -64,7 +64,7 @@ impl Element<CanEnd> for Prompt {
)))) ))))
} }
} }
_ => self.input.handle(event).map(Resp::into_can_end), _ => self.input.handle(state, event).map(Resp::into_can_end),
} }
} }
} }
@ -80,7 +80,7 @@ pub struct Show {
} }
impl Element<CanEnd> for Show { impl Element<CanEnd> for Show {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| { match event.to_action(|e| {
if e.is_cancel() { if e.is_cancel() {
Some(Action::Cancel) Some(Action::Cancel)
@ -113,7 +113,7 @@ pub struct Confirm {
} }
impl Element<CanEnd> for Confirm { impl Element<CanEnd> for Confirm {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| { match event.to_action(|e| {
if e.is_cancel() || e.to_char() == Some('n') { if e.is_cancel() || e.to_char() == Some('n') {
Some(Action::Cancel) Some(Action::Cancel)

View file

@ -24,14 +24,14 @@ impl Root {
} }
impl Element<CanEnd> for Root { impl Element<CanEnd> for Root {
fn handle(&mut self, mut event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, state: &mut State, mut event: Event) -> Result<Resp<CanEnd>, Event> {
// Pass the event down through the list of tasks until we meet one that can handle it // Pass the event down through the list of tasks until we meet one that can handle it
let mut task_idx = self.tasks.len(); let mut task_idx = self.tasks.len();
let action = loop { let action = loop {
task_idx = match task_idx.checked_sub(1) { task_idx = match task_idx.checked_sub(1) {
Some(task_idx) => task_idx, Some(task_idx) => task_idx,
None => { None => {
break match self.panes.handle(event) { break match self.panes.handle(state, event) {
Ok(resp) => resp.action, Ok(resp) => resp.action,
Err(event) => event.to_action(|e| { Err(event) => event.to_action(|e| {
if e.is_prompt() { if e.is_prompt() {
@ -42,14 +42,14 @@ impl Element<CanEnd> for Root {
None None
} }
}), }),
} };
} }
}; };
let res = match &mut self.tasks[task_idx] { let res = match &mut self.tasks[task_idx] {
Task::Prompt(p) => p.handle(event), Task::Prompt(p) => p.handle(state, event),
Task::Show(s) => s.handle(event), Task::Show(s) => s.handle(state, event),
Task::Confirm(c) => c.handle(event), Task::Confirm(c) => c.handle(state, event),
}; };
match res { match res {