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]
name = "zte2"
version = "0.2.0"
edition = "2021"
edition = "2024"
[dependencies]
clap = { version = "4.4", features = ["derive"] }

View file

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

View file

@ -1,10 +1,9 @@
use crate::{
theme,
Action, Args, Color, Error, Event, theme,
ui::{self, Element as _, Resp},
Action, Args, Color, Error, Event,
};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use slotmap::{new_key_type, HopSlotMap};
use slotmap::{HopSlotMap, new_key_type};
use std::{io, path::PathBuf};
new_key_type! {
@ -12,8 +11,10 @@ new_key_type! {
pub struct CursorId;
}
#[derive(Default)]
pub struct Cursor {}
#[derive(Copy, Clone, Default)]
pub struct Cursor {
pub pos: usize,
}
pub struct Buffer {
pub path: PathBuf,
@ -25,6 +26,7 @@ impl Buffer {
pub fn new(path: PathBuf) -> Result<Self, Error> {
let chars = match std::fs::read_to_string(&path) {
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) => return Err(err.into()),
};
@ -34,6 +36,15 @@ impl Buffer {
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 {

View file

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

View file

@ -15,7 +15,7 @@ impl 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| {
e.to_char()
.map(Action::Char)

View file

@ -13,8 +13,8 @@ pub use self::{
};
use crate::{
terminal::{Color, Rect},
Action, Dir, Event, State,
terminal::{Color, Rect},
};
pub enum CannotEnd {}
@ -59,7 +59,7 @@ pub trait Element<CanEnd = CannotEnd> {
///
/// If handled, convert into a series of secondary actions.
/// 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 {

View file

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

View file

@ -41,7 +41,7 @@ impl 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| {
if e.is_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 {
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| {
if e.is_cancel() {
Some(Action::Cancel)
@ -113,7 +113,7 @@ pub struct 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| {
if e.is_cancel() || e.to_char() == Some('n') {
Some(Action::Cancel)

View file

@ -24,14 +24,14 @@ impl 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
let mut task_idx = self.tasks.len();
let action = loop {
task_idx = match task_idx.checked_sub(1) {
Some(task_idx) => task_idx,
None => {
break match self.panes.handle(event) {
break match self.panes.handle(state, event) {
Ok(resp) => resp.action,
Err(event) => event.to_action(|e| {
if e.is_prompt() {
@ -42,14 +42,14 @@ impl Element<CanEnd> for Root {
None
}
}),
}
};
}
};
let res = match &mut self.tasks[task_idx] {
Task::Prompt(p) => p.handle(event),
Task::Show(s) => s.handle(event),
Task::Confirm(c) => c.handle(event),
Task::Prompt(p) => p.handle(state, event),
Task::Show(s) => s.handle(state, event),
Task::Confirm(c) => c.handle(state, event),
};
match res {