Compare commits
3 Commits
0926bbee46
...
91ac418bc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ac418bc0 | ||
|
|
1044003179 | ||
|
|
e3e2d64b2a |
51
examples/focus_advanced.rs
Normal file
51
examples/focus_advanced.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use tui_orchestrator::focus::{FocusId, FocusManager, Focusable};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
enum FormElement {
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
RememberMe,
|
||||||
|
Submit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusId for FormElement {}
|
||||||
|
|
||||||
|
struct FormPage {
|
||||||
|
username: alloc::string::String,
|
||||||
|
password: alloc::string::String,
|
||||||
|
remember: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable<FormElement> for FormPage {
|
||||||
|
fn focus_targets(&self) -> alloc::vec::Vec<FormElement> {
|
||||||
|
alloc::vec![
|
||||||
|
FormElement::Username,
|
||||||
|
FormElement::Password,
|
||||||
|
FormElement::RememberMe,
|
||||||
|
FormElement::Submit,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let form = FormPage {
|
||||||
|
username: alloc::string::String::new(),
|
||||||
|
password: alloc::string::String::new(),
|
||||||
|
remember: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut focus_manager = FocusManager::new();
|
||||||
|
focus_manager.set_targets(form.focus_targets());
|
||||||
|
println!("Current focus: {:?}", focus_manager.current());
|
||||||
|
|
||||||
|
focus_manager.next();
|
||||||
|
println!("After next: {:?}", focus_manager.current());
|
||||||
|
|
||||||
|
focus_manager.last();
|
||||||
|
println!("After last: {:?}", focus_manager.current());
|
||||||
|
|
||||||
|
println!("Is first: {}", focus_manager.is_first());
|
||||||
|
println!("Is last: {}", focus_manager.is_last());
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use tui_orchestrator::focus::{FocusManager, Focusable};
|
use tui_orchestrator::focus::{FocusId, FocusManager, Focusable};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
enum FormElement {
|
enum FormElement {
|
||||||
@@ -11,6 +11,8 @@ enum FormElement {
|
|||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FocusId for FormElement {}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct LoginForm {
|
struct LoginForm {
|
||||||
username: String,
|
username: String,
|
||||||
|
|||||||
55
examples/simple_input.rs
Normal file
55
examples/simple_input.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use tui_orchestrator::input::{default_bindings, ComponentAction, InputError, InputSource, Key};
|
||||||
|
|
||||||
|
struct MockInput {
|
||||||
|
keys: alloc::vec::Vec<Key>,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockInput {
|
||||||
|
fn new(keys: alloc::vec::Vec<Key>) -> Self {
|
||||||
|
Self { keys, index: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputSource for MockInput {
|
||||||
|
fn read_key(&mut self) -> Result<Key, InputError> {
|
||||||
|
if self.index < self.keys.len() {
|
||||||
|
let key = self.keys[self.index];
|
||||||
|
self.index += 1;
|
||||||
|
Ok(key)
|
||||||
|
} else {
|
||||||
|
Err(InputError::BackendError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let bindings = default_bindings();
|
||||||
|
let mut input = MockInput::new(alloc::vec![Key::tab(), Key::enter(), Key::esc(),]);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match input.read_key() {
|
||||||
|
Ok(key) => {
|
||||||
|
if let Some(action) = bindings.get(&key) {
|
||||||
|
match action {
|
||||||
|
ComponentAction::Next => println!("Next"),
|
||||||
|
ComponentAction::Select => println!("Select"),
|
||||||
|
ComponentAction::Cancel => {
|
||||||
|
println!("Cancel - exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => println!("Other action: {:?}", action),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("No binding for key: {:?}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// path_from_the_root: src/component/mod.rs
|
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod r#trait;
|
pub mod r#trait;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// path_from_the_root: src/component/trait.rs
|
|
||||||
|
|
||||||
use super::error::ComponentError;
|
use super::error::ComponentError;
|
||||||
use crate::focus::FocusId;
|
use crate::focus::FocusId;
|
||||||
|
use crate::input::Action;
|
||||||
|
|
||||||
pub trait Component {
|
pub trait Component {
|
||||||
type Focus: FocusId;
|
type Focus: FocusId;
|
||||||
type Action: core::fmt::Debug + Clone;
|
type Action: Action;
|
||||||
type Event: Clone + core::fmt::Debug;
|
type Event: Clone + core::fmt::Debug;
|
||||||
|
|
||||||
fn targets(&self) -> &[Self::Focus];
|
fn targets(&self) -> &[Self::Focus];
|
||||||
|
|||||||
26
src/focus/builder.rs
Normal file
26
src/focus/builder.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FocusBuilder<F: super::FocusId> {
|
||||||
|
targets: alloc::vec::Vec<F>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: super::FocusId> FocusBuilder<F> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
targets: alloc::vec::Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target(mut self, target: F) -> Self {
|
||||||
|
self.targets.push(target);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn targets(mut self, targets: &[F]) -> Self {
|
||||||
|
self.targets.extend_from_slice(targets);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> alloc::vec::Vec<F> {
|
||||||
|
self.targets
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// path_from_the_root: src/focus/id.rs
|
// path_from_the_root: src/focus/id.rs
|
||||||
|
|
||||||
pub trait FocusId: Clone + PartialEq + Eq + core::hash::Hash {}
|
pub trait FocusId: Clone + PartialEq + Eq + core::hash::Hash {}
|
||||||
|
|
||||||
impl<T: Clone + PartialEq + Eq + core::hash::Hash> FocusId for T {}
|
|
||||||
|
|||||||
@@ -133,4 +133,39 @@ impl<F: FocusId> FocusManager<F> {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.targets.is_empty()
|
self.targets.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_index(&self) -> Option<usize> {
|
||||||
|
if self.targets.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wrap_next(&mut self) {
|
||||||
|
if !self.targets.is_empty() {
|
||||||
|
self.index = (self.index + 1) % self.targets.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wrap_prev(&mut self) {
|
||||||
|
if !self.targets.is_empty() {
|
||||||
|
self.index = if self.index == 0 {
|
||||||
|
self.targets.len() - 1
|
||||||
|
} else {
|
||||||
|
self.index - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_first(&self) -> bool {
|
||||||
|
self.current_index() == Some(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_last(&self) -> bool {
|
||||||
|
match self.current_index() {
|
||||||
|
Some(idx) => idx == self.targets.len().saturating_sub(1),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/focus/mode.rs
Normal file
9
src/focus/mode.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
pub trait FocusModeHint<F: super::FocusId> {
|
||||||
|
fn focus_modes(&self) -> &[&'static str];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: super::FocusId> FocusModeHint<F> for super::FocusManager<F> {
|
||||||
|
fn focus_modes(&self) -> &[&'static str] {
|
||||||
|
&["general"]
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/focus/navigation.rs
Normal file
11
src/focus/navigation.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
pub trait FocusNavigation<F: super::FocusId> {
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
fn can_navigate_forward(&self, from: &F) -> bool;
|
||||||
|
|
||||||
|
fn can_navigate_backward(&self, from: &F) -> bool;
|
||||||
|
|
||||||
|
fn navigate_forward(&mut self) -> Result<Option<F>, Self::Error>;
|
||||||
|
|
||||||
|
fn navigate_backward(&mut self) -> Result<Option<F>, Self::Error>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,35 @@
|
|||||||
// path_from_the_root: src/input/action.rs
|
/// Marker trait for actions that can be bound to keys.
|
||||||
|
///
|
||||||
|
/// Actions must be cloneable, comparable, and debuggable.
|
||||||
pub trait Action: Clone + PartialEq + Eq + core::fmt::Debug {}
|
pub trait Action: Clone + PartialEq + Eq + core::fmt::Debug {}
|
||||||
|
|
||||||
|
/// Default component actions for common TUI patterns.
|
||||||
|
///
|
||||||
|
/// These actions cover the most common TUI interactions:
|
||||||
|
/// - Navigation (next, prev, first, last, directional)
|
||||||
|
/// - Interaction (select, cancel)
|
||||||
|
/// - Text input (type character, backspace, delete)
|
||||||
|
/// - Custom actions via `Custom(usize)`
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ComponentAction {
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
|
Select,
|
||||||
|
Cancel,
|
||||||
|
TypeChar(char),
|
||||||
|
Backspace,
|
||||||
|
Delete,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Custom(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action for ComponentAction {}
|
||||||
|
|||||||
@@ -1,43 +1,98 @@
|
|||||||
// path_from_the_root: src/input/bindings.rs
|
use super::action::{Action, ComponentAction};
|
||||||
|
|
||||||
use super::action::Action;
|
|
||||||
use super::key::Key;
|
use super::key::Key;
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
|
|
||||||
|
/// Maps keys to actions.
|
||||||
|
///
|
||||||
|
/// When `alloc` feature is enabled, uses HashMap for O(1) lookup.
|
||||||
|
/// Without `alloc`, falls back to Vec with O(n) lookup.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Bindings<A: Action> {
|
pub struct Bindings<A: Action> {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
bindings: hashbrown::HashMap<Key, A>,
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
bindings: alloc::vec::Vec<(Key, A)>,
|
bindings: alloc::vec::Vec<(Key, A)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Action> Bindings<A> {
|
impl<A: Action> Bindings<A> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
bindings: hashbrown::HashMap::new(),
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
bindings: alloc::vec::Vec::new(),
|
bindings: alloc::vec::Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind(&mut self, key: Key, action: A) {
|
pub fn bind(&mut self, key: Key, action: A) {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
|
self.bindings.insert(key, action);
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
self.bindings.push((key, action));
|
self.bindings.push((key, action));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, key: &Key) -> Option<&A> {
|
pub fn get(&self, key: &Key) -> Option<&A> {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
|
self.bindings.get(key)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a)
|
self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, key: &Key) {
|
pub fn remove(&mut self, key: &Key) {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
|
self.bindings.remove(key);
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
self.bindings.retain(|(k, _)| k != key);
|
self.bindings.retain(|(k, _)| k != key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
self.bindings.is_empty()
|
self.bindings.is_empty()
|
||||||
}
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
|
self.bindings.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
self.bindings.len()
|
self.bindings.len()
|
||||||
}
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
|
self.bindings.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keys(&self) -> alloc::vec::Vec<&Key> {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
|
self.bindings.keys().collect()
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
|
self.bindings.iter().map(|(k, _)| k).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &(Key, A)> {
|
pub fn iter(&self) -> impl Iterator<Item = &(Key, A)> {
|
||||||
self.bindings.iter()
|
self.bindings.iter()
|
||||||
}
|
}
|
||||||
@@ -52,19 +107,71 @@ impl<A: Action> Default for Bindings<A> {
|
|||||||
#[cfg(feature = "sequences")]
|
#[cfg(feature = "sequences")]
|
||||||
impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
|
impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
|
||||||
pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) {
|
pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) {
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
|
for key in keys {
|
||||||
|
self.bindings.insert(key, action.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
for key in keys {
|
for key in keys {
|
||||||
self.bindings.push((key, action.clone()));
|
self.bindings.push((key, action.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_sequences(&self) -> alloc::vec::Vec<&A> {
|
pub fn get_sequences(&self) -> alloc::vec::Vec<&A> {
|
||||||
let mut actions = alloc::vec::Vec::new();
|
let mut actions = alloc::vec::Vec::new();
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
{
|
||||||
for (_, action) in &self.bindings {
|
for (_, action) in &self.bindings {
|
||||||
if seen.insert(action) {
|
if seen.insert(action) {
|
||||||
actions.push(action);
|
actions.push(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
{
|
||||||
|
for (_, action) in &self.bindings {
|
||||||
|
if seen.insert(action) {
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns default key bindings for common TUI patterns.
|
||||||
|
///
|
||||||
|
/// Includes:
|
||||||
|
/// - Tab/Shift+Tab for next/prev navigation
|
||||||
|
/// - Enter for select
|
||||||
|
/// - Esc for cancel
|
||||||
|
/// - Arrow keys for directional movement
|
||||||
|
/// - Home/End/PageUp/PageDown for scrolling
|
||||||
|
/// - Backspace/Delete for editing
|
||||||
|
/// - Ctrl+C as custom quit action
|
||||||
|
///
|
||||||
|
/// Use as-is or extend with your own bindings.
|
||||||
|
pub fn default_bindings() -> Bindings<ComponentAction> {
|
||||||
|
let mut bindings = Bindings::new();
|
||||||
|
bindings.bind(Key::tab(), ComponentAction::Next);
|
||||||
|
bindings.bind(Key::shift_tab(), ComponentAction::Prev);
|
||||||
|
bindings.bind(Key::enter(), ComponentAction::Select);
|
||||||
|
bindings.bind(Key::esc(), ComponentAction::Cancel);
|
||||||
|
bindings.bind(Key::home(), ComponentAction::Home);
|
||||||
|
bindings.bind(Key::end(), ComponentAction::End);
|
||||||
|
bindings.bind(Key::page_up(), ComponentAction::PageUp);
|
||||||
|
bindings.bind(Key::page_down(), ComponentAction::PageDown);
|
||||||
|
bindings.bind(Key::up(), ComponentAction::Up);
|
||||||
|
bindings.bind(Key::down(), ComponentAction::Down);
|
||||||
|
bindings.bind(Key::left(), ComponentAction::Left);
|
||||||
|
bindings.bind(Key::right(), ComponentAction::Right);
|
||||||
|
bindings.bind(Key::backspace(), ComponentAction::Backspace);
|
||||||
|
bindings.bind(Key::delete(), ComponentAction::Delete);
|
||||||
|
bindings.bind(Key::ctrl('c'), ComponentAction::Custom(0));
|
||||||
|
bindings
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// path_from_the_root: src/input/handler.rs
|
|
||||||
|
|
||||||
#[cfg(feature = "sequences")]
|
#[cfg(feature = "sequences")]
|
||||||
use super::action::Action;
|
use super::action::Action;
|
||||||
|
|
||||||
@@ -9,10 +7,20 @@ use super::key::Key;
|
|||||||
#[cfg(feature = "sequences")]
|
#[cfg(feature = "sequences")]
|
||||||
use super::result::MatchResult;
|
use super::result::MatchResult;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "sequences", feature = "std"))]
|
||||||
|
extern crate std;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "sequences", feature = "std"))]
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[cfg(feature = "sequences")]
|
#[cfg(feature = "sequences")]
|
||||||
pub struct SequenceHandler<A: Action> {
|
pub struct SequenceHandler<A: Action> {
|
||||||
sequences: alloc::vec::Vec<(alloc::vec::Vec<Key>, A)>,
|
sequences: alloc::vec::Vec<(alloc::vec::Vec<Key>, A)>,
|
||||||
current: alloc::vec::Vec<Key>,
|
current: alloc::vec::Vec<Key>,
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
last_timestamp: Option<Instant>,
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sequences")]
|
#[cfg(feature = "sequences")]
|
||||||
@@ -21,6 +29,17 @@ impl<A: Action> SequenceHandler<A> {
|
|||||||
Self {
|
Self {
|
||||||
sequences: alloc::vec::Vec::new(),
|
sequences: alloc::vec::Vec::new(),
|
||||||
current: alloc::vec::Vec::new(),
|
current: alloc::vec::Vec::new(),
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
last_timestamp: None,
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
timeout: Duration::from_millis(500),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_timeout_ms(&mut self, ms: u64) {
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
{
|
||||||
|
self.timeout = Duration::from_millis(ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +49,16 @@ impl<A: Action> SequenceHandler<A> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(&mut self, key: Key) -> MatchResult<A> {
|
pub fn handle(&mut self, key: Key) -> MatchResult<A> {
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
{
|
||||||
|
if let Some(last) = self.last_timestamp {
|
||||||
|
if last.elapsed() > self.timeout {
|
||||||
|
self.current.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_timestamp = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
self.current.push(key);
|
self.current.push(key);
|
||||||
|
|
||||||
for (seq, action) in &self.sequences {
|
for (seq, action) in &self.sequences {
|
||||||
|
|||||||
131
src/input/key.rs
131
src/input/key.rs
@@ -1,5 +1,7 @@
|
|||||||
// path_from_the_root: src/input/key.rs
|
/// Represents a key code without modifiers.
|
||||||
|
///
|
||||||
|
/// This includes character keys, special keys (Enter, Tab, Esc, etc.),
|
||||||
|
/// and function keys F1-F255.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum KeyCode {
|
pub enum KeyCode {
|
||||||
Char(char),
|
Char(char),
|
||||||
@@ -20,6 +22,9 @@ pub enum KeyCode {
|
|||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents key modifier flags (Control, Alt, Shift).
|
||||||
|
///
|
||||||
|
/// These modifiers can be combined with any key code.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct KeyModifiers {
|
pub struct KeyModifiers {
|
||||||
pub control: bool,
|
pub control: bool,
|
||||||
@@ -56,6 +61,9 @@ impl KeyModifiers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a complete key press with modifiers.
|
||||||
|
///
|
||||||
|
/// This is the main type used throughout the input system.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
pub code: KeyCode,
|
pub code: KeyCode,
|
||||||
@@ -81,6 +89,125 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn shift_tab() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Tab,
|
||||||
|
modifiers: KeyModifiers::new().with_shift(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn alt_char(c: char) -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
modifiers: KeyModifiers::new().with_alt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn ctrl_shift_char(c: char) -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
modifiers: KeyModifiers::new().with_control().with_shift(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn alt_ctrl_char(c: char) -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
modifiers: KeyModifiers::new().with_alt().with_control(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn enter() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Enter,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn esc() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Esc,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn backspace() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Backspace,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn delete() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Delete,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn tab() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Tab,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn page_up() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::PageUp,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn page_down() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::PageDown,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn up() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Up,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn down() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Down,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn left() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn right() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Right,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn home() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::Home,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn end() -> Self {
|
||||||
|
Self {
|
||||||
|
code: KeyCode::End,
|
||||||
|
modifiers: KeyModifiers::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn display_string(&self) -> alloc::string::String {
|
pub fn display_string(&self) -> alloc::string::String {
|
||||||
let mut out = alloc::string::String::new();
|
let mut out = alloc::string::String::new();
|
||||||
if self.modifiers.control {
|
if self.modifiers.control {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ pub mod bindings;
|
|||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod key;
|
pub mod key;
|
||||||
pub mod result;
|
pub mod result;
|
||||||
|
pub mod source;
|
||||||
|
|
||||||
pub use action::Action;
|
pub use action::{Action, ComponentAction};
|
||||||
pub use bindings::Bindings;
|
pub use bindings::{default_bindings, Bindings};
|
||||||
pub use key::{Key, KeyCode, KeyModifiers};
|
pub use key::{Key, KeyCode, KeyModifiers};
|
||||||
pub use result::MatchResult;
|
pub use result::MatchResult;
|
||||||
|
pub use source::{InputError, InputSource};
|
||||||
|
|
||||||
#[cfg(feature = "sequences")]
|
#[cfg(feature = "sequences")]
|
||||||
pub use handler::SequenceHandler;
|
pub use handler::SequenceHandler;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// path_from_the_root: src/input/result.rs
|
/// Result of matching a key or sequence.
|
||||||
|
///
|
||||||
|
/// - `Match(action)` - Key or sequence matched, returns the action
|
||||||
|
/// - `Pending` - Sequence in progress, waiting for more keys
|
||||||
|
/// - `NoMatch` - No binding found
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum MatchResult<A> {
|
pub enum MatchResult<A> {
|
||||||
Match(A),
|
Match(A),
|
||||||
|
|||||||
38
src/input/source.rs
Normal file
38
src/input/source.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use super::key::Key;
|
||||||
|
|
||||||
|
/// Errors that can occur when reading input.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InputError {
|
||||||
|
NotAKeyEvent,
|
||||||
|
BackendError,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for reading keys from a backend.
|
||||||
|
///
|
||||||
|
/// Users implement this trait to bridge to crossterm, termion,
|
||||||
|
/// or any other terminal backend.
|
||||||
|
///
|
||||||
|
/// Example with crossterm:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use crossterm::event;
|
||||||
|
///
|
||||||
|
/// struct CrosstermInput;
|
||||||
|
///
|
||||||
|
/// impl InputSource for CrosstermInput {
|
||||||
|
/// fn read_key(&mut self) -> Result<Key, InputError> {
|
||||||
|
/// match event::read()? {
|
||||||
|
/// event::Event::Key(key_event) => {
|
||||||
|
/// Ok(Key::new(
|
||||||
|
/// KeyCode::from(key_event.code),
|
||||||
|
/// KeyModifiers::from(key_event.modifiers),
|
||||||
|
/// ))
|
||||||
|
/// }
|
||||||
|
/// _ => Err(InputError::NotAKeyEvent),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait InputSource {
|
||||||
|
fn read_key(&mut self) -> Result<Key, InputError>;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use tui_orchestrator::component::{Component, ComponentAction};
|
use tui_orchestrator::component::{Component, ComponentAction};
|
||||||
|
use tui_orchestrator::focus::FocusId;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
enum TestFocus {
|
enum TestFocus {
|
||||||
@@ -9,6 +10,8 @@ enum TestFocus {
|
|||||||
ButtonC,
|
ButtonC,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FocusId for TestFocus {}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum TestEvent {
|
enum TestEvent {
|
||||||
ButtonCPressed,
|
ButtonCPressed,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use tui_orchestrator::focus::{FocusError, FocusManager, Focusable};
|
use tui_orchestrator::focus::{FocusError, FocusId, FocusManager, Focusable};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -11,6 +11,8 @@ enum TestId {
|
|||||||
Dialog,
|
Dialog,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FocusId for TestId {}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_focus_id_trait() {
|
fn test_focus_id_trait() {
|
||||||
let id1 = TestId::Button("save");
|
let id1 = TestId::Button("save");
|
||||||
@@ -253,31 +255,3 @@ fn test_focusable_trait() {
|
|||||||
let targets = component.focus_targets();
|
let targets = component.focus_targets();
|
||||||
assert_eq!(targets.len(), 3);
|
assert_eq!(targets.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_usize_focus_id() {
|
|
||||||
let mut manager: FocusManager<usize> = FocusManager::new();
|
|
||||||
|
|
||||||
manager.set_targets(vec![0, 1, 2, 3]);
|
|
||||||
assert_eq!(manager.current(), Some(&0));
|
|
||||||
|
|
||||||
manager.next();
|
|
||||||
assert_eq!(manager.current(), Some(&1));
|
|
||||||
|
|
||||||
manager.set_focus(3).unwrap();
|
|
||||||
assert_eq!(manager.current(), Some(&3));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_string_focus_id() {
|
|
||||||
let mut manager: FocusManager<&str> = FocusManager::new();
|
|
||||||
|
|
||||||
manager.set_targets(vec!["input1", "input2", "button_save"]);
|
|
||||||
assert_eq!(manager.current(), Some(&"input1"));
|
|
||||||
|
|
||||||
manager.next();
|
|
||||||
assert_eq!(manager.current(), Some(&"input2"));
|
|
||||||
|
|
||||||
manager.set_focus("button_save").unwrap();
|
|
||||||
assert_eq!(manager.current(), Some(&"button_save"));
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user