Compare commits
9 Commits
d872b3d786
...
v0.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb22d270c | ||
|
|
d7f35690e3 | ||
|
|
33002f89a6 | ||
|
|
ad9bb78fc8 | ||
|
|
91ac418bc0 | ||
|
|
1044003179 | ||
|
|
e3e2d64b2a | ||
|
|
0926bbee46 | ||
|
|
d41182b13b |
50
Cargo.lock
generated
50
Cargo.lock
generated
@@ -3,22 +3,19 @@
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
name = "hash32"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -26,14 +23,35 @@ version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"rustc-std-workspace-alloc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui_orchestrator"
|
||||
name = "heapless"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pages-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"heapless",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-std-workspace-alloc"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d441c3b2ebf55cebf796bfdc265d67fa09db17b7bb6bd4be75c509e1e8fec3"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,16 +1,17 @@
|
||||
[package]
|
||||
name = "tui_orchestrator"
|
||||
name = "pages-tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
||||
alloc = ["hashbrown"]
|
||||
default = []
|
||||
std = ["alloc"]
|
||||
alloc = ["dep:hashbrown"]
|
||||
sequences = ["alloc"]
|
||||
|
||||
[dependencies]
|
||||
hashbrown = { version = "0.15", optional = true }
|
||||
hashbrown = { version = "0.15", optional = true, default-features = false, features = ["alloc"] }
|
||||
heapless = { version = "0.9.2", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
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;
|
||||
|
||||
use tui_orchestrator::focus::{FocusManager, Focusable};
|
||||
use tui_orchestrator::focus::{FocusId, FocusManager, Focusable};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum FormElement {
|
||||
@@ -11,6 +11,8 @@ enum FormElement {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl FocusId for FormElement {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct LoginForm {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
examples/simple_usage.rs
Normal file
75
examples/simple_usage.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
extern crate alloc;
|
||||
|
||||
use tui_orchestrator::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum Focus {
|
||||
Username,
|
||||
Password,
|
||||
LoginButton,
|
||||
}
|
||||
|
||||
impl FocusId for Focus {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum AppEvent {
|
||||
LoginAttempt {
|
||||
username: alloc::string::String,
|
||||
password: alloc::string::String,
|
||||
},
|
||||
Quit,
|
||||
}
|
||||
|
||||
struct LoginPage {
|
||||
username: alloc::string::String,
|
||||
password: alloc::string::String,
|
||||
}
|
||||
|
||||
impl Component for LoginPage {
|
||||
type Focus = Focus;
|
||||
type Action = ComponentAction;
|
||||
type Event = AppEvent;
|
||||
|
||||
fn targets(&self) -> &[Self::Focus] {
|
||||
&[Focus::Username, Focus::Password, Focus::LoginButton]
|
||||
}
|
||||
|
||||
fn handle(
|
||||
&mut self,
|
||||
focus: &Self::Focus,
|
||||
action: Self::Action,
|
||||
) -> Result<Option<Self::Event>, ComponentError> {
|
||||
match (focus, action) {
|
||||
(Focus::LoginButton, ComponentAction::Select) => Ok(Some(AppEvent::LoginAttempt {
|
||||
username: self.username.clone(),
|
||||
password: self.password.clone(),
|
||||
})),
|
||||
(Focus::Username, ComponentAction::TypeChar(c)) => {
|
||||
self.username.push(c);
|
||||
Ok(None)
|
||||
}
|
||||
(Focus::Password, ComponentAction::TypeChar(c)) => {
|
||||
self.password.push(c);
|
||||
Ok(None)
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), ComponentError> {
|
||||
let mut orch = Orchestrator::new();
|
||||
|
||||
orch.bind(Key::enter(), ComponentAction::Select);
|
||||
orch.bind(Key::tab(), ComponentAction::Next);
|
||||
orch.bind(Key::shift_tab(), ComponentAction::Prev);
|
||||
|
||||
let login_page = LoginPage {
|
||||
username: alloc::string::String::new(),
|
||||
password: alloc::string::String::new(),
|
||||
};
|
||||
|
||||
orch.register_page(alloc::string::String::from("login"), login_page);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
49
flake.nix
Normal file
49
flake.nix
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
description = "Komp AC - Kompress Accounting";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
mermaid-cli
|
||||
# Rust toolchain
|
||||
rustc
|
||||
cargo
|
||||
rustfmt
|
||||
clippy
|
||||
cargo-watch
|
||||
rust-analyzer
|
||||
cargo-tarpaulin
|
||||
cargo-flamegraph
|
||||
rust-code-analysis
|
||||
|
||||
# C build tools (for your linker issue)
|
||||
gcc
|
||||
binutils
|
||||
pkg-config
|
||||
|
||||
# OpenSSL for crypto dependencies
|
||||
openssl
|
||||
openssl.dev
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export OPENSSL_DIR="${pkgs.openssl.dev}"
|
||||
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
|
||||
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
|
||||
echo "🦀 Rust development environment loaded"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ComponentError {
|
||||
EmptyTargets,
|
||||
InvalidAction,
|
||||
InvalidFocus,
|
||||
NoComponent,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// path_from_the_root: src/component/mod.rs
|
||||
|
||||
pub mod action;
|
||||
pub mod error;
|
||||
pub mod r#trait;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// path_from_the_root: src/component/trait.rs
|
||||
|
||||
use super::error::ComponentError;
|
||||
use crate::focus::FocusId;
|
||||
use crate::input::Action;
|
||||
|
||||
pub trait Component {
|
||||
type Focus: FocusId;
|
||||
type Action: core::fmt::Debug + Clone;
|
||||
type Action: Action;
|
||||
type Event: Clone + core::fmt::Debug;
|
||||
|
||||
fn targets(&self) -> &[Self::Focus];
|
||||
@@ -34,7 +33,7 @@ pub trait Component {
|
||||
|
||||
fn handle_text(
|
||||
&mut self,
|
||||
focus: &Self::Focus,
|
||||
_focus: &Self::Focus,
|
||||
_ch: char,
|
||||
) -> Result<Option<Self::Event>, ComponentError> {
|
||||
Ok(None)
|
||||
|
||||
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
|
||||
|
||||
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 {
|
||||
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,5 +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 {}
|
||||
|
||||
/// 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,33 +1,57 @@
|
||||
// path_from_the_root: src/input/bindings.rs
|
||||
|
||||
use super::action::Action;
|
||||
use super::action::{Action, ComponentAction};
|
||||
use super::key::Key;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use hashbrown::HashSet;
|
||||
|
||||
/// Maps keys to actions.
|
||||
///
|
||||
/// When `alloc` feature is enabled, uses HashMap for O(1) lookup.
|
||||
/// Without `alloc`, uses heapless::LinearMap with fixed capacity N.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bindings<A: Action> {
|
||||
bindings: alloc::vec::Vec<(Key, A)>,
|
||||
pub struct Bindings<A: Action, const N: usize = 32> {
|
||||
#[cfg(feature = "alloc")]
|
||||
bindings: hashbrown::HashMap<Key, A>,
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
bindings: heapless::LinearMap<Key, A, N>,
|
||||
}
|
||||
|
||||
impl<A: Action> Bindings<A> {
|
||||
impl<A: Action, const N: usize> Bindings<A, N> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bindings: alloc::vec::Vec::new(),
|
||||
#[cfg(feature = "alloc")]
|
||||
bindings: hashbrown::HashMap::new(),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
bindings: heapless::LinearMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, key: Key, action: A) {
|
||||
self.bindings.push((key, action));
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
self.bindings.insert(key, action);
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
// V heapless verzii ignorujeme chybu pri plnej kapacite,
|
||||
// alebo by ste mohli vrátiť Result.
|
||||
let _ = self.bindings.insert(key, action);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &Key) -> Option<&A> {
|
||||
self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a)
|
||||
self.bindings.get(key)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &Key) {
|
||||
self.bindings.retain(|(k, _)| k != key);
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
self.bindings.remove(key);
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
self.bindings.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -38,25 +62,48 @@ impl<A: Action> Bindings<A> {
|
||||
self.bindings.len()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &(Key, A)> {
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn keys(&self) -> alloc::vec::Vec<&Key> {
|
||||
self.bindings.keys().collect()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
pub fn keys(&self) -> heapless::Vec<&Key, N> {
|
||||
let mut v = heapless::Vec::new();
|
||||
for k in self.bindings.keys() {
|
||||
let _ = v.push(k);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Key, &A)> {
|
||||
self.bindings.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Action> Default for Bindings<A> {
|
||||
impl<A: Action, const N: usize> Default for Bindings<A, N> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sequences")]
|
||||
impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
|
||||
impl<A: Action + core::hash::Hash + Eq, const N: usize> Bindings<A, N> {
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) {
|
||||
for key in keys {
|
||||
self.bindings.push((key, action.clone()));
|
||||
self.bindings.insert(key, action.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
pub fn bind_sequence<const K: usize>(&mut self, keys: heapless::Vec<Key, K>, action: A) {
|
||||
for key in keys {
|
||||
let _ = self.bindings.insert(key, action.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn get_sequences(&self) -> alloc::vec::Vec<&A> {
|
||||
let mut actions = alloc::vec::Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
@@ -67,4 +114,36 @@ impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
pub fn get_sequences(&self) -> heapless::Vec<&A, N> {
|
||||
let mut actions = heapless::Vec::new();
|
||||
// V no_std/no_alloc bez HashSetu robíme O(n^2) kontrolu unikátnosti
|
||||
for (_, action) in &self.bindings {
|
||||
if !actions.iter().any(|&a| a == action) {
|
||||
let _ = actions.push(action);
|
||||
}
|
||||
}
|
||||
actions
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_bindings() -> Bindings<ComponentAction, 16> {
|
||||
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")]
|
||||
use super::action::Action;
|
||||
|
||||
@@ -9,10 +7,20 @@ use super::key::Key;
|
||||
#[cfg(feature = "sequences")]
|
||||
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")]
|
||||
pub struct SequenceHandler<A: Action> {
|
||||
sequences: alloc::vec::Vec<(alloc::vec::Vec<Key>, A)>,
|
||||
current: alloc::vec::Vec<Key>,
|
||||
#[cfg(feature = "std")]
|
||||
last_timestamp: Option<Instant>,
|
||||
#[cfg(feature = "std")]
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sequences")]
|
||||
@@ -21,6 +29,17 @@ impl<A: Action> SequenceHandler<A> {
|
||||
Self {
|
||||
sequences: 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> {
|
||||
#[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);
|
||||
|
||||
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)]
|
||||
pub enum KeyCode {
|
||||
Char(char),
|
||||
@@ -20,6 +22,9 @@ pub enum KeyCode {
|
||||
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)]
|
||||
pub struct KeyModifiers {
|
||||
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)]
|
||||
pub struct Key {
|
||||
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 {
|
||||
let mut out = alloc::string::String::new();
|
||||
if self.modifiers.control {
|
||||
|
||||
@@ -5,11 +5,13 @@ pub mod bindings;
|
||||
pub mod handler;
|
||||
pub mod key;
|
||||
pub mod result;
|
||||
pub mod source;
|
||||
|
||||
pub use action::Action;
|
||||
pub use bindings::Bindings;
|
||||
pub use action::{Action, ComponentAction};
|
||||
pub use bindings::{default_bindings, Bindings};
|
||||
pub use key::{Key, KeyCode, KeyModifiers};
|
||||
pub use result::MatchResult;
|
||||
pub use source::{InputError, InputSource};
|
||||
|
||||
#[cfg(feature = "sequences")]
|
||||
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)]
|
||||
pub enum MatchResult<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>;
|
||||
}
|
||||
12
src/lib.rs
12
src/lib.rs
@@ -5,9 +5,15 @@ extern crate alloc;
|
||||
pub mod component;
|
||||
pub mod focus;
|
||||
pub mod input;
|
||||
pub mod orchestrator;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::component::*;
|
||||
pub use crate::focus::*;
|
||||
pub use crate::input::*;
|
||||
pub use crate::component::{Component, ComponentAction, ComponentError};
|
||||
pub use crate::focus::{FocusError, FocusId, FocusManager, FocusQuery, Focusable};
|
||||
pub use crate::input::{Action, Bindings, Key, KeyCode, KeyModifiers, MatchResult};
|
||||
pub use crate::orchestrator::{
|
||||
ActionResolver, CommandHandler, CommandResult, DefaultActionResolver,
|
||||
DefaultCommandHandler, DefaultStateCoordinator, EventBus, EventHandler, ModeName,
|
||||
ModeStack, Orchestrator, Router, RouterEvent, StateCoordinator, StateSync,
|
||||
};
|
||||
}
|
||||
|
||||
21
src/orchestrator/action_resolver.rs
Normal file
21
src/orchestrator/action_resolver.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// path_from_the_root: src/orchestrator/action_resolver.rs
|
||||
|
||||
use crate::component::Component;
|
||||
|
||||
pub struct ResolveContext<'a, C: Component> {
|
||||
pub component: &'a C,
|
||||
pub focus: &'a C::Focus,
|
||||
pub action: C::Action,
|
||||
}
|
||||
|
||||
pub trait ActionResolver<C: Component> {
|
||||
fn resolve(&mut self, ctx: ResolveContext<C>) -> C::Action;
|
||||
}
|
||||
|
||||
pub struct DefaultActionResolver;
|
||||
|
||||
impl<C: Component> ActionResolver<C> for DefaultActionResolver {
|
||||
fn resolve(&mut self, ctx: ResolveContext<C>) -> C::Action {
|
||||
ctx.action
|
||||
}
|
||||
}
|
||||
47
src/orchestrator/command_handler.rs
Normal file
47
src/orchestrator/command_handler.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// path_from_the_root: src/orchestrator/command_handler.rs
|
||||
|
||||
use crate::input::Key;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommandResult<A> {
|
||||
Resolved(A),
|
||||
Incomplete,
|
||||
Unknown,
|
||||
Exit,
|
||||
}
|
||||
|
||||
pub trait CommandHandler<A: Clone> {
|
||||
fn is_active(&self) -> bool;
|
||||
|
||||
fn handle(&mut self, key: Key) -> CommandResult<A>;
|
||||
|
||||
fn enter(&mut self);
|
||||
|
||||
fn exit(&mut self);
|
||||
}
|
||||
|
||||
pub struct DefaultCommandHandler<A: Clone> {
|
||||
_phantom: core::marker::PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<A: Clone> Default for DefaultCommandHandler<A> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
_phantom: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone> CommandHandler<A> for DefaultCommandHandler<A> {
|
||||
fn is_active(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn handle(&mut self, _key: Key) -> CommandResult<A> {
|
||||
CommandResult::Exit
|
||||
}
|
||||
|
||||
fn enter(&mut self) {}
|
||||
|
||||
fn exit(&mut self) {}
|
||||
}
|
||||
235
src/orchestrator/core.rs
Normal file
235
src/orchestrator/core.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
// path_from_the_root: src/orchestrator/core.rs
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::component::{Component, ComponentError};
|
||||
use crate::input::{Bindings, Key};
|
||||
use crate::orchestrator::{
|
||||
ActionResolver, CommandHandler, CommandResult, DefaultActionResolver, DefaultCommandHandler,
|
||||
DefaultStateCoordinator, EventBus, ModeName, ModeStack, ResolveContext, Router, RouterEvent,
|
||||
StateCoordinator,
|
||||
};
|
||||
|
||||
pub struct Orchestrator<C: Component> {
|
||||
router: Router<C>,
|
||||
bindings: Bindings<C::Action>,
|
||||
action_resolver: Box<dyn ActionResolver<C>>,
|
||||
command_handler: Box<dyn CommandHandler<C::Action>>,
|
||||
state_coordinator: Box<dyn StateCoordinator<C>>,
|
||||
modes: ModeStack,
|
||||
event_bus: EventBus<C::Event>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
impl<C: Component> Default for Orchestrator<C>
|
||||
where
|
||||
C::Action: Clone + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> Orchestrator<C>
|
||||
where
|
||||
C::Action: Clone + 'static,
|
||||
C::Event: Clone,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
router: Router::new(),
|
||||
bindings: Bindings::new(),
|
||||
action_resolver: Box::new(DefaultActionResolver),
|
||||
command_handler: Box::new(DefaultCommandHandler::default()),
|
||||
state_coordinator: Box::new(DefaultStateCoordinator),
|
||||
modes: ModeStack::new(),
|
||||
event_bus: EventBus::new(),
|
||||
running: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_page(&mut self, id: String, page: C) {
|
||||
self.router.register(id, page);
|
||||
}
|
||||
|
||||
pub fn navigate_to(&mut self, id: String) -> Result<(), ComponentError> {
|
||||
if let Some(RouterEvent::Navigated { from, to }) = self
|
||||
.router
|
||||
.navigate(id.clone())
|
||||
.map_err(|_| ComponentError::InvalidFocus)?
|
||||
{
|
||||
let _ = self.state_coordinator.on_navigate(from, to);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn back(&mut self) -> Result<(), ComponentError> {
|
||||
if let Some(RouterEvent::Back { to }) = self
|
||||
.router
|
||||
.back()
|
||||
.map_err(|_| ComponentError::InvalidFocus)?
|
||||
{
|
||||
let _ = self.state_coordinator.on_navigate(Some(to.clone()), to);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn forward(&mut self) -> Result<(), ComponentError> {
|
||||
if let Some(RouterEvent::Forward { to }) = self
|
||||
.router
|
||||
.forward()
|
||||
.map_err(|_| ComponentError::InvalidFocus)?
|
||||
{
|
||||
let _ = self.state_coordinator.on_navigate(Some(to.clone()), to);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, key: Key, action: C::Action) {
|
||||
self.bindings.bind(key, action);
|
||||
}
|
||||
|
||||
pub fn process_frame(&mut self, key: Key) -> Result<Vec<C::Event>, ComponentError> {
|
||||
if !self.running {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut events = Vec::new();
|
||||
|
||||
if self.command_handler.is_active() {
|
||||
match self.command_handler.handle(key) {
|
||||
CommandResult::Resolved(action) => {
|
||||
if let Some(event) = self.handle_action(action)? {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
CommandResult::Incomplete | CommandResult::Unknown => {}
|
||||
CommandResult::Exit => {
|
||||
self.command_handler.exit();
|
||||
}
|
||||
}
|
||||
} else if let Some(action) = self.bindings.get(&key) {
|
||||
let action = action.clone();
|
||||
|
||||
if let Some(_) = self.router.current_id() {
|
||||
let component = self.router.current().ok_or(ComponentError::NoComponent)?;
|
||||
let focus = self.router.focus_manager().current().ok_or(ComponentError::NoComponent)?;
|
||||
|
||||
let ctx = ResolveContext {
|
||||
component,
|
||||
focus,
|
||||
action,
|
||||
};
|
||||
let resolved_action = self.action_resolver.resolve(ctx);
|
||||
|
||||
if let Some(event) = self.handle_action(resolved_action)? {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for event in &events {
|
||||
self.event_bus.emit(event.clone());
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
fn handle_action(&mut self, action: C::Action) -> Result<Option<C::Event>, ComponentError> {
|
||||
let focus = self.router.focus_manager().current().cloned();
|
||||
|
||||
if let Some(focus) = focus {
|
||||
let old_focus = self.router.focus_manager().current().cloned();
|
||||
|
||||
let result = {
|
||||
let component = self.router.current_mut().ok_or(ComponentError::NoComponent)?;
|
||||
component.handle(&focus, action)?
|
||||
};
|
||||
|
||||
let new_focus = self.router.focus_manager().current().cloned();
|
||||
|
||||
if old_focus != new_focus {
|
||||
let _ = self.state_coordinator.on_focus_change(old_focus, new_focus);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&C> {
|
||||
self.router.current()
|
||||
}
|
||||
|
||||
pub fn current_mut(&mut self) -> Option<&mut C> {
|
||||
self.router.current_mut()
|
||||
}
|
||||
|
||||
pub fn focus_manager(&self) -> &crate::focus::FocusManager<C::Focus> {
|
||||
self.router.focus_manager()
|
||||
}
|
||||
|
||||
pub fn focus_manager_mut(&mut self) -> &mut crate::focus::FocusManager<C::Focus> {
|
||||
self.router.focus_manager_mut()
|
||||
}
|
||||
|
||||
pub fn modes(&self) -> &ModeStack {
|
||||
&self.modes
|
||||
}
|
||||
|
||||
pub fn modes_mut(&mut self) -> &mut ModeStack {
|
||||
&mut self.modes
|
||||
}
|
||||
|
||||
pub fn push_mode(&mut self, mode: ModeName) {
|
||||
self.modes.push(mode);
|
||||
}
|
||||
|
||||
pub fn pop_mode(&mut self) -> Option<ModeName> {
|
||||
self.modes.pop()
|
||||
}
|
||||
|
||||
pub fn event_bus(&self) -> &EventBus<C::Event> {
|
||||
&self.event_bus
|
||||
}
|
||||
|
||||
pub fn event_bus_mut(&mut self) -> &mut EventBus<C::Event> {
|
||||
&mut self.event_bus
|
||||
}
|
||||
|
||||
pub fn set_action_resolver<R: ActionResolver<C> + 'static>(&mut self, resolver: R) {
|
||||
self.action_resolver = Box::new(resolver);
|
||||
}
|
||||
|
||||
pub fn set_command_handler<H: CommandHandler<C::Action> + 'static>(
|
||||
&mut self,
|
||||
handler: H,
|
||||
) {
|
||||
self.command_handler = Box::new(handler);
|
||||
}
|
||||
|
||||
pub fn set_state_coordinator<S: StateCoordinator<C> + 'static>(&mut self, coordinator: S) {
|
||||
self.state_coordinator = Box::new(coordinator);
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.running
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.running = false;
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.running = true;
|
||||
}
|
||||
|
||||
pub fn current_id(&self) -> Option<&String> {
|
||||
self.router.current_id()
|
||||
}
|
||||
}
|
||||
46
src/orchestrator/event_bus.rs
Normal file
46
src/orchestrator/event_bus.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
// path_from_the_root: src/orchestrator/event_bus.rs
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub trait EventHandler<E> {
|
||||
fn handle(&mut self, event: E);
|
||||
}
|
||||
|
||||
pub struct EventBus<E: Clone> {
|
||||
handlers: Vec<Box<dyn EventHandler<E>>>,
|
||||
}
|
||||
|
||||
impl<E: Clone> Default for EventBus<E> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Clone> EventBus<E> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
handlers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&mut self, handler: Box<dyn EventHandler<E>>) {
|
||||
self.handlers.push(handler);
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, event: E) {
|
||||
for handler in &mut self.handlers {
|
||||
handler.handle(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handler_count(&self) -> usize {
|
||||
self.handlers.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.handlers.is_empty()
|
||||
}
|
||||
}
|
||||
17
src/orchestrator/mod.rs
Normal file
17
src/orchestrator/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
// path_from_the_root: src/orchestrator/mod.rs
|
||||
|
||||
pub mod action_resolver;
|
||||
pub mod command_handler;
|
||||
pub mod core;
|
||||
pub mod event_bus;
|
||||
pub mod mode;
|
||||
pub mod router;
|
||||
pub mod state_coordinator;
|
||||
|
||||
pub use action_resolver::{ActionResolver, DefaultActionResolver, ResolveContext};
|
||||
pub use command_handler::{CommandHandler, CommandResult, DefaultCommandHandler};
|
||||
pub use core::Orchestrator;
|
||||
pub use event_bus::{EventBus, EventHandler};
|
||||
pub use mode::{ModeName, ModeStack};
|
||||
pub use router::{Router, RouterEvent};
|
||||
pub use state_coordinator::{DefaultStateCoordinator, StateCoordinator, StateSync};
|
||||
83
src/orchestrator/mode.rs
Normal file
83
src/orchestrator/mode.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
// path_from_the_root: src/orchestrator/mode.rs
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ModeName {
|
||||
General,
|
||||
Navigation,
|
||||
Editing,
|
||||
Command,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ModeStack {
|
||||
modes: Vec<ModeName>,
|
||||
}
|
||||
|
||||
impl ModeStack {
|
||||
pub fn new() -> Self {
|
||||
Self { modes: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, mode: ModeName) {
|
||||
self.modes.push(mode);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<ModeName> {
|
||||
self.modes.pop()
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&ModeName> {
|
||||
self.modes.last()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.modes.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.modes.len()
|
||||
}
|
||||
|
||||
pub fn contains(&self, mode: &ModeName) -> bool {
|
||||
self.modes.contains(mode)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.modes.clear();
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, mode: ModeName) {
|
||||
self.modes.clear();
|
||||
self.modes.push(mode);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ModeResolver {
|
||||
mappings: BTreeMap<String, Vec<ModeName>>,
|
||||
}
|
||||
|
||||
impl ModeResolver {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
mappings: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&mut self, key: String, modes: Vec<ModeName>) {
|
||||
self.mappings.insert(key, modes);
|
||||
}
|
||||
|
||||
pub fn resolve(&self, key: &str) -> Option<&Vec<ModeName>> {
|
||||
self.mappings.get(key)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.mappings.is_empty()
|
||||
}
|
||||
}
|
||||
256
src/orchestrator/router.rs
Normal file
256
src/orchestrator/router.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
// path_from_the_root: src/orchestrator/router.rs
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use hashbrown::HashMap;
|
||||
|
||||
use crate::component::Component;
|
||||
use crate::focus::{FocusError, FocusManager};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RouterEvent {
|
||||
Navigated { from: Option<String>, to: String },
|
||||
Back { to: String },
|
||||
Forward { to: String },
|
||||
}
|
||||
|
||||
pub struct Router<C: Component> {
|
||||
#[cfg(feature = "alloc")]
|
||||
pages: HashMap<String, C>,
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
pages: Vec<(String, C)>,
|
||||
current: Option<String>,
|
||||
history: Vec<String>,
|
||||
future: Vec<String>,
|
||||
focus: FocusManager<C::Focus>,
|
||||
}
|
||||
|
||||
impl<C: Component> Default for Router<C> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> Router<C> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "alloc")]
|
||||
pages: HashMap::new(),
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
pages: Vec::new(),
|
||||
current: None,
|
||||
history: Vec::new(),
|
||||
future: Vec::new(),
|
||||
focus: FocusManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&mut self, id: String, mut page: C) {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
if self.current.as_ref() == Some(&id) {
|
||||
let _ = page.on_enter();
|
||||
let targets = page.targets();
|
||||
self.focus.set_targets(targets.to_vec());
|
||||
}
|
||||
self.pages.insert(id, page);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
if self.current.as_ref() == Some(&id) {
|
||||
let _ = page.on_enter();
|
||||
let targets = page.targets();
|
||||
self.focus.set_targets(targets.to_vec());
|
||||
}
|
||||
self.pages.push((id, page));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_page_mut(&mut self, id: &str) -> Option<&mut C> {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
self.pages.get_mut(id)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
self.pages
|
||||
.iter_mut()
|
||||
.find(|(page_id, _)| page_id == id)
|
||||
.map(|(_, page)| page)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_page(&self, id: &str) -> Option<&C> {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
self.pages.get(id)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
self.pages
|
||||
.iter()
|
||||
.find(|(page_id, _)| page_id == id)
|
||||
.map(|(_, page)| page)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn navigate(&mut self, id: String) -> Result<Option<RouterEvent>, FocusError> {
|
||||
let result = if let Some(current_id) = self.current.take() {
|
||||
if let Some(current_page) = self.get_page_mut(¤t_id) {
|
||||
let _ = current_page.on_exit();
|
||||
}
|
||||
self.history.push(current_id.clone());
|
||||
self.future.clear();
|
||||
Some(RouterEvent::Navigated {
|
||||
from: Some(current_id),
|
||||
to: id.clone(),
|
||||
})
|
||||
} else {
|
||||
Some(RouterEvent::Navigated {
|
||||
from: None,
|
||||
to: id.clone(),
|
||||
})
|
||||
};
|
||||
|
||||
let targets = if let Some(page) = self.get_page_mut(&id) {
|
||||
let _ = page.on_enter();
|
||||
Some(page.targets().to_vec())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(targets) = targets {
|
||||
self.focus.set_targets(targets);
|
||||
}
|
||||
|
||||
self.current = Some(id);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn back(&mut self) -> Result<Option<RouterEvent>, FocusError> {
|
||||
if let Some(current) = self.current.take() {
|
||||
if let Some(from) = self.history.pop() {
|
||||
self.future.push(current.clone());
|
||||
let to = from.clone();
|
||||
|
||||
if let Some(current_page) = self.get_page_mut(¤t) {
|
||||
let _ = current_page.on_exit();
|
||||
}
|
||||
|
||||
self.current = Some(to.clone());
|
||||
|
||||
let targets = if let Some(page) = self.get_page_mut(&to) {
|
||||
let _ = page.on_enter();
|
||||
Some(page.targets().to_vec())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(targets) = targets {
|
||||
self.focus.set_targets(targets);
|
||||
}
|
||||
|
||||
Ok(Some(RouterEvent::Back { to }))
|
||||
} else {
|
||||
self.current = Some(current);
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forward(&mut self) -> Result<Option<RouterEvent>, FocusError> {
|
||||
if let Some(current) = self.current.take() {
|
||||
if let Some(from) = self.future.pop() {
|
||||
self.history.push(current.clone());
|
||||
let to = from.clone();
|
||||
|
||||
if let Some(current_page) = self.get_page_mut(¤t) {
|
||||
let _ = current_page.on_exit();
|
||||
}
|
||||
|
||||
self.current = Some(to.clone());
|
||||
|
||||
let targets = if let Some(page) = self.get_page_mut(&to) {
|
||||
let _ = page.on_enter();
|
||||
Some(page.targets().to_vec())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(targets) = targets {
|
||||
self.focus.set_targets(targets);
|
||||
}
|
||||
|
||||
Ok(Some(RouterEvent::Forward { to }))
|
||||
} else {
|
||||
self.current = Some(current);
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<&C> {
|
||||
self.current.as_ref().and_then(|id| self.get_page(id))
|
||||
}
|
||||
|
||||
pub fn current_mut(&mut self) -> Option<&mut C> {
|
||||
if let Some(id) = self.current.clone() {
|
||||
self.get_page_mut(&id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_id(&self) -> Option<&String> {
|
||||
self.current.as_ref()
|
||||
}
|
||||
|
||||
pub fn focus_manager(&self) -> &FocusManager<C::Focus> {
|
||||
&self.focus
|
||||
}
|
||||
|
||||
pub fn focus_manager_mut(&mut self) -> &mut FocusManager<C::Focus> {
|
||||
&mut self.focus
|
||||
}
|
||||
|
||||
pub fn history(&self) -> &[String] {
|
||||
&self.history
|
||||
}
|
||||
|
||||
pub fn future(&self) -> &[String] {
|
||||
&self.future
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
self.pages.len()
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
self.pages.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_page(&self, id: &str) -> bool {
|
||||
#[cfg(feature = "alloc")]
|
||||
{
|
||||
self.pages.contains_key(id)
|
||||
}
|
||||
#[cfg(not(feature = "alloc"))]
|
||||
{
|
||||
self.pages.iter().any(|(page_id, _)| page_id == id)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/orchestrator/state_coordinator.rs
Normal file
48
src/orchestrator/state_coordinator.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
// path_from_the_root: src/orchestrator/state_coordinator.rs
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::component::Component;
|
||||
|
||||
pub enum StateSync {
|
||||
Synced,
|
||||
Conflict { details: String },
|
||||
}
|
||||
|
||||
pub trait StateCoordinator<C: Component> {
|
||||
fn on_navigate(&mut self, from: Option<String>, to: String) -> Result<StateSync, String>;
|
||||
|
||||
fn on_focus_change(
|
||||
&mut self,
|
||||
old: Option<C::Focus>,
|
||||
new: Option<C::Focus>,
|
||||
) -> Result<StateSync, String>;
|
||||
|
||||
fn on_mode_change(&mut self, _old: Vec<String>, _new: Vec<String>)
|
||||
-> Result<StateSync, String>;
|
||||
}
|
||||
|
||||
pub struct DefaultStateCoordinator;
|
||||
|
||||
impl<C: Component> StateCoordinator<C> for DefaultStateCoordinator {
|
||||
fn on_navigate(&mut self, _from: Option<String>, _to: String) -> Result<StateSync, String> {
|
||||
Ok(StateSync::Synced)
|
||||
}
|
||||
|
||||
fn on_focus_change(
|
||||
&mut self,
|
||||
_old: Option<C::Focus>,
|
||||
_new: Option<C::Focus>,
|
||||
) -> Result<StateSync, String> {
|
||||
Ok(StateSync::Synced)
|
||||
}
|
||||
|
||||
fn on_mode_change(
|
||||
&mut self,
|
||||
_old: Vec<String>,
|
||||
_new: Vec<String>,
|
||||
) -> Result<StateSync, String> {
|
||||
Ok(StateSync::Synced)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use tui_orchestrator::input::{Bindings, Key};
|
||||
extern crate alloc;
|
||||
|
||||
use tui_orchestrator::input::{Action, Bindings, Key};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[allow(dead_code)]
|
||||
@@ -8,6 +10,8 @@ enum TestAction {
|
||||
Open,
|
||||
}
|
||||
|
||||
impl Action for TestAction {}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_new() {
|
||||
let _bindings: Bindings<TestAction> = Bindings::new();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
extern crate alloc;
|
||||
|
||||
use tui_orchestrator::component::{Component, ComponentAction};
|
||||
use tui_orchestrator::focus::FocusId;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum TestFocus {
|
||||
@@ -9,6 +10,8 @@ enum TestFocus {
|
||||
ButtonC,
|
||||
}
|
||||
|
||||
impl FocusId for TestFocus {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum TestEvent {
|
||||
ButtonCPressed,
|
||||
@@ -51,11 +54,22 @@ impl Component for TestComponent {
|
||||
self.field_b.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_text(
|
||||
&mut self,
|
||||
focus: &Self::Focus,
|
||||
ch: char,
|
||||
) -> Result<Option<Self::Event>, tui_orchestrator::component::error::ComponentError> {
|
||||
match focus {
|
||||
Self::Focus::FieldA | Self::Focus::FieldB => Ok(Some(Self::Event::TextTyped(ch))),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_targets() {
|
||||
let mut component = TestComponent {
|
||||
let component = TestComponent {
|
||||
field_a: alloc::string::String::new(),
|
||||
field_b: alloc::string::String::new(),
|
||||
};
|
||||
@@ -109,7 +123,7 @@ fn test_component_on_enter_clears() {
|
||||
|
||||
#[test]
|
||||
fn test_component_defaults() {
|
||||
let component = TestComponent {
|
||||
let mut component = TestComponent {
|
||||
field_a: alloc::string::String::new(),
|
||||
field_b: alloc::string::String::new(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extern crate alloc;
|
||||
|
||||
use tui_orchestrator::focus::{FocusError, FocusManager, Focusable};
|
||||
use tui_orchestrator::focus::{FocusError, FocusId, FocusManager, Focusable};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[allow(dead_code)]
|
||||
@@ -11,6 +11,8 @@ enum TestId {
|
||||
Dialog,
|
||||
}
|
||||
|
||||
impl FocusId for TestId {}
|
||||
|
||||
#[test]
|
||||
fn test_focus_id_trait() {
|
||||
let id1 = TestId::Button("save");
|
||||
@@ -253,31 +255,3 @@ fn test_focusable_trait() {
|
||||
let targets = component.focus_targets();
|
||||
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