Primitive text entry
This commit is contained in:
parent
4d4d6a3470
commit
8c0a033f3c
9 changed files with 66 additions and 31 deletions
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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(());
|
||||||
|
|
|
||||||
21
src/state.rs
21
src/state.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue