Recreate repository due to Git object corruption (all files preserved)

This commit is contained in:
filipriec_vm
2026-01-11 09:53:37 +01:00
commit 35b9e8e5a8
54 changed files with 4803 additions and 0 deletions

1
.git_corrupt_backup/HEAD Normal file
View File

@@ -0,0 +1 @@
ref: refs/heads/main

View File

@@ -0,0 +1,11 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
symlinks = true
ignorecase = false
precomposeunicode = false
[remote "origin"]
url = ssh://git@git.farmeris.sk:2222/filipriec/pages-tui.git
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1 @@
Unnamed repository; everything before the `;` is the name of the repository.

View File

@@ -0,0 +1,25 @@
#!/bin/sh
# A sample hook to check commit messages created by `git am`
###########################################################
#
# When you receive a patch via email, the `git am` command is commonly used to apply
# that patch. During the `git am` process, the `applypatch-msg` hook is executed before
# creating the commit. Its purpose is to validate and modify the commit log message
# before the patch is applied as a commit in your Git repository.
#
# This script serves as an example to validate that the commit message introduced by
# the patch from an email would pass the `commit-msg` hook, which would be executed
# if you had created the commit yourself.
#
# This hook is the first and followed up by `pre-applypatch` and `post-applypatch`.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
# Retrieve the path of the commit-msg hook script.
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
# If the commit-msg hook script is executable, execute it and pass any command-line arguments to it.
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
# Be sure to exit without error if `exec` isn't called.
:

View File

@@ -0,0 +1,25 @@
#!/bin/sh
# A sample hook to check commit messages created by `git commit`
################################################################
#
# This example script checks commit messages for duplicate `Signed-off-by`
# lines and rejects the commit if these are present.
#
# It is called by "git commit" with a single argument: the name of the file
# that contains the final commit message, which would be used in the commit.
# A a non-zero exit status after issuing an appropriate message stops the operation.
# The hook is allowed to edit the commit message file by rewriting the file
# containing it.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
# Check for duplicate Signed-off-by lines in the commit message.
# The following command uses grep to find lines starting with "Signed-off-by: "
# in the commit message file specified by the first argument `$1`.
# It then sorts the lines, counts the number of occurrences of each line,
# and removes any lines that occur only once.
# If there are any remaining lines, it means there are duplicate Signed-off-by lines.
test "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" = "" || {
echo "Remove duplicate Signed-off-by lines and repeat the commit." 1>&2
exit 1
}

View File

@@ -0,0 +1 @@
https://git-scm.com/docs/githooks

View File

@@ -0,0 +1,16 @@
#!/usr/bin/sh
# How to use hook-based fs-monitor integrations
###############################################
# This script is meant as a placeholder for integrating filesystem monitors with git
# using hooks in order to speed up commands like `git-status`.
#
# To setup the fs-monitor for use with watchman, run
# `git config core.fsmonitor .git/hooks/fsmonitor-watchman` and paste the content of
# the example script over at https://github.com/git/git/blob/aa9166bcc0ba654fc21f198a30647ec087f733ed/templates/hooks--fsmonitor-watchman.sample
# into `.git/hooks/fsmonitor-watchman`.
#
# Note that by now and as of this writing on MacOS and Windows and starting from git 2.35.1
# one can use the built-in fs-monitor implementation using `git config core.fsmonitor true`
exit 42

View File

@@ -0,0 +1,12 @@
#!/bin/sh
# A sample hook that runs after receiving a pack on a remote
############################################################
# This hook is called after a pack was received on the remote, i.e. after a successful `git push` operation.
# It's useful on the server side only.
#
# There many more receive hooks which are documented in the official documentation: https://git-scm.com/docs/githooks.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
# Update static files to support the 'dumb' git HTTP protocol.
exec git update-server-info

View File

@@ -0,0 +1,27 @@
#!/bin/sh
# A sample hook to check commit messages created by `git am`
###########################################################
# This hook script is triggered by `git am` without any context just before creating a commit,
# which is useful to inspect the current tree or run scripts for further verification.
#
# If it exits with a non-zero exit code, the commit will not be created. Everything printed
# to the output or error channels will be visible to the user.
#
# Note that there is a sibling hook called `post-applypatch` (also without further context)
# which is run after the commit was created. It is useful to use the commit hash for further
# processing, like sending information to the involved parties.
# Finally, the `applypatch-msg` hook is called at the very beginning of the `git am` operation
# to provide access to the commit-message.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
# Retrieve the path to the pre-commit hook script using the "git rev-parse" command.
precommit="$(git rev-parse --git-path hooks/pre-commit)"
# Check if the pre-commit hook script exists and is executable.
# If it does, execute it passing the arguments from this script (if any) using the "exec" command.
test -x "$precommit" && exec "$precommit" ${1+"$@"}
# Be sure to exit without error if `exec` isn't called.
:

View File

@@ -0,0 +1,19 @@
#!/bin/sh
# A sample hook to prevent commits with merge-markers
#####################################################
# This example hook rejects changes that are about to be committed with merge markers,
# as that would be a clear indication of a failed merge. It is triggered by `git commit`
# and returning with non-zero exit status prevents the commit from being created.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
# Check for merge markers in modified files
for file in $(git diff --cached --name-only); do
if grep -q -E '^(<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|)$' "$file"; then
echo "Error: File '$file' contains merge markers. Please remove them before committing."
exit 1
fi
done
# Exit with success if there are no errors
exit 0

View File

@@ -0,0 +1,16 @@
#!/bin/sh
# A sample hook to check commits created by `git merge`
#######################################################
#
# This hook is invoked by `git merge` without further context right before creating a commit.
# It should be used to validate the current state that is supposed to be committed, or exit
# with a non-zero status to prevent the commit.
# All output will be visible to the user.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
# Check if the pre-commit hook exists and is executable. If it is, it executes the pre-commit hook script.
test -x "$GIT_DIR/hooks/pre-commit" && exec "$GIT_DIR/hooks/pre-commit"
# Be sure to exit without error if `exec` isn't called.
:

View File

@@ -0,0 +1,46 @@
#!/bin/sh
# Check for "DELME" in commit messages of about-to-be-pushed commits
####################################################################
# This hook script is triggered by `git push` right after a connection to the remote
# was established and its initial response was received, and right before generating
# and pushing a pack-file.
# The operation will be aborted when exiting with a non-zero status.
#
# The following arguments are provided:
#
# $1 - The symbolic name of the remote to push to, like "origin" or the URL like "https://github.com/GitoxideLabs/gitoxide" if there is no such name.
# $2 - The URL of the remote to push to, like "https://github.com/GitoxideLabs/gitoxide".
#
# The hook should then read from standard input in a line-by-line fashion and split the following space-separated fields:
#
# * local ref - the left side of a ref-spec, i.e. "local" of the "local:refs/heads/remote" ref-spec
# * local hash - the hash of the commit pointed to by `local ref`
# * remote ref - the right side of a ref-spec, i.e. "refs/heads/remote" of the "local:refs/heads/remote" ref-spec
# * remote hash - the hash of the commit pointed to by `remote ref`
#
# In this example, we abort the push if any of the about-to-be-pushed commits have "DELME" in their commit message.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
remote="$1"
url="$2"
# Check each commit being pushed
while read _local_ref local_hash _remote_ref _remote_hash; do
# Skip if the local hash is all zeroes (deletion)
zero_sha=$(printf "%0${#local_hash}d" 0)
if [ "$local_hash" = "$zero_sha" ]; then
continue
fi
# Get the commit message
commit_msg=$(git log --format=%s -n 1 "$local_hash")
# Check if the commit message contains "DELME"
if echo "$commit_msg" | grep -iq "DELME"; then
echo "Error: Found commit with 'DELME' in message. Push aborted to $remote ($url) aborted." 1>&2
exit 1
fi
done
# If no commit with "DELME" found, allow the push
exit 0

View File

@@ -0,0 +1,40 @@
#!/bin/sh
# A sample hook to validate the branches involved in a rebase operation
#######################################################################
#
# This hook is invoked right before `git rebase` starts its work and
# prevents anything else to happen by returning a non-zero exit code.
#
# The following arguments are provided:
#
# $1 - the branch that contains the commit from which $2 was forked.
# $2 - the branch being rebased or no second argument at all if the rebase applies to `HEAD`.
#
# This example hook aborts the rebase operation if the branch being rebased is not up to date
# with the latest changes from the upstream branch, or if there are any uncommitted changes.
#
# To enable this hook remove the `.sample` suffix from this file entirely.
upstream_branch=$1
if [ "$#" -eq 2 ]; then
branch_being_rebased=$2
else
branch_being_rebased=$(git symbolic-ref --quiet --short HEAD) || exit 0 # ignore rebases on detached heads
fi
# Check if the branch being rebased is behind the upstream branch
if git log --oneline ${upstream_branch}..${branch_being_rebased} > /dev/null; then
echo "Warning: The branch being rebased (${branch_being_rebased}) is behind the upstream branch (${upstream_branch})." 1>&2
echo "Please update your branch before rebasing." 1>&2
exit 1
fi
# Check if there are any uncommitted changes
if ! git diff-index --quiet HEAD --; then
echo "Warning: There are uncommitted changes in your branch ${branch_being_rebased}." 1>&2
echo "Please commit or stash your changes before rebasing." 1>&2
exit 2
fi
# All good, let the rebase proceed.
exit 0

View File

@@ -0,0 +1,54 @@
#!/bin/sh
# A hook called by `git commit` to adjust the commit message right before the user sees it
##########################################################################################
#
# This script is called by `git commit` after commit message was initialized and right before
# an editor is launched.
#
# It receives one to three arguments:
#
# $1 - the path to the file containing the commit message. It can be edited to change the message.
# $2 - the kind of source of the message contained in $1. Possible values are
# "message" - a message was provided via `-m` or `-F`
# "commit" - `-c`, `-C` or `--amend` was given
# "squash" - the `.git/SQUASH_MSG` file exists
# "merge" - this is a merge or the `.git/MERGE` file exists
# "template" - `-t` was provided or `commit.template` was set
# $3 - If $2 is "commit" then this is the hash of the commit.
# It can also take other values, best understood by studying the source code at
# https://github.com/git/git/blob/aa9166bcc0ba654fc21f198a30647ec087f733ed/builtin/commit.c#L745
#
# The following example
#
# To enable this hook remove the `.sample` suffix from this file entirely.
COMMIT_MSG_FILE=$1
# Check if the commit message file is empty or already contains a message
if [ -s "$COMMIT_MSG_FILE" ]; then
# If the commit message is already provided, exit without making any changes.
# This can happen if the user provided a message via `-m` or a template.
exit 0
fi
# Retrieve the branch name from the current HEAD commit
BRANCH_NAME=$(git symbolic-ref --short HEAD)
# Generate a default commit message based on the branch name
DEFAULT_MSG=""
case "$BRANCH_NAME" in
"feature/*")
DEFAULT_MSG="feat: "
;;
"bugfix/*")
DEFAULT_MSG="fix: "
;;
*)
DEFAULT_MSG="chore: "
;;
esac
# Set the commit message that will be presented to the user.
echo "$DEFAULT_MSG" > "$COMMIT_MSG_FILE"

BIN
.git_corrupt_backup/index Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
# This file contains repository-wide exclude patterns that git will ignore.
# They are local and will not be shared when pushing or pulling.
# When using Rust the following would be typical exclude patterns.
# Remove the '# ' prefix to let them take effect.
# /target/

View File

View File

View File

@@ -0,0 +1 @@
777b68e48a535cf1c87c2b4889b78efc439dd52c

View File

@@ -0,0 +1,28 @@
# pack-refs with: peeled fully-peeled sorted
9693db5919088f5fa91e5d200818ebf566ec9d8f refs/heads/main
09f6a116c1001fb295872421af6f61296c68c7ac refs/jj/keep/09f6a116c1001fb295872421af6f61296c68c7ac
11bd90f3aeded245e3e57747d22bf95704150198 refs/jj/keep/11bd90f3aeded245e3e57747d22bf95704150198
134505e578ab378b0072612f63582ec88fb9cc00 refs/jj/keep/134505e578ab378b0072612f63582ec88fb9cc00
1d40cc8da38259f9e3e14e07e9d0fc3acf36b32f refs/jj/keep/1d40cc8da38259f9e3e14e07e9d0fc3acf36b32f
2128d0ea288ef40a5f1a4a6a2ef935aca32cb7dc refs/jj/keep/2128d0ea288ef40a5f1a4a6a2ef935aca32cb7dc
27b811fe102b61e549576590721f58d3e1a82628 refs/jj/keep/27b811fe102b61e549576590721f58d3e1a82628
281b18224ebdb30488b3a632e0e97861af85a0db refs/jj/keep/281b18224ebdb30488b3a632e0e97861af85a0db
3bc1faa632fe2bdf18e4b17d137b20e8de73c83d refs/jj/keep/3bc1faa632fe2bdf18e4b17d137b20e8de73c83d
6f24966c51a784ecb168bc3455953b41e454ed51 refs/jj/keep/6f24966c51a784ecb168bc3455953b41e454ed51
777b68e48a535cf1c87c2b4889b78efc439dd52c refs/jj/keep/777b68e48a535cf1c87c2b4889b78efc439dd52c
8a9bd43aa3a2b4840ae0ba5e887815ddbcc36c85 refs/jj/keep/8a9bd43aa3a2b4840ae0ba5e887815ddbcc36c85
8c75d691d8dead10c195a160327840f64e463202 refs/jj/keep/8c75d691d8dead10c195a160327840f64e463202
923d0b1c1d2f5f5a0c7332d7fc193fb50ed08c25 refs/jj/keep/923d0b1c1d2f5f5a0c7332d7fc193fb50ed08c25
9693db5919088f5fa91e5d200818ebf566ec9d8f refs/jj/keep/9693db5919088f5fa91e5d200818ebf566ec9d8f
ac2bf616eb6117d6c84f73b652a0a13e15bacbec refs/jj/keep/ac2bf616eb6117d6c84f73b652a0a13e15bacbec
bea70e92069cd95bff29f3b86ceaad0a093e53bb refs/jj/keep/bea70e92069cd95bff29f3b86ceaad0a093e53bb
c513c11b8030e99decf7ba949b5f677b60111cef refs/jj/keep/c513c11b8030e99decf7ba949b5f677b60111cef
ce384ada9570ac1b00bc21a2ea8ed8ffefadb7ab refs/jj/keep/ce384ada9570ac1b00bc21a2ea8ed8ffefadb7ab
cec4b35d4d615b101ced81108d6f3ed7b370f72d refs/jj/keep/cec4b35d4d615b101ced81108d6f3ed7b370f72d
d44c4504797c3aafc9af7915f834919ba06d71f4 refs/jj/keep/d44c4504797c3aafc9af7915f834919ba06d71f4
e30edcdea549a0f66892ae8815628c6c7c2a5abc refs/jj/keep/e30edcdea549a0f66892ae8815628c6c7c2a5abc
e595deb42806579768549f2c51601a8c6dfd997d refs/jj/keep/e595deb42806579768549f2c51601a8c6dfd997d
eb0e7d3a819b5f23ed3857d243298bf2ee664d0e refs/jj/keep/eb0e7d3a819b5f23ed3857d243298bf2ee664d0e
ebb069d0d7e3355f47f4d1ad7d7e8eaa9c2aed6b refs/jj/keep/ebb069d0d7e3355f47f4d1ad7d7e8eaa9c2aed6b
ef491813784ea6d7881c4a34912aa21c3e9caac3 refs/jj/keep/ef491813784ea6d7881c4a34912aa21c3e9caac3
f173f20b3193c97694db77ba9116e9c657f2f6f8 refs/jj/keep/f173f20b3193c97694db77ba9116e9c657f2f6f8

View File

@@ -0,0 +1 @@
6f24966c51a784ecb168bc3455953b41e454ed51

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
komp_ac_client/
target/

32
AGENTS.md Normal file
View File

@@ -0,0 +1,32 @@
## Architecture
- Allways follow feature-based structuring
- Feature-based tree structure—group by domain, not by type
- Each feature is self-contained: handler, logic, types, tests
- Functional programming style
- Use structs, traits, enums, `impl`, `match` over `if`
- Avoid shared mutable state—decouple with enums
- Keep it simple: small, decoupled, easy-to-read blocks
- Don't invent new states/booleans—reuse existing features
- Forbidden to use Arc, Mutex, RefCell and others
## File Structure
- `mod.rs` is for routing only, no logic
- Tests live in `tests/` dir equivalent to src/
- If a feature exceeds 510 files, reconsider the design
- Nest features logically: `auth/`, `auth/login/`, `auth/register/`
## Error Handling
- Use `Result<T, E>` everywhere—no `.unwrap()` in production code(tests can use unwraps)
- Custom error enums per feature, map to a shared app error at boundaries
## Naming
- Clear, descriptive names—no abbreviations
- Types are nouns, functions are verbs
- Top of the file should always contain // path_from_the_root
## Dependencies
- Always use the latest stable versions
- No legacy or deprecated versions for compatibility
## Komp_ac
Komp_ac_client is a codebase out of the app, we are getting inspired from. We only copy code out of it. Its already in gitignore

39
Cargo.lock generated Normal file
View File

@@ -0,0 +1,39 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "equivalent"
version = "1.0.2"
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"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "tui_orchestrator"
version = "0.1.0"
dependencies = [
"hashbrown",
]

16
Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "tui_orchestrator"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[features]
default = ["std"]
std = []
alloc = ["hashbrown"]
sequences = ["alloc"]
[dependencies]
hashbrown = { version = "0.15", optional = true }
[dev-dependencies]

734
INPUT_PIPELINE_MIGRATION.md Normal file
View File

@@ -0,0 +1,734 @@
# Input Pipeline Migration Guide
## Goal
Migrate `komp_ac_client/src/input_pipeline/` to a generalized `no_std` compatible API in `src/input_pipeline/` that can be used by any TUI application.
## Current State (komp_ac_client)
Files in `komp_ac_client/src/input_pipeline/`:
- `key_chord.rs` - Uses crossterm::event::{KeyCode, KeyModifiers}
- `sequence.rs` - Uses std::time::{Duration, Instant}
- `registry.rs` - Uses std::collections::HashMap
- `pipeline.rs` - Pure logic
- `response.rs` - Pure types
Dependencies to remove:
- `crossterm::event` - Replace with custom types
- `std::time` - Replace with alloc-based solution or make optional
- `std::collections` - Use alloc or custom structures
## Target Structure
```
src/input_pipeline/
├── mod.rs # Routing only
├── key.rs # KeyCode, KeyModifiers enums (no_std)
├── chord.rs # KeyChord type (no_std)
├── sequence.rs # KeySequence type (no_std)
├── key_map.rs # KeyMap: Chord -> Action mapping (no_std)
├── key_registry.rs # Registry for storing key bindings (alloc)
├── sequence_tracker.rs # Track incomplete sequences (optional with std feature)
├── pipeline.rs # Main pipeline logic (no_std)
└── response.rs # Response types (no_std)
```
## Step 1: Core Types (no_std)
### `src/input_pipeline/key.rs`
Define backend-agnostic KeyCode and KeyModifiers:
```rust
// path_from_the_root: src/input_pipeline/key.rs
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyCode {
Char(char),
Enter,
Tab,
Esc,
Backspace,
Delete,
Home,
End,
PageUp,
PageDown,
Up,
Down,
Left,
Right,
F(u8),
Null,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct KeyModifiers {
pub control: bool,
pub alt: bool,
pub shift: bool,
}
impl KeyModifiers {
pub const fn new() -> Self {
Self {
control: false,
alt: false,
shift: false,
}
}
pub const fn with_control(mut self) -> Self {
self.control = true;
self
}
pub const fn with_alt(mut self) -> Self {
self.alt = true;
self
}
pub const fn with_shift(mut self) -> Self {
self.shift = true;
self
}
pub const fn is_empty(&self) -> bool {
!self.control && !self.alt && !self.shift
}
}
```
### `src/input_pipeline/chord.rs`
Define KeyChord using custom types:
```rust
// path_from_the_root: src/input_pipeline/chord.rs
use super::key::{KeyCode, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyChord {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyChord {
pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
pub const fn char(c: char) -> Self {
Self {
code: KeyCode::Char(c),
modifiers: KeyModifiers::new(),
}
}
pub fn display_string(&self) -> alloc::string::String {
let mut out = alloc::string::String::new();
if self.modifiers.control {
out.push_str("Ctrl+");
}
if self.modifiers.alt {
out.push_str("Alt+");
}
if self.modifiers.shift {
out.push_str("Shift+");
}
match self.code {
KeyCode::Char(c) => out.push(c),
KeyCode::Enter => out.push_str("Enter"),
KeyCode::Tab => out.push_str("Tab"),
KeyCode::Esc => out.push_str("Esc"),
KeyCode::Backspace => out.push_str("Backspace"),
KeyCode::Delete => out.push_str("Delete"),
KeyCode::Up => out.push_str("Up"),
KeyCode::Down => out.push_str("Down"),
KeyCode::Left => out.push_str("Left"),
KeyCode::Right => out.push_str("Right"),
KeyCode::F(n) => {
out.push('F');
out.push(char::from_digit(n as u32, 10).unwrap_or('0'));
}
KeyCode::Home => out.push_str("Home"),
KeyCode::End => out.push_str("End"),
KeyCode::PageUp => out.push_str("PageUp"),
KeyCode::PageDown => out.push_str("PageDown"),
KeyCode::Null => out.push_str("Null"),
}
out
}
}
impl From<KeyCode> for KeyChord {
fn from(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::new(),
}
}
}
```
## Step 2: Sequence Types (no_std)
### `src/input_pipeline/sequence.rs`
```rust
// path_from_the_root: src/input_pipeline/sequence.rs
use super::chord::KeyChord;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeySequence {
chords: alloc::vec::Vec<KeyChord>,
}
impl KeySequence {
pub fn new() -> Self {
Self {
chords: alloc::vec::Vec::new(),
}
}
pub fn from_chords(chords: impl IntoIterator<Item = KeyChord>) -> Self {
Self {
chords: chords.into_iter().collect(),
}
}
pub fn push(&mut self, chord: KeyChord) {
self.chords.push(chord);
}
pub fn chords(&self) -> &[KeyChord] {
&self.chords
}
pub fn len(&self) -> usize {
self.chords.len()
}
pub fn is_empty(&self) -> bool {
self.chords.is_empty()
}
pub fn starts_with(&self, other: &KeySequence) -> bool {
self.chords.starts_with(other.chords())
}
}
```
### `src/input_pipeline/key_map.rs`
```rust
// path_from_the_root: src/input_pipeline/key_map.rs
use super::chord::KeyChord;
use super::sequence::KeySequence;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyMapEntry<Action> {
Chord(KeyChord, Action),
Sequence(KeySequence, Action),
}
impl<Action: Clone> KeyMapEntry<Action> {
pub fn chord(chord: KeyChord, action: Action) -> Self {
Self::Chord(chord, action)
}
pub fn sequence(sequence: KeySequence, action: Action) -> Self {
Self::Sequence(sequence, action)
}
pub fn action(&self) -> &Action {
match self {
Self::Chord(_, action) | Self::Sequence(_, action) => action,
}
}
}
```
## Step 3: Response Types (no_std)
### `src/input_pipeline/response.rs`
```rust
// path_from_the_root: src/input_pipeline/response.rs
use super::chord::KeyChord;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PipelineResponse<Action> {
Execute(Action),
Type(KeyChord),
Wait(alloc::vec::Vec<InputHint<Action>>),
Cancel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputHint<Action> {
pub chord: KeyChord,
pub action: Action,
}
```
## Step 4: Key Registry (alloc)
### `src/input_pipeline/key_registry.rs`
```rust
// path_from_the_root: src/input_pipeline/key_registry.rs
use super::chord::KeyChord;
use super::sequence::KeySequence;
use super::key_map::KeyMapEntry;
#[derive(Debug, Clone)]
pub struct KeyRegistry<Action> {
chords: alloc::collections::HashMap<KeyChord, Action>,
sequences: alloc::vec::Vec<(KeySequence, Action)>,
}
impl<Action: Clone> KeyRegistry<Action> {
pub fn new() -> Self {
Self {
chords: alloc::collections::HashMap::new(),
sequences: alloc::vec::Vec::new(),
}
}
pub fn register_chord(&mut self, chord: KeyChord, action: Action) {
self.chords.insert(chord, action);
}
pub fn register_sequence(&mut self, sequence: KeySequence, action: Action) {
self.sequences.push((sequence, action));
}
pub fn get_chord(&self, chord: &KeyChord) -> Option<&Action> {
self.chords.get(chord)
}
pub fn find_sequences_starting_with(
&self,
prefix: &KeySequence,
) -> alloc::vec::Vec<&KeySequence> {
self.sequences
.iter()
.filter(|(seq, _)| seq.starts_with(prefix))
.map(|(seq, _)| seq)
.collect()
}
pub fn get_sequence(&self, sequence: &KeySequence) -> Option<&Action> {
self.sequences
.iter()
.find(|(seq, _)| seq == sequence)
.map(|(_, action)| action)
}
}
impl<Action: Clone> Default for KeyRegistry<Action> {
fn default() -> Self {
Self::new()
}
}
```
## Step 5: Sequence Tracker (optional, with std feature)
### `src/input_pipeline/sequence_tracker.rs`
```rust
// path_from_the_root: src/input_pipeline/sequence_tracker.rs
use super::sequence::KeySequence;
#[derive(Debug, Clone)]
pub struct SequenceTracker {
current: KeySequence,
#[cfg(feature = "std")]
last_input: Option<std::time::Instant>,
#[cfg(feature = "std")]
timeout: std::time::Duration,
}
impl SequenceTracker {
pub fn new() -> Self {
Self {
current: KeySequence::new(),
#[cfg(feature = "std")]
last_input: None,
#[cfg(feature = "std")]
timeout: std::time::Duration::from_millis(1000),
}
}
#[cfg(feature = "std")]
pub fn with_timeout(timeout_ms: u64) -> Self {
Self {
current: KeySequence::new(),
last_input: None,
timeout: std::time::Duration::from_millis(timeout_ms),
}
}
pub fn reset(&mut self) {
self.current = KeySequence::new();
#[cfg(feature = "std")]
{
self.last_input = None;
}
}
pub fn add(&mut self, chord: KeyChord) {
self.current.push(chord);
#[cfg(feature = "std")]
{
self.last_input = Some(std::time::Instant::now());
}
}
#[cfg(feature = "std")]
pub fn is_expired(&self) -> bool {
match self.last_input {
None => false,
Some(last) => last.elapsed() > self.timeout,
}
}
#[cfg(not(feature = "std"))]
pub fn is_expired(&self) -> bool {
false
}
pub fn current(&self) -> &KeySequence {
&self.current
}
pub fn is_empty(&self) -> bool {
self.current.is_empty()
}
}
impl Default for SequenceTracker {
fn default() -> Self {
Self::new()
}
}
```
## Step 6: Pipeline Logic (no_std)
### `src/input_pipeline/pipeline.rs`
```rust
// path_from_the_root: src/input_pipeline/pipeline.rs
use super::chord::KeyChord;
use super::key_registry::KeyRegistry;
use super::sequence::KeySequence;
use super::response::{PipelineResponse, InputHint};
use super::sequence_tracker::SequenceTracker;
pub struct KeyPipeline<Action: Clone> {
registry: KeyRegistry<Action>,
#[cfg(feature = "std")]
tracker: SequenceTracker,
}
impl<Action: Clone> KeyPipeline<Action> {
pub fn new() -> Self {
Self {
registry: KeyRegistry::new(),
#[cfg(feature = "std")]
tracker: SequenceTracker::new(),
}
}
pub fn register_chord(&mut self, chord: KeyChord, action: Action) {
self.registry.register_chord(chord, action);
}
pub fn register_sequence(&mut self, sequence: KeySequence, action: Action) {
self.registry.register_sequence(sequence, action);
}
pub fn process(&mut self, chord: KeyChord) -> PipelineResponse<Action> {
#[cfg(feature = "std")]
{
if self.tracker.is_expired() {
self.tracker.reset();
}
}
#[cfg(feature = "std")]
if !self.tracker.is_empty() {
self.tracker.add(chord);
let current = self.tracker.current();
if let Some(action) = self.registry.get_sequence(current) {
self.tracker.reset();
return PipelineResponse::Execute(action.clone());
}
let matching = self.registry.find_sequences_starting_with(current);
if matching.is_empty() {
self.tracker.reset();
PipelineResponse::Cancel
} else {
let hints: alloc::vec::Vec<InputHint<Action>> = matching
.into_iter()
.filter_map(|seq| {
if seq.len() > current.len() {
let next_chord = seq.chords()[current.len()];
self.registry.get_sequence(seq)
.map(|action| InputHint {
chord: next_chord,
action: action.clone(),
})
} else {
None
}
})
.collect();
PipelineResponse::Wait(hints)
}
} else {
if let Some(action) = self.registry.get_chord(&chord) {
PipelineResponse::Execute(action.clone())
} else {
let one_chord_seq = KeySequence::from_chords([chord]);
let matching = self.registry.find_sequences_starting_with(&one_chord_seq);
if !matching.is_empty() {
#[cfg(feature = "std")]
{
self.tracker.add(chord);
let hints: alloc::vec::Vec<InputHint<Action>> = matching
.into_iter()
.filter_map(|seq| {
if seq.len() > 1 {
let next_chord = seq.chords()[1];
self.registry.get_sequence(seq)
.map(|action| InputHint {
chord: next_chord,
action: action.clone(),
})
} else {
None
}
})
.collect();
PipelineResponse::Wait(hints)
}
#[cfg(not(feature = "std"))]
{
PipelineResponse::Cancel
}
} else {
PipelineResponse::Type(chord)
}
}
}
}
}
impl<Action: Clone> Default for KeyPipeline<Action> {
fn default() -> Self {
Self::new()
}
}
```
## Step 7: Module Routing
### `src/input_pipeline/mod.rs`
```rust
// path_from_the_root: src/input_pipeline/mod.rs
pub mod key;
pub mod chord;
pub mod sequence;
pub mod key_map;
pub mod key_registry;
pub mod sequence_tracker;
pub mod pipeline;
pub mod response;
pub use key::{KeyCode, KeyModifiers};
pub use chord::KeyChord;
pub use sequence::KeySequence;
pub use key_map::KeyMapEntry;
pub use key_registry::KeyRegistry;
pub use sequence_tracker::SequenceTracker;
pub use pipeline::KeyPipeline;
pub use response::{PipelineResponse, InputHint};
```
## Step 8: Update lib.rs
```rust
// path_from_the_root: src/lib.rs
#![no_std]
extern crate alloc;
pub mod input_pipeline;
pub mod prelude {
pub use crate::input_pipeline::*;
}
```
## Step 9: Update Cargo.toml
```toml
[package]
name = "tui_orchestrator"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[features]
default = ["std"]
std = []
alloc = []
[dependencies]
[dev-dependencies]
```
## Step 10: Tests
Create `tests/input_pipeline/` directory with tests for each module:
```
tests/input_pipeline/
├── key_tests.rs
├── chord_tests.rs
├── sequence_tests.rs
├── registry_tests.rs
└── pipeline_tests.rs
```
Example test file:
```rust
// path_from_the_root: tests/input_pipeline/chord_tests.rs
use tui_orchestrator::input_pipeline::{KeyChord, KeyCode, KeyModifiers};
#[test]
fn test_chord_creation() {
let chord = KeyChord::new(
KeyCode::Char('a'),
KeyModifiers::new().with_control(),
);
assert_eq!(chord.code, KeyCode::Char('a'));
assert!(chord.modifiers.control);
}
#[test]
fn test_chord_display() {
let chord = KeyChord::new(
KeyCode::Char('a'),
KeyModifiers::new().with_control().with_shift(),
);
let display = chord.display_string();
assert!(display.contains("Ctrl+"));
assert!(display.contains("Shift+"));
assert!(display.contains('a'));
}
```
## Integration with komp_ac_client
After migration, update `komp_ac_client/Cargo.toml`:
```toml
[dependencies]
tui_orchestrator = { path = "..", features = ["std"] }
```
Then in `komp_ac_client/src/input_pipeline/mod.rs`, replace with:
```rust
// path_from_the_root: komp_ac_client/src/input_pipeline/mod.rs
pub use tui_orchestrator::input_pipeline::*;
// Add crossterm conversion trait
use crossterm::event::{KeyCode, KeyModifiers as CrosstermModifiers};
impl From<&crossterm::event::KeyEvent> for KeyChord {
fn from(event: &crossterm::event::KeyEvent) -> Self {
let code = match event.code {
KeyCode::Char(c) => KeyCode::Char(c),
KeyCode::Enter => KeyCode::Enter,
KeyCode::Tab => KeyCode::Tab,
KeyCode::Esc => KeyCode::Esc,
KeyCode::Backspace => KeyCode::Backspace,
KeyCode::Delete => KeyCode::Delete,
KeyCode::Home => KeyCode::Home,
KeyCode::End => KeyCode::End,
KeyCode::PageUp => KeyCode::PageUp,
KeyCode::PageDown => KeyCode::PageDown,
KeyCode::Up => KeyCode::Up,
KeyCode::Down => KeyCode::Down,
KeyCode::Left => KeyCode::Left,
KeyCode::Right => KeyCode::Right,
KeyCode::F(n) => KeyCode::F(n),
KeyCode::Null => KeyCode::Null,
};
let modifiers = KeyModifiers {
control: event.modifiers.contains(CrosstermModifiers::CONTROL),
alt: event.modifiers.contains(CrosstermModifiers::ALT),
shift: event.modifiers.contains(CrosstermModifiers::SHIFT),
};
KeyChord::new(code, modifiers)
}
}
```
Delete the migrated files from `komp_ac_client/src/input_pipeline/`.
## Benefits
1. **no_std compatible** - Works on embedded systems and WASM
2. **Backend agnostic** - No crossterm/ratatui dependency
3. **General purpose** - Any TUI can use this API
4. **Type safe** - Strong typing for key codes and modifiers
5. **Testable** - Pure functions, easy to unit test
6. **Flexible** - Applications define their own Action types
## Migration Checklist
- [ ] Create `src/input_pipeline/key.rs`
- [ ] Create `src/input_pipeline/chord.rs`
- [ ] Create `src/input_pipeline/sequence.rs`
- [ ] Create `src/input_pipeline/key_map.rs`
- [ ] Create `src/input_pipeline/key_registry.rs`
- [ ] Create `src/input_pipeline/sequence_tracker.rs`
- [ ] Create `src/input_pipeline/pipeline.rs`
- [ ] Create `src/input_pipeline/response.rs`
- [ ] Create `src/input_pipeline/mod.rs`
- [ ] Update `src/lib.rs`
- [ ] Update `src/key/mod.rs` (remove old placeholders or convert)
- [ ] Create tests in `tests/input_pipeline/`
- [ ] Run tests: `cargo test --no-default-features`
- [ ] Run tests with std: `cargo test`
- [ ] Update komp_ac_client to use library
- [ ] Delete migrated files from komp_ac_client
- [ ] Run komp_ac_client tests

582
INTEGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,582 @@
# TUI Orchestrator Integration Guide
This guide shows how to use the TUI Orchestrator framework to build terminal user interfaces with minimal boilerplate.
---
## Quick Start: Your First TUI App
### Step 1: Define Your Component
A component represents a page or UI section with focusable elements:
```rust
extern crate alloc;
use tui_orchestrator::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum LoginFocus {
Username,
Password,
LoginButton,
CancelButton,
}
#[derive(Debug, Clone)]
enum LoginEvent {
AttemptLogin { username: String, password: String },
Cancel,
}
struct LoginPage {
username: alloc::string::String,
password: alloc::string::String,
}
impl Component for LoginPage {
type Focus = LoginFocus;
type Action = ComponentAction;
type Event = LoginEvent;
fn targets(&self) -> &[Self::Focus] {
&[
LoginFocus::Username,
LoginFocus::Password,
LoginFocus::LoginButton,
LoginFocus::CancelButton,
]
}
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
match (focus, action) {
(LoginFocus::LoginButton, ComponentAction::Select) => {
Ok(Some(LoginEvent::AttemptLogin {
username: self.username.clone(),
password: self.password.clone(),
}))
}
(LoginFocus::CancelButton, ComponentAction::Select) => {
Ok(Some(LoginEvent::Cancel))
}
_ => Ok(None),
}
}
fn handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>> {
match focus {
LoginFocus::Username => {
self.username.push(ch);
Ok(None)
}
LoginFocus::Password => {
self.password.push(ch);
Ok(None)
}
_ => Ok(None),
}
}
fn on_enter(&mut self) -> Result<()> {
self.username.clear();
self.password.clear();
Ok(())
}
}
```
### Step 2: Register and Run
```rust
use tui_orchestrator::prelude::*;
fn main() -> Result<()> {
let mut orch = Orchestrator::builder()
.with_page("login", LoginPage::new())
.with_default_bindings()
.build()?;
orch.navigate_to("login")?;
orch.run(&mut MyInputSource)?;
}
```
**That's it.** The library handles:
- Input processing
- Focus management (Tab/Shift+Tab navigation)
- Button activation (Enter key)
- Text input typing
- Page lifecycle (on_enter/on_exit)
---
## Component Trait Deep Dive
### Associated Types
```rust
pub trait Component {
type Focus: FocusId + Clone; // What can be focused in this component
type Action: Action + Clone; // What actions this component handles
type Event: Clone + Debug; // Events this component emits
}
```
### Required Methods
**`targets(&self) -> &[Self::Focus]`**
Returns all focusable elements. Order determines navigation sequence (next/prev).
```rust
fn targets(&self) -> &[Self::Focus] {
&[
Focus::Username,
Focus::Password,
Focus::LoginButton,
Focus::CancelButton,
]
}
```
**`handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>`**
Called when a bound action occurs. Returns optional event for application to handle.
```rust
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
match (focus, action) {
(Focus::Submit, ComponentAction::Select) => Ok(Some(Event::Submit)),
_ => Ok(None),
}
}
```
### Optional Methods
All have default implementations—only override what you need.
**`on_enter(&mut self) -> Result<()>`**
Called when component becomes active (page is navigated to). Good for resetting state.
**`on_exit(&mut self) -> Result<()>`**
Called when component becomes inactive (page is navigated away). Good for cleanup.
**`on_focus(&mut self, focus: &Self::Focus) -> Result<()>`**
Called when a specific focus target gains focus.
**`on_blur(&mut self, focus: &Self::Focus) -> Result<()>`**
Called when a specific focus target loses focus.
**`handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>>`**
Called when character is typed (not a bound action). Only called for text-friendly focus targets.
**`can_navigate_forward(&self, focus: &Self::Focus) -> bool`**
Return false to prevent Next action from moving focus (useful for boundary detection).
**`can_navigate_backward(&self, focus: &Self::Focus) -> bool`**
Return false to prevent Prev action from moving focus.
---
## Standard Component Actions
The library provides these actions automatically bound to keys:
| Action | Default Key | Description |
|--------|--------------|-------------|
| `ComponentAction::Next` | Tab | Move focus to next target |
| `ComponentAction::Prev` | Shift+Tab | Move focus to previous target |
| `ComponentAction::First` | Home | Move focus to first target |
| `ComponentAction::Last` | End | Move focus to last target |
| `ComponentAction::Select` | Enter | Activate current focus target |
| `ComponentAction::Cancel` | Esc | Cancel or close |
| `ComponentAction::TypeChar(c)` | Any character | Type character |
| `ComponentAction::Backspace` | Backspace | Delete character before cursor |
| `ComponentAction::Delete` | Delete | Delete character at cursor |
| `ComponentAction::Custom(n)` | None | User-defined action |
### Customizing Bindings
```rust
let mut orch = Orchestrator::new();
// Override default bindings
orch.bindings().bind(Key::ctrl('s'), ComponentAction::Custom(0)); // Custom save action
orch.bindings().bind(Key::char(':'), ComponentAction::Custom(1)); // Enter command mode
```
---
## Orchestrator API
### Basic Setup
```rust
let mut orch = Orchestrator::new();
// Register pages
orch.register_page("login", LoginPage::new())?;
orch.register_page("home", HomePage::new())?;
// Navigation
orch.navigate_to("login")?;
```
### Processing Input
```rust
loop {
let key = read_key()?;
let events = orch.process_frame(key)?;
for event in events {
match event {
LoginEvent::AttemptLogin => do_login(username, password),
LoginEvent::Cancel => return Ok(()),
}
}
render(&orch)?;
}
```
### Reading State
```rust
// Get current page
if let Some(page) = orch.current_page() {
// Access page...
}
// Get current focus
if let Some(focus) = orch.focus().current() {
// Check what's focused...
}
// Create query snapshot
let query = orch.focus().query();
if query.is_focused(&LoginFocus::Username) {
// Username field is focused...
}
```
---
## Multiple Pages Example
```rust
#[derive(Debug, Clone)]
enum MyPage {
Login(LoginPage),
Home(HomePage),
Settings(SettingsPage),
}
fn main() -> Result<()> {
let mut orch = Orchestrator::new();
orch.register_page("login", LoginPage::new())?;
orch.register_page("home", HomePage::new())?;
orch.register_page("settings", SettingsPage::new())?;
orch.navigate_to("login")?;
orch.run()?;
}
```
Navigation with history:
```rust
orch.navigate_to("home")?;
orch.navigate_to("settings")?;
orch.back()? // Return to home
```
---
## Extension: Custom Mode Resolution
For apps with complex mode systems (like komp_ac):
```rust
pub struct CustomModeResolver {
state: AppState,
}
impl ModeResolver for CustomModeResolver {
fn resolve(&self, focus: &dyn Any) -> alloc::vec::Vec<ModeName> {
match focus.downcast_ref::<FocusTarget>() {
Some(FocusTarget::CanvasField(_)) => {
// Dynamic mode based on editor state
vec![self.state.editor_mode(), ModeName::Common, ModeName::Global]
}
_ => vec![ModeName::General, ModeName::Global],
}
}
}
let mut orch = Orchestrator::new();
orch.set_mode_resolver(CustomModeResolver::new(state));
```
---
## Extension: Custom Overlays
For apps with complex overlay types (command palette, dialogs):
```rust
pub struct CustomOverlayManager {
command_palette: CommandPalette,
dialogs: Vec<Dialog>,
}
impl OverlayManager for CustomOverlayManager {
fn is_active(&self) -> bool {
self.command_palette.is_active() || !self.dialogs.is_empty()
}
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> {
if let Some(result) = self.command_palette.handle_input(key) {
return Some(result);
}
// Handle dialogs...
None
}
}
let mut orch = Orchestrator::new();
orch.set_overlay_manager(CustomOverlayManager::new());
```
---
## Integration with External Libraries
### Reading Input from crossterm
```rust
use crossterm::event;
use tui_orchestrator::input::Key;
impl InputSource for CrosstermInput {
fn read_key(&mut self) -> Result<Key> {
match event::read()? {
event::Event::Key(key_event) => {
let code = match key_event.code {
event::KeyCode::Char(c) => KeyCode::Char(c),
event::KeyCode::Enter => KeyCode::Enter,
event::KeyCode::Tab => KeyCode::Tab,
event::KeyCode::Esc => KeyCode::Esc,
// ... map all codes ...
};
let modifiers = KeyModifiers {
control: key_event.modifiers.contains(event::KeyModifiers::CONTROL),
alt: key_event.modifiers.contains(event::KeyModifiers::ALT),
shift: key_event.modifiers.contains(event::KeyModifiers::SHIFT),
};
Ok(Key::new(code, modifiers))
}
_ => Err(Error::NotAKeyEvent),
}
}
}
```
### Using with ratatui for Rendering
The library is backend-agnostic—you can render with any framework:
```rust
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use tui_orchestrator::prelude::*;
struct MyApp {
orch: Orchestrator<...>,
terminal: Terminal<CrosstermBackend<std::io::Stdout>>,
}
impl MyApp {
fn render(&mut self) -> Result<()> {
self.terminal.draw(|f| {
let focus = self.orch.focus().query();
let page = self.orch.current_page().unwrap();
// Render page with focus context
page.render(f, &focus);
})?;
}
fn run(&mut self) -> Result<()> {
loop {
let key = self.orch.read_key()?;
let events = self.orch.process_frame(key)?;
for event in events {
self.handle_event(event)?;
}
self.render()?;
}
}
}
```
---
## Testing Components
### Unit Tests
Test component logic in isolation:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_login_button_action() {
let mut page = LoginPage::new();
let focus = LoginFocus::LoginButton;
let action = ComponentAction::Select;
let event = page.handle(&focus, action).unwrap();
assert!(matches!(event, Some(LoginEvent::AttemptLogin { .. })));
}
}
```
### Integration Tests
Test with orchestrator:
```rust
#[test]
fn test_full_login_flow() {
let mut orch = Orchestrator::new();
orch.register_page("login", LoginPage::new()).unwrap();
// Simulate tab navigation
let _ = orch.process_frame(Key::tab()).unwrap();
assert_eq!(orch.focus().current(), Some(&LoginFocus::Password));
// Simulate typing
let _ = orch.process_frame(Key::char('p')).unwrap();
let _ = orch.process_frame(Key::char('a')).unwrap();
let _ = orch.process_frame(Key::char('s')).unwrap();
let _ = orch.process_frame(Key::char('s')).unwrap();
// Simulate enter
let events = orch.process_frame(Key::enter()).unwrap();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], LoginEvent::AttemptLogin { .. }));
}
```
---
## Migration from Existing Code
### Migrating from Manual Wiring
**Before:**
```rust
// Manual setup
let mut focus = FocusManager::new();
let mut bindings = Bindings::new();
let mut router = Router::new();
let mut page = LoginPage::new();
focus.set_targets(page.targets());
bindings.bind(Key::tab(), MyAction::Next);
// Manual loop
loop {
let key = read_key()?;
if let Some(action) = bindings.handle(key) {
match action {
MyAction::Next => focus.next(),
MyAction::Select => {
let focused = focus.current()?;
page.handle_button(focused)?;
}
}
}
}
```
**After:**
```rust
// Framework setup
let mut orch = Orchestrator::builder()
.with_page("login", LoginPage::new())
.build()?;
orch.run()?;
```
### Keeping Custom Behavior
If your existing code has custom behavior (like komp_ac's mode resolution), use extension points:
```rust
let mut orch = Orchestrator::new()
.with_mode_resolver(CustomModeResolver::new(state))
.with_overlay_manager(CustomOverlayManager::new())
.with_event_handler(CustomEventHandler::new(router));
```
---
## Best Practices
### 1. Keep Components Focused
Components should handle their own logic only. Don't directly manipulate other components.
### 2. Use Events for Communication
Components should emit events, not directly call methods on other components.
### 3. Respect Optional Methods
Only override lifecycle hooks when you need them. Default implementations are fine for most cases.
### 4. Test Component Isolation
Test components without orchestrator to ensure logic is correct.
### 5. Leverage Default Bindings
Use `with_default_bindings()` unless you have specific keybinding requirements.
### 6. Use Extension Points Wisely
Only implement custom resolvers/handlers when default behavior doesn't meet your needs.
---
## Summary
The TUI Orchestrator framework provides:
1. **Zero boilerplate** - Define components, library handles the rest
2. **Sensible defaults** - Works without configuration
3. **Full extension** - Customize via traits when needed
4. **Backend-agnostic** - Works with any rendering library
5. **no_std compatible** - Runs on embedded systems and WASM
Your job: define components with buttons and logic. Our job: make it just work.

540
PLAN.md Normal file
View File

@@ -0,0 +1,540 @@
# TUI Orchestrator - Complete TUI Framework
## Overview
`tui_orchestrator` is a **ready-to-use TUI framework** that provides a complete runtime for building terminal user interfaces. Users define their pages, buttons, and logic—library handles everything else: input routing, focus management, page navigation, lifecycle hooks, and event orchestration.
### Key Philosophy
**"Register pages with buttons and logic—it just works."**
The library is a **complete application framework** where:
- User defines components (pages with focusable elements)
- Library orchestrates all runtime concerns
- Everything is optional—define what you need
- Fully extendable for complex apps like komp_ac
### Zero Boilerplate
Users write:
```rust
impl Component for LoginPage {
fn targets(&self) -> &[Focus];
fn handle(&mut self, focus: &Focus, action: Action) -> Result<Option<Event>> {
// What happens when button pressed
}
}
```
Library handles:
- Input processing
- Focus management
- Page navigation
- Lifecycle hooks
- Event routing
- Default keybindings
### Extension Model
komp_ac can use the library for 90% of functionality while extending:
- Mode resolution (dynamic Canvas-style modes)
- Overlay management (command palette, find file, search)
- Event routing (global vs page vs canvas actions)
- Custom behaviors (boundary detection, navigation rules)
**Defaults work, override what's different.**
---
## Architecture
```
┌─────────────────────────────────────────────────┐
│ User Code (What You Define) │
│ │
│ Component trait │
│ - Page structs/enums │
│ - Focus targets (buttons, fields) │
│ - Button logic (what happens on press) │
│ - Lifecycle hooks (optional) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Orchestrator (Library Runtime) │
│ │
│ - ComponentRegistry │
│ - FocusManager │
│ - Bindings (default + custom) │
│ - Router + history │
│ - ModeStack │
│ - OverlayStack │
│ - EventBus │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Extension Points (For komp_ac) │
│ │
│ - ModeResolver (dynamic mode resolution) │
│ - OverlayManager (custom overlay types) │
│ - EventHandler (custom event routing) │
│ - FocusNavigation (boundary detection) │
└─────────────────────────────────────────────────┘
```
---
## Implementation Phases
### Phase 1: Core Foundation ✅ COMPLETE
**Completed:**
- `src/input/` - Key types, bindings, sequence handling
- `src/focus/` - Focus manager, queries, traits
**What this provides:**
- Backend-agnostic key representation
- Key-to-action mappings
- Focus tracking with navigation
- Generic focus IDs (user-defined enums, `usize`, `String`, etc.)
---
### Phase 2: Component System (CURRENT)
**Goal:** Unified abstraction for pages/components.
**Files to create:**
- `src/component/mod.rs` - Component trait
- `src/component/action.rs` - Standard component actions
- `src/component/error.rs` - Component-specific errors
**Component Trait:**
```rust
pub trait Component {
type Focus: FocusId + Clone;
type Action: Action + Clone;
type Event: Clone + core::fmt::Debug;
fn targets(&self) -> &[Self::Focus];
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>;
fn on_enter(&mut self) -> Result<()> { Ok(()) }
fn on_exit(&mut self) -> Result<()> { Ok(()) }
fn on_focus(&mut self, focus: &Self::Focus) -> Result<()> { Ok(()) }
fn on_blur(&mut self, focus: &Self::Focus) -> Result<()> { Ok(()) }
fn handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>> { Ok(None) }
fn can_navigate_forward(&self, focus: &Self::Focus) -> bool { true }
fn can_navigate_backward(&self, focus: &Self::Focus) -> bool { true }
}
```
**Standard Component Actions:**
```rust
pub enum ComponentAction {
Next, // Tab by default
Prev, // Shift+Tab by default
First,
Last,
Select, // Enter by default
Cancel, // Esc by default
TypeChar(char),
Backspace,
Delete,
Custom(usize), // User extension
}
```
---
### Phase 3: Router & Lifecycle
**Goal:** Page navigation with automatic lifecycle hooks.
**Files to create:**
- `src/router/mod.rs` - Router trait and implementation
- `src/router/history.rs` - Navigation history
**Router API:**
```rust
pub struct Router<C: Component> {
pages: alloc::collections::HashMap<String, C>,
current: Option<String>,
history: alloc::vec::Vec<String>,
}
impl<C: Component> Router<C> {
pub fn new() -> Self;
pub fn navigate(&mut self, id: &str) -> Result<()>;
pub fn back(&mut self) -> Result<Option<()>>;
pub fn forward(&mut self) -> Result<Option<()>>;
pub fn current(&self) -> Option<&C>;
}
```
**Automatic behavior:**
- `navigate()` calls `old_page.on_exit()` → swaps page → calls `new_page.on_enter()`
- `back()`/`forward()` manage history stack
- Focus targets auto-updated from `targets()`
---
### Phase 4: Orchestrator (The Core Runtime)
**Goal:** Wire everything together into complete TUI runtime.
**Files to create:**
- `src/orchestrator/mod.rs` - Orchestrator struct
- `src/orchestrator/modes.rs` - Mode stack and resolution
- `src/orchestrator/overlays.rs` - Overlay stack
- `src/orchestrator/events.rs` - Event bus
- `src/orchestrator/bindings.rs` - Default + custom bindings
**Orchestrator API:**
```rust
pub struct Orchestrator<C: Component> {
registry: ComponentRegistry<C>,
focus: FocusManager<C::Focus>,
bindings: Bindings<ComponentAction>,
router: Router<C>,
modes: ModeStack<ModeName>,
overlays: OverlayStack<C::Event>,
events: EventBus<C::Event>,
}
impl<C: Component> Orchestrator<C> {
pub fn new() -> Self;
pub fn register_page(&mut self, id: &str, page: C) -> Result<()>;
pub fn navigate_to(&mut self, id: &str) -> Result<()>;
pub fn process_frame(&mut self, key: Key) -> Result<alloc::vec::Vec<C::Event>>;
pub fn run<I: InputSource>(&mut self, input: I) -> Result<()>;
// Extension points
pub fn set_mode_resolver<R: ModeResolver + 'static>(&mut self, resolver: R);
pub fn set_overlay_manager<O: OverlayManager + 'static>(&mut self, manager: O);
pub fn set_event_handler<H: EventHandler<C::Event> + 'static>(&mut self, handler: H);
}
```
**Process flow:**
1. Check overlay active → route to overlay
2. Get current mode + focus
3. Lookup binding → get action
4. Get current component
5. Call `component.handle(action, focus)`
6. Collect events returned
7. Handle internal events (focus changes, page nav)
8. Return external events to user
---
### Phase 5: Extension Traits
**Goal:** Provide extension points for komp_ac's custom behavior.
**Files to create:**
- `src/extension/mod.rs` - Extension traits
- `src/extension/mode.rs` - Mode resolver
- `src/extension/overlay.rs` - Overlay manager
- `src/extension/event.rs` - Event handler
**ModeResolver (for dynamic mode resolution):**
```rust
pub trait ModeResolver {
fn resolve(&self, focus: &dyn core::any::Any) -> alloc::vec::Vec<ModeName>;
}
pub struct DefaultModeResolver;
impl ModeResolver for DefaultModeResolver {
fn resolve(&self, _focus: &dyn core::any::Any) -> alloc::vec::Vec<ModeName> {
alloc::vec![ModeName::General]
}
}
```
**OverlayManager (for custom overlays):**
```rust
pub trait OverlayManager {
fn is_active(&self) -> bool;
fn handle_input(&mut self, key: Key) -> Option<OverlayResult>;
}
pub enum OverlayResult {
Dismissed,
Selected(OverlayData),
Continue,
}
```
**EventHandler (for custom event routing):**
```rust
pub trait EventHandler<E> {
fn handle(&mut self, event: E) -> Result<HandleResult>;
}
pub enum HandleResult {
Consumed,
Forward,
Navigate(String),
}
```
---
### Phase 6: Builder & Defaults
**Goal:** Easy setup with sensible defaults.
**Files to create:**
- `src/builder/mod.rs` - Builder pattern
- `src/defaults/bindings.rs` - Preset keybindings
**Builder API:**
```rust
impl<C: Component> Orchestrator<C> {
pub fn builder() -> Builder<C> { Builder::new() }
}
pub struct Builder<C: Component> {
orchestrator: Orchestrator<C>,
}
impl<C: Component> Builder<C> {
pub fn with_page(mut self, id: &str, page: C) -> Result<Self>;
pub fn with_default_bindings(mut self) -> Self;
pub fn with_mode_resolver<R: ModeResolver + 'static>(mut self, resolver: R) -> Self;
pub fn with_overlay_manager<O: OverlayManager + 'static>(mut self, manager: O) -> Self;
pub fn build(self) -> Result<Orchestrator<C>>;
}
```
**Default bindings:**
```rust
pub fn default_bindings<A: Action>() -> 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::ctrl('c'), ComponentAction::Custom(0)); // Quit
}
```
---
### Phase 7: Integration (Optional)
**Goal:** Seamless integration with komp_ac.
**Files to create:**
- `src/integration/mod.rs` - Integration helpers
- `src/integration/komp_ac.rs` - komp_ac-specific adapters
**Adapter pattern:**
```rust
impl Component for komp_ac::LoginPage {
type Focus = komp_ac::FocusTarget;
type Action = komp_ac::ResolvedAction;
type Event = komp_ac::AppEvent;
fn targets(&self) -> &[Self::Focus] { ... }
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> { ... }
}
```
---
## File Structure
```
src/
├── lib.rs # Routing only, re-exports
├── prelude.rs # Common imports
├── input/ # Phase 1 ✅
│ ├── mod.rs
│ ├── key.rs # KeyCode, KeyModifiers
│ ├── bindings.rs # Bindings
│ ├── handler.rs # InputHandler
│ ├── result.rs # MatchResult
│ └── action.rs # Action trait
├── focus/ # Phase 1 ✅
│ ├── mod.rs
│ ├── id.rs # FocusId trait
│ ├── manager.rs # FocusManager
│ ├── query.rs # FocusQuery
│ ├── error.rs # FocusError
│ └── traits.rs # Focusable
├── component/ # Phase 2
│ ├── mod.rs
│ ├── trait.rs # Component trait
│ ├── action.rs # ComponentAction
│ └── error.rs # ComponentError
├── router/ # Phase 3
│ ├── mod.rs
│ ├── router.rs # Router
│ └── history.rs # HistoryStack
├── orchestrator/ # Phase 4
│ ├── mod.rs
│ ├── core.rs # Orchestrator
│ ├── modes.rs # ModeStack, ModeResolver
│ ├── overlays.rs # OverlayStack
│ ├── bindings.rs # Component bindings
│ └── events.rs # EventBus
├── extension/ # Phase 5
│ ├── mod.rs
│ ├── mode.rs # ModeResolver trait
│ ├── overlay.rs # OverlayManager trait
│ └── event.rs # EventHandler trait
├── builder/ # Phase 6
│ ├── mod.rs
│ ├── builder.rs # Builder pattern
│ └── defaults.rs # Default bindings
└── integration/ # Phase 7
├── mod.rs
└── komp_ac.rs # komp_ac adapters
tests/ # Mirror src/ structure
├── input/
├── focus/
├── component/
├── router/
├── orchestrator/
└── integration/
```
---
## Design Principles
### From AGENTS.md
- **Feature-based tree structure**—group by domain
- **Each feature is self-contained**—handler, logic, types, tests
- **Functional programming style**—pure functions, stateless where possible
- **Use structs, traits, enums, impl, match** over if
- **No Arc/Mutex/RefCell**
- **Result<T, E> everywhere**
- **mod.rs is for routing only**
- **No comments unless necessary**
### Additional for Framework
- **Batteries included**—not just building blocks
- **Sensible defaults**—zero configuration works
- **Optional everything**—define only what you need
- **Extension points**—override defaults when needed
- **no_std compatible**—works on embedded, WASM
- **Backend-agnostic**—no crossterm/ratatui dependencies
- **User-focused**—"register page" not "register_chord"
---
## Dependencies
### Core (no_std)
- `alloc` - For dynamic collections (Vec, HashMap)
### Optional Features
- `std` - Enable std library support
- `sequences` - Enable multi-key sequences
---
## User Experience
### Before (Building Blocks)
Users must manually wire everything:
- Create focus manager
- Create bindings
- Create router
- Set up components
- Write main loop
- Handle lifecycle manually
**Result:** Lots of boilerplate, easy to get wrong.
### After (Framework)
Users define components and run:
```rust
#[derive(Debug, Clone)]
struct LoginPage;
impl Component for LoginPage {
fn targets(&self) -> &[Focus] { ... }
fn handle(&mut self, focus: &Focus, action: Action) -> Result<Option<Event>> { ... }
}
fn main() -> Result<()> {
let mut orch = Orchestrator::new();
orch.register_page("login", LoginPage::new())?;
orch.run()?;
}
```
**Result:** Zero boilerplate, everything just works.
---
## Migration for komp_ac
### Before Integration
komp_ac has:
- Custom orchestrator
- Custom mode resolution (Canvas-style)
- Custom overlays (command bar, find file, search)
- Custom action routing (page vs canvas vs global)
### After Integration
komp_ac:
1. Implements `Component` trait for all pages
2. Uses library's `Orchestrator` as runtime
3. Extends with custom `ModeResolver`
4. Extends with custom `OverlayManager`
5. Extends with custom `EventHandler`
**Result:**
- Library handles 90% of runtime
- komp_ac keeps all custom behavior
- No code duplication
- Cleaner, more maintainable codebase
---
## Feature Checklist
- [x] Phase 1: Input handling (keys, bindings, focus)
- [ ] Phase 2: Component trait and actions
- [ ] Phase 3: Router with lifecycle
- [ ] Phase 4: Orchestrator runtime
- [ ] Phase 5: Extension traits
- [ ] Phase 6: Builder and defaults
- [ ] Phase 7: Integration with komp_ac
- [ ] Documentation updates
- [ ] Example applications
- [ ] Full test coverage

431
PROGRESS.md Normal file
View File

@@ -0,0 +1,431 @@
# TUI Orchestrator - Progress Summary
## Current Status
### Completed Features ✅
#### Phase 1: Core Foundation
- **Input handling** (`src/input/`)
- Key types (KeyCode, KeyModifiers, Key)
- Bindings (key → action mappings)
- SequenceHandler (multi-key sequences, feature-gated)
- MatchResult (action/pending/no-match)
- **Focus management** (`src/focus/`)
- FocusId trait (generic focus identifiers)
- FocusManager (focus tracking, navigation, overlays)
- FocusQuery (read-only focus state for rendering)
- Focusable trait (components declare focusable elements)
- FocusError (error types)
#### Documentation ✅
- **PLAN.md** - Complete implementation plan for framework approach
- **REDESIGN.md** - Deep dive into framework architecture
- **INTEGRATION_GUIDE.md** - User guide for building TUI apps
- **README.md** - Project overview and quick start
#### Tests ✅
- Input tests: 12 tests passing
- Focus tests: 18 tests passing
- Total: 30 tests covering all core functionality
---
## Next Steps
### Phase 2: Component System
The foundation for the entire framework. This is the highest priority.
**Files to create:**
- `src/component/mod.rs` - Module routing
- `src/component/trait.rs` - Component trait definition
- `src/component/action.rs` - Standard component actions
- `src/component/error.rs` - Component-specific errors
**Component trait design:**
```rust
pub trait Component {
type Focus: FocusId + Clone;
type Action: Action + Clone;
type Event: Clone + core::fmt::Debug;
// REQUIRED
fn targets(&self) -> &[Self::Focus];
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>;
// OPTIONAL (all with defaults)
fn on_enter(&mut self) -> Result<()>;
fn on_exit(&mut self) -> Result<()>;
fn on_focus(&mut self, focus: &Self::Focus) -> Result<()>;
fn on_blur(&mut self, focus: &Self::Focus) -> Result<()>;
fn handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>>;
fn can_navigate_forward(&self, focus: &Self::Focus) -> bool;
fn can_navigate_backward(&self, focus: &Self::Focus) -> bool;
}
```
**ComponentAction enum:**
```rust
pub enum ComponentAction {
Next, // Tab → move focus forward
Prev, // Shift+Tab → move focus backward
First, // Home → move to first
Last, // End → move to last
Select, // Enter → activate current focus
Cancel, // Esc → cancel/close
TypeChar(char), // Character → type text
Backspace, // Backspace → delete before cursor
Delete, // Delete → delete at cursor
Custom(usize), // User-defined action
}
```
**Priority:** HIGHEST - This enables all other functionality
---
### Phase 3: Router & Lifecycle
Page navigation with automatic lifecycle hook invocation.
**Files to create:**
- `src/router/mod.rs` - Module routing
- `src/router/router.rs` - Router implementation
- `src/router/history.rs` - Navigation history
**Router API:**
```rust
pub struct Router<C: Component> {
pages: alloc::collections::HashMap<String, C>,
current: Option<String>,
history: alloc::vec::Vec<String>,
future: alloc::vec::Vec<String>, // For forward navigation
}
impl<C: Component> Router<C> {
pub fn new() -> Self;
pub fn navigate(&mut self, id: &str) -> Result<()>;
pub fn back(&mut self) -> Result<Option<()>>;
pub fn forward(&mut self) -> Result<Option<()>>;
pub fn current(&self) -> Option<&C>;
}
```
**Automatic behavior:**
- `navigate()` calls `old_page.on_exit()` → swaps → calls `new_page.on_enter()`
- `back()`/`forward()` manage history stack
**Priority:** HIGH - Essential for multi-page apps
---
### Phase 4: Orchestrator Core
The complete runtime that wires everything together.
**Files to create:**
- `src/orchestrator/mod.rs` - Module routing
- `src/orchestrator/core.rs` - Main Orchestrator struct
- `src/orchestrator/bindings.rs` - Default + custom bindings
- `src/orchestrator/modes.rs` - Mode stack and resolution
- `src/orchestrator/overlays.rs` - Overlay stack
- `src/orchestrator/events.rs` - Event bus
**Orchestrator API:**
```rust
pub struct Orchestrator<C: Component> {
registry: ComponentRegistry<C>,
focus: FocusManager<C::Focus>,
bindings: Bindings<ComponentAction>,
router: Router<C>,
modes: ModeStack<ModeName>,
overlays: OverlayStack<C::Event>,
events: EventBus<C::Event>,
}
impl<C: Component> Orchestrator<C> {
pub fn new() -> Self;
pub fn register_page(&mut self, id: &str, page: C) -> Result<()>;
pub fn navigate_to(&mut self, id: &str) -> Result<()>;
pub fn process_frame(&mut self, key: Key) -> Result<alloc::vec::Vec<C::Event>>;
pub fn run<I: InputSource>(&mut self, input: I) -> Result<()>;
// Extension points
pub fn set_mode_resolver<R: ModeResolver + 'static>(&mut self, resolver: R);
pub fn set_overlay_manager<O: OverlayManager + 'static>(&mut self, manager: O);
pub fn set_event_handler<H: EventHandler<C::Event> + 'static>(&mut self, handler: H);
}
```
**Process flow:**
1. Check overlay active → route to overlay
2. Get current mode + focus
3. Lookup binding → get action
4. Get current component
5. Call `component.handle(action, focus)`
6. Collect events
7. Handle internal events (focus changes, page nav)
8. Return external events
**Priority:** HIGH - This makes the framework "ready to use"
---
### Phase 5: Extension Traits
Extension points for komp_ac and other complex apps.
**Files to create:**
- `src/extension/mod.rs` - Module routing
- `src/extension/mode.rs` - ModeResolver trait
- `src/extension/overlay.rs` - OverlayManager trait
- `src/extension/event.rs` - EventHandler trait
**ModeResolver trait:**
```rust
pub trait ModeResolver {
fn resolve(&self, focus: &dyn core::any::Any) -> alloc::vec::Vec<ModeName>;
}
pub struct DefaultModeResolver;
impl ModeResolver for DefaultModeResolver { ... }
```
**OverlayManager trait:**
```rust
pub trait OverlayManager {
fn is_active(&self) -> bool;
fn handle_input(&mut self, key: Key) -> Option<OverlayResult>;
}
pub enum OverlayResult {
Dismissed,
Selected(OverlayData),
Continue,
}
```
**EventHandler trait:**
```rust
pub trait EventHandler<E> {
fn handle(&mut self, event: E) -> Result<HandleResult>;
}
pub enum HandleResult {
Consumed,
Forward,
Navigate(String),
}
```
**Priority:** MEDIUM - Defaults work for most apps, komp_ac needs these
---
### Phase 6: Builder & Defaults
Easy setup pattern with sensible defaults.
**Files to create:**
- `src/builder/mod.rs` - Module routing
- `src/builder/builder.rs` - Builder pattern
- `src/builder/defaults.rs` - Preset keybindings
**Builder API:**
```rust
pub struct Builder<C: Component> {
orchestrator: Orchestrator<C>,
}
impl<C: Component> Builder<C> {
pub fn new() -> Self;
pub fn with_page(mut self, id: &str, page: C) -> Result<Self>;
pub fn with_default_bindings(mut self) -> Self;
pub fn with_mode_resolver<R: ModeResolver + 'static>(mut self, resolver: R) -> Self;
pub fn with_overlay_manager<O: OverlayManager + 'static>(mut self, manager: O) -> Self;
pub fn build(self) -> Result<Orchestrator<C>>;
}
```
**Default bindings:**
```rust
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::ctrl('c'), ComponentAction::Custom(0)); // Common quit
}
```
**Priority:** MEDIUM - Improves developer experience
---
### Phase 7: Integration with komp_ac
Adapters and integration helpers for seamless komp_ac migration.
**Files to create:**
- `src/integration/mod.rs` - Module routing
- `src/integration/komp_ac.rs` - komp_ac-specific adapters
**Integration pattern:**
```rust
impl Component for komp_ac::LoginPage {
type Focus = komp_ac::FocusTarget;
type Action = komp_ac::ResolvedAction;
type Event = komp_ac::AppEvent;
fn targets(&self) -> &[Self::Focus] { ... }
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> { ... }
}
```
**komp_ac setup:**
```rust
let mut orch = Orchestrator::new()
.with_mode_resolver(CanvasModeResolver::new(app_state))
.with_overlay_manager(KompAcOverlayManager::new())
.with_event_handler(KompAcEventHandler::new(router, focus));
```
**Priority:** LOW - Not needed for general library users, but essential for komp_ac
---
## File Structure After All Phases
```
src/
├── lib.rs # Routing
├── prelude.rs # Common imports
├── input/ # Phase 1 ✅
│ ├── mod.rs
│ ├── key.rs
│ ├── bindings.rs
│ ├── handler.rs
│ ├── result.rs
│ └── action.rs
├── focus/ # Phase 1 ✅
│ ├── mod.rs
│ ├── id.rs
│ ├── manager.rs
│ ├── query.rs
│ ├── error.rs
│ └── traits.rs
├── component/ # Phase 2 (NEXT)
│ ├── mod.rs
│ ├── trait.rs
│ ├── action.rs
│ └── error.rs
├── router/ # Phase 3
│ ├── mod.rs
│ ├── router.rs
│ └── history.rs
├── orchestrator/ # Phase 4
│ ├── mod.rs
│ ├── core.rs
│ ├── bindings.rs
│ ├── modes.rs
│ ├── overlays.rs
│ └── events.rs
├── extension/ # Phase 5
│ ├── mod.rs
│ ├── mode.rs
│ ├── overlay.rs
│ └── event.rs
├── builder/ # Phase 6
│ ├── mod.rs
│ ├── builder.rs
│ └── defaults.rs
└── integration/ # Phase 7
├── mod.rs
└── komp_ac.rs
tests/
├── input/
├── focus/
├── component/
├── router/
├── orchestrator/
└── integration/
```
---
## Testing Strategy
For each phase:
1. **Write tests first** - Define expected behavior
2. **Implement to pass** - Code should make tests pass
3. **Run cargo test** - Verify all pass
4. **Run cargo clippy** - Ensure code quality
5. **Run cargo fmt** - Ensure formatting
**Target:** 100% test coverage for all public APIs
---
## Dependencies Update
### Cargo.toml (after Phase 4)
```toml
[package]
name = "tui_orchestrator"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[features]
default = ["std"]
std = []
alloc = ["hashbrown"]
sequences = ["alloc"]
[dependencies]
hashbrown = { version = "0.15", optional = true }
[dev-dependencies]
```
---
## Next Action
**Implement Phase 2: Component System**
This is the foundation that enables:
- Page/component registration
- Button logic definition
- Lifecycle hooks
- Everything the framework needs
**Tasks:**
1. Create `src/component/mod.rs`
2. Create `src/component/trait.rs` with Component trait
3. Create `src/component/action.rs` with ComponentAction enum
4. Create `src/component/error.rs` with ComponentError enum
5. Write tests in `tests/component/`
6. Update `src/lib.rs` to export component module
7. Update `src/prelude.rs` to include Component types
8. Run `cargo test --all-features`
9. Run `cargo clippy --all-features`
10. Update documentation if needed
Ready to implement?

322
README.md Normal file
View File

@@ -0,0 +1,322 @@
# TUI Orchestrator
A complete, **ready-to-use TUI framework** that handles input routing, focus management, page navigation, and lifecycle hooks—so you can define your pages, buttons, and logic, and it just works.
## Features
- **Zero boilerplate** - Define components, library handles everything else
- **Ready to use** - Register pages and run, no manual wiring needed
- **Sensible defaults** - Works without configuration
- **Fully extendable** - Customize via traits when needed
- **no_std compatible** - Works on embedded systems and WebAssembly
- **Backend-agnostic** - No crossterm/ratatui dependencies
- **Zero unsafe** - Pure Rust, no unsafe code
## Quick Start
### Define Your Component
```rust
extern crate alloc;
use tui_orchestrator::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum LoginFocus {
Username,
Password,
LoginButton,
CancelButton,
}
#[derive(Debug, Clone)]
enum LoginEvent {
AttemptLogin { username: String, password: String },
Cancel,
}
struct LoginPage {
username: alloc::string::String,
password: alloc::string::String,
}
impl Component for LoginPage {
type Focus = LoginFocus;
type Action = ComponentAction;
type Event = LoginEvent;
fn targets(&self) -> &[Self::Focus] {
&[
LoginFocus::Username,
LoginFocus::Password,
LoginFocus::LoginButton,
LoginFocus::CancelButton,
]
}
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
match (focus, action) {
(LoginFocus::LoginButton, ComponentAction::Select) => {
Ok(Some(LoginEvent::AttemptLogin {
username: self.username.clone(),
password: self.password.clone(),
}))
}
(LoginFocus::CancelButton, ComponentAction::Select) => {
Ok(Some(LoginEvent::Cancel))
}
_ => Ok(None),
}
}
fn handle_text(&mut self, focus: &Self::Focus, ch: char) -> Result<Option<Self::Event>> {
match focus {
LoginFocus::Username => {
self.username.push(ch);
Ok(None)
}
LoginFocus::Password => {
self.password.push(ch);
Ok(None)
}
_ => Ok(None),
}
}
}
```
### Register and Run
```rust
use tui_orchestrator::prelude::*;
fn main() -> Result<()> {
let mut orch = Orchestrator::builder()
.with_page("login", LoginPage::new())
.with_default_bindings()
.build()?;
orch.navigate_to("login")?;
orch.run(&mut MyInputSource)?;
}
```
**That's it.** The library handles:
- Input processing (read keys, route to actions)
- Focus management (next/prev navigation)
- Page navigation (on_exit, swap, on_enter)
- Default keybindings (Tab=Next, Enter=Select)
- Event collection and routing
---
## Core Concepts
### Component
The main abstraction in tui_orchestrator. A component represents a page or UI section with focusable elements.
```rust
pub trait Component {
type Focus: FocusId; // What can receive focus
type Action: Action; // What actions this handles
type Event: Clone + Debug; // Events this component emits
fn targets(&self) -> &[Self::Focus];
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>>;
}
```
**Optional methods** (all have defaults):
- `on_enter()` - Called when component becomes active
- `on_exit()` - Called when component becomes inactive
- `on_focus()` - Called when a focus target gains focus
- `on_blur()` - Called when a focus target loses focus
- `handle_text()` - Called when character is typed
- `can_navigate_forward/backward()` - Control focus movement
### Component Actions
Standard actions the library provides:
```rust
pub enum ComponentAction {
Next, // Tab
Prev, // Shift+Tab
First, // Home
Last, // End
Select, // Enter
Cancel, // Esc
TypeChar(char), // Any character
Backspace, // Backspace
Delete, // Delete
Custom(usize), // User-defined
}
```
### Focus Management
Focus tracks which element is currently active. The library provides:
- `FocusManager<F>` - Generic focus tracking
- `FocusQuery` - Read-only focus state for rendering
- Automatic navigation (next, prev, first, last)
### Orchestrator
The complete TUI runtime that wires everything together:
- `Orchestrator<C>` - Main framework struct
- `process_frame()` - Process one input frame
- `run()` - Complete main loop
- Extension points for custom behavior
---
## Extension Points
For complex applications (like komp_ac), the library provides extension points to customize behavior:
### ModeResolver
Customize how modes are resolved (dynamic vs static).
```rust
impl ModeResolver for CustomResolver {
fn resolve(&self, focus: &dyn Any) -> Vec<ModeName> { ... }
}
```
### OverlayManager
Customize overlay types (dialogs, command palettes, search).
```rust
impl OverlayManager for CustomOverlayManager {
fn is_active(&self) -> bool { ... }
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> { ... }
}
```
### EventHandler
Customize how events are routed to handlers.
```rust
impl EventHandler<AppEvent> for CustomHandler {
fn handle(&mut self, event: AppEvent) -> Result<HandleResult> { ... }
}
```
---
## Example: Multi-Page App
```rust
#[derive(Debug, Clone)]
enum MyPage {
Login(LoginPage),
Home(HomePage),
Settings(SettingsPage),
}
fn main() -> Result<()> {
let mut orch = Orchestrator::builder()
.with_page("login", LoginPage::new())
.with_page("home", HomePage::new())
.with_page("settings", SettingsPage::new())
.with_default_bindings()
.build()?;
orch.navigate_to("login")?;
orch.run()?;
}
```
Navigation with history:
```rust
orch.navigate_to("home")?;
orch.navigate_to("settings")?;
orch.back()? // Return to home
```
---
## Feature Flags
```toml
[dependencies]
tui_orchestrator = { version = "0.1", features = ["std"] }
# Optional features
sequences = ["alloc"] # Enable multi-key sequences
```
- `default` - No features (pure no_std)
- `std` - Enable std library support
- `alloc` - Enable alloc support (needed for collections)
---
## Design Philosophy
1. **Plugin-play model** - Library is runtime, components are plugins
2. **Sensible defaults** - Zero configuration works
3. **Optional everything** - Define only what you need
4. **Extension points** - Override defaults when needed
5. **User-focused** - "register page" not "bind chord to registry"
6. **no_std first** - Works on embedded, opt-in std
---
## For komp_ac Integration
komp_ac can:
1. Implement `Component` trait for all pages
2. Use library's `Orchestrator` as runtime
3. Extend with custom `ModeResolver` for dynamic Canvas-style modes
4. Extend with custom `OverlayManager` for command palette, find file, search
5. Extend with custom `EventHandler` for page/global/canvas routing
**Result:** komp_ac uses library's core while keeping all custom behavior.
See [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) for details.
---
## Migration Guide
If you're migrating from a TUI built with manual wiring:
1. **Identify components** - What are your pages/sections?
2. **Implement Component trait** - `targets()`, `handle()`, optional hooks
3. **Remove manual orchestration** - Delete manual focus/binding/router setup
4. **Use Orchestrator** - Register pages and run
5. **Add extensions if needed** - ModeResolver, OverlayManager, EventHandler
The library handles everything else.
---
## Examples
See `examples/` directory for complete working applications:
- `simple_app.rs` - Basic multi-page TUI
- `form_app.rs` - Form with text input
- `extended_app.rs` - Using extension points
---
## Documentation
- [PLAN.md](PLAN.md) - Complete implementation plan
- [REDESIGN.md](REDESIGN.md) - Framework architecture deep dive
- [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) - Integration examples and patterns
---
## License
MIT OR Apache-2.0

497
REDESIGN.md Normal file
View File

@@ -0,0 +1,497 @@
# TUI Orchestrator: Framework-Based Design
## Philosophy Shift
### From Building Blocks to Framework
**Old approach:** Provide individual primitives (keys, bindings, focus) that users wire together manually.
**New approach:** Provide complete TUI framework where users define components and library handles everything else.
This is a **plugin-play model**:
- Library is the runtime
- Components are plugins
- Extension points allow customization
- Everything else is optional with sensible defaults
---
## The "Ready to Use" Vision
### What Users Should Do
```rust
// 1. Define component
#[derive(Debug, Clone)]
enum LoginPage {
Username,
Password,
LoginBtn,
}
#[derive(Debug, Clone)]
enum LoginEvent {
AttemptLogin { username: String, password: String },
Cancel,
}
impl Component for LoginPage {
type Focus = LoginPage;
type Action = ComponentAction;
type Event = LoginEvent;
fn targets(&self) -> &[Self::Focus] { ... }
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> { ... }
}
// 2. Register and run
fn main() -> Result<()> {
let mut orch = Orchestrator::builder()
.with_page("login", LoginPage::new())
.with_default_bindings()
.build()?;
orch.run()?;
}
```
### What Library Does
**Automatically:**
- Input processing (read keys, route to actions)
- Focus management (next, prev, set, clear overlay)
- Page navigation (call on_exit, swap, call on_enter)
- Lifecycle hooks (on_focus, on_blur called at right time)
- Default bindings (Tab=Next, Enter=Select, etc.)
- Event collection and routing
**Never:**
- Forces user to write glue code
- Requires manual lifecycle management
- Makes assumptions about app structure
- Requires complex configuration
---
## Extension Model
### Three-Layer Architecture
```
Layer 1: Core Framework (Library)
├── Component trait
├── Orchestrator runtime
├── Default bindings
└── Router + lifecycle
Layer 2: Extension Points (For komp_ac)
├── ModeResolver - dynamic mode resolution
├── OverlayManager - custom overlay types
├── EventHandler - custom event routing
└── FocusNavigation - boundary detection
Layer 3: App Logic (User)
├── Page definitions
├── Business logic (gRPC, authentication)
└── Rendering
```
### Layer 1: What Library Provides
**Component trait** - The abstraction:
- `targets()` - What's focusable
- `handle()` - What happens on action
- `on_enter/on_exit` - Lifecycle hooks
- `on_focus/on_blur` - Focus lifecycle
- `handle_text()` - Optional text input
- `can_navigate_*()` - Optional boundary detection
**Orchestrator** - The runtime:
- `register_page()` - Add pages
- `navigate_to()` - Page navigation
- `process_frame()` - Process one input frame
- `run()` - Complete main loop
**Standard actions** - Common patterns:
- `Next`, `Prev`, `First`, `Last` - Navigation
- `Select`, `Cancel` - Selection
- `TypeChar`, `Backspace`, `Delete` - Text input
- `Custom(usize)` - User extension
### Layer 2: Extension Points
Each extension has a **default implementation** that works for simple apps, and a **trait** that komp_ac implements for custom behavior.
#### ModeResolver
**Default:** Static mode stack
```rust
pub struct DefaultModeResolver;
impl ModeResolver for DefaultModeResolver {
fn resolve(&self, _focus: &dyn Any) -> Vec<ModeName> {
vec![ModeName::General]
}
}
```
**komp_ac extension:** Dynamic Canvas-style mode resolution
```rust
pub struct CanvasModeResolver {
app_state: AppState,
}
impl ModeResolver for CanvasModeResolver {
fn resolve(&self, focus: &dyn Any) -> Vec<ModeName> {
// Check if focus is canvas field
// Get editor mode (Edit/ReadOnly)
// Return mode stack: [EditorMode, Common, Global]
}
}
```
**Use case:** Simple app doesn't care about modes. komp_ac needs dynamic resolution based on editor state.
#### OverlayManager
**Default:** Simple dialog/input overlay
```rust
pub struct DefaultOverlayManager {
stack: Vec<Overlay>,
}
impl OverlayManager for DefaultOverlayManager {
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> { ... }
}
```
**komp_ac extension:** Complex overlay types (command palette, find file, search palette)
```rust
pub struct KompAcOverlayManager {
command_bar: CommandBar,
find_file: FindFilePalette,
search: SearchPalette,
}
impl OverlayManager for KompAcOverlayManager {
fn handle_input(&mut self, key: Key) -> Option<OverlayResult> {
// Route to appropriate overlay
}
}
```
**Use case:** Simple app uses built-in dialogs. komp_ac needs custom overlays that integrate with editor, gRPC, etc.
#### EventHandler
**Default:** Return events to user
```rust
pub struct DefaultEventHandler<E>;
impl<E> EventHandler for DefaultEventHandler<E> {
fn handle(&mut self, event: E) -> Result<HandleResult> {
// Just pass events back to user
Ok(HandleResult::Consumed)
}
}
```
**komp_ac extension:** Route to page/global/canvas handlers
```rust
pub struct KompAcEventHandler {
router: Router,
focus: FocusManager,
canvas_handlers: HashMap<Page, Box<dyn CanvasHandler>>,
}
impl EventHandler for KompAcEventHandler {
fn handle(&mut self, event: AppEvent) -> Result<HandleResult> {
match self.focus.current() {
Some(FocusTarget::CanvasField(_)) => self.canvas_handler.handle(event),
_ => self.page_handler.handle(event),
}
}
}
```
**Use case:** Simple app just processes events. komp_ac needs complex routing based on focus type and context.
### Layer 3: App Logic
**This is entirely user-defined:**
- Page structs/enums
- Business logic
- API calls (gRPC, HTTP)
- State management
- Rendering
The library never touches this.
---
## Key Design Decisions
### 1. Associated Types vs Generics
**Choice:** Component trait uses associated types
```rust
pub trait Component {
type Focus: FocusId;
type Action: Action;
type Event: Clone + Debug;
}
```
**Why:**
- One component = one configuration
- Type system ensures consistency
- Cleaner trait signature
**Alternative:** Generics `Component<F, A, E>`
**Why not:**
- More verbose
- Type inference harder
- Less "component feels like a thing"
### 2. Automatic vs Explicit Navigation
**Choice:** Library automatically moves focus on Next/Prev actions
**Why:**
- Reduces boilerplate
- Consistent behavior across apps
- Component only needs to know "button was pressed"
**Alternative:** Library passes Next/Prev action, component decides what to do
**Why not:**
- Every component implements same logic
- Easy to miss patterns
- Library already has FocusManager—use it
**Escape hatch:** Components can override with `can_navigate_forward/backward()`
### 3. Event Model
**Choice:** Components return `Option<Event>`, library collects and returns
**Why:**
- Library can handle internal events (focus changes, page nav)
- Users get clean list of events to process
- Decouples component from application
**Alternative:** Components emit events directly to channel/bus
**Why not:**
- Requires async or channels
- More complex setup
- Library can't orchestrate internal events
### 4. Page vs Component
**Choice:** Library doesn't distinguish—everything is a Component
**Why:**
- Simpler API
- User can nest components if needed
- Flat hierarchy, easy to understand
**Alternative:** Library has `Page` and `Component` concepts
**Why not:**
- Forces app structure
- Some apps don't have pages
- More concepts to learn
### 5. Extension Points
**Choice:** Extension points are trait objects (`Box<dyn Trait> + 'static`)
**Why:**
- Allows komp_ac to pass stateful resolvers
- Flexible at runtime
- Can be swapped dynamically
**Alternative:** Generic with bounds (`<R: ModeResolver + Sized>`)
**Why not:**
- Monomorphization bloat
- Can't store different implementations
- Less flexible
---
## Comparison: Building Blocks vs Framework
### Building Blocks (Old Design)
**What user writes:**
```rust
// Setup
let mut focus = FocusManager::new();
let mut bindings = Bindings::new();
let mut router = Router::new();
// Configuration
bindings.bind(Key::tab(), MyAction::Next);
bindings.bind(Key::enter(), MyAction::Select);
focus.set_targets(page.targets());
router.navigate(Page::Login);
// Main loop
loop {
let key = read_key()?;
if let Some(action) = bindings.handle(key) {
match action {
MyAction::Next => focus.next(),
MyAction::Select => {
let focused = focus.current()?;
let result = page.handle_button(focused)?;
// Handle result...
}
}
}
render(&focus, &router)?;
}
```
**Problems:**
- Tons of boilerplate
- User must understand all systems
- Easy to miss lifecycle (forgot to call on_exit?)
- Manual wiring everywhere
- Every app reinvents same code
### Framework (New Design)
**What user writes:**
```rust
impl Component for LoginPage {
fn targets(&self) -> &[Focus] { ... }
fn handle(&mut self, focus: &Focus, action: Action) -> Result<Option<Event>> { ... }
}
fn main() -> Result<()> {
let mut orch = Orchestrator::builder()
.with_page("login", LoginPage::new())
.build()?;
orch.run()?;
}
```
**Benefits:**
- Zero boilerplate
- Library handles everything
- Lifecycle automatic
- Consistent behavior
- Easy to reason about
---
## Extension Strategy for komp_ac
### What komp_ac Keeps
komp_ac continues to own:
- All page state and logic
- gRPC client and authentication
- Rendering with ratatui
- Canvas editor integration
- Command palette logic
- Find file palette logic
- Business rules
### What komp_ac Replaces
komp_ac removes:
- `InputOrchestrator` - Uses library's `Orchestrator`
- `ActionDecider` routing logic - Uses library's event handler
- Manual lifecycle calls - Uses library's automatic hooks
- Mode stack assembly - Uses library's `ModeResolver` extension
- Overlay management - Uses library's `OverlayManager` extension
### Integration Pattern
komp_ac implements `Component` trait for each page:
```rust
impl Component for LoginPage {
type Focus = FocusTarget;
type Action = ResolvedAction;
type Event = AppEvent;
fn targets(&self) -> &[Self::Focus] {
// Return existing focus targets
&[FocusTarget::CanvasField(0), FocusTarget::Button(0), ...]
}
fn handle(&mut self, focus: &Self::Focus, action: Self::Action) -> Result<Option<Self::Event>> {
// Return existing app events
match (focus, action) {
(FocusTarget::Button(0), ResolvedAction::Keybind(KeybindAction::Save)) => {
Ok(Some(AppEvent::FormSave { path: self.path.clone() }))
}
_ => Ok(None),
}
}
}
```
komp_ac uses extension points:
```rust
let mut orch = Orchestrator::new()
.with_mode_resolver(CanvasModeResolver::new(app_state))
.with_overlay_manager(KompAcOverlayManager::new())
.with_event_handler(KompAcEventHandler::new(router, focus));
```
**Result:** komp_ac uses library's core while keeping all custom behavior.
---
## Future-Proofing
### What Can Be Added Without Breaking Changes
1. **Additional lifecycle hooks:** Add new methods to `Component` trait with default impls
2. **More actions:** Add variants to `ComponentAction` enum
3. **New overlay types:** Implement `OverlayManager` trait
4. **Custom input sources:** Implement `InputSource` trait
5. **Animation support:** Add hooks for frame updates
6. **Accessibility:** Add hooks for screen readers
### What Requires Breaking Changes
1. **Component trait signature:** Changing associated types
2. **Orchestrator API:** Major method signature changes
3. **Extension point contracts:** Changing trait methods
**Strategy:** Mark APIs as `#[doc(hidden)]` or `#[deprecated]` before removing.
---
## Summary
The redesigned TUI Orchestrator is:
1. **Complete framework** - Not just building blocks
2. **Zero boilerplate** - Users define components, library runs show
3. **Sensible defaults** - Works without configuration
4. **Fully extendable** - Trait-based extension points
5. **komp_ac compatible** - Can replace existing orchestration
6. **User-focused** - "register page" not "bind chord to registry"
The library becomes a **TUI runtime** where users write application logic and library handles everything else.

57
examples/focus_example.rs Normal file
View File

@@ -0,0 +1,57 @@
extern crate alloc;
use tui_orchestrator::focus::{FocusManager, Focusable};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum FormElement {
Username,
Password,
RememberMe,
Submit,
Cancel,
}
#[allow(dead_code)]
struct LoginForm {
username: String,
password: String,
remember: bool,
}
impl Focusable<FormElement> for LoginForm {
fn focus_targets(&self) -> alloc::vec::Vec<FormElement> {
vec![
FormElement::Username,
FormElement::Password,
FormElement::RememberMe,
FormElement::Submit,
FormElement::Cancel,
]
}
}
fn main() {
let form = LoginForm {
username: String::new(),
password: String::new(),
remember: false,
};
let mut focus_manager: FocusManager<FormElement> = FocusManager::new();
focus_manager.set_targets(form.focus_targets());
assert_eq!(focus_manager.current(), Some(&FormElement::Username));
focus_manager.next();
assert_eq!(focus_manager.current(), Some(&FormElement::Password));
focus_manager.set_focus(FormElement::Submit).unwrap();
assert_eq!(focus_manager.current(), Some(&FormElement::Submit));
let query = focus_manager.query();
assert!(query.is_focused(&FormElement::Submit));
focus_manager.set_overlay(FormElement::Cancel);
assert!(focus_manager.has_overlay());
assert_eq!(focus_manager.current(), Some(&FormElement::Cancel));
}

19
src/component/action.rs Normal file
View File

@@ -0,0 +1,19 @@
// path_from_the_root: src/component/action.rs
use crate::input::Action;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentAction {
Next,
Prev,
First,
Last,
Select,
Cancel,
TypeChar(char),
Backspace,
Delete,
Custom(usize),
}
impl Action for ComponentAction {}

7
src/component/error.rs Normal file
View File

@@ -0,0 +1,7 @@
// path_from_the_root: src/component/error.rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComponentError {
EmptyTargets,
InvalidFocus,
}

9
src/component/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
// path_from_the_root: src/component/mod.rs
pub mod action;
pub mod error;
pub mod r#trait;
pub use action::ComponentAction;
pub use error::ComponentError;
pub use r#trait::Component;

50
src/component/trait.rs Normal file
View File

@@ -0,0 +1,50 @@
// path_from_the_root: src/component/trait.rs
use super::error::ComponentError;
use crate::focus::FocusId;
pub trait Component {
type Focus: FocusId;
type Action: core::fmt::Debug + Clone;
type Event: Clone + core::fmt::Debug;
fn targets(&self) -> &[Self::Focus];
fn handle(
&mut self,
focus: &Self::Focus,
action: Self::Action,
) -> Result<Option<Self::Event>, ComponentError>;
fn on_enter(&mut self) -> Result<(), ComponentError> {
Ok(())
}
fn on_exit(&mut self) -> Result<(), ComponentError> {
Ok(())
}
fn on_focus(&mut self, _focus: &Self::Focus) -> Result<(), ComponentError> {
Ok(())
}
fn on_blur(&mut self, _focus: &Self::Focus) -> Result<(), ComponentError> {
Ok(())
}
fn handle_text(
&mut self,
focus: &Self::Focus,
_ch: char,
) -> Result<Option<Self::Event>, ComponentError> {
Ok(None)
}
fn can_navigate_forward(&self, _focus: &Self::Focus) -> bool {
true
}
fn can_navigate_backward(&self, _focus: &Self::Focus) -> bool {
true
}
}

8
src/focus/error.rs Normal file
View File

@@ -0,0 +1,8 @@
// path_from_the_root: src/focus/error.rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FocusError {
TargetNotFound,
EmptyTargets,
OverlayActive,
}

5
src/focus/id.rs Normal file
View File

@@ -0,0 +1,5 @@
// 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 {}

136
src/focus/manager.rs Normal file
View File

@@ -0,0 +1,136 @@
// path_from_the_root: src/focus/manager.rs
use super::error::FocusError;
use super::id::FocusId;
use super::query::FocusQuery;
#[derive(Debug, Clone)]
pub struct FocusManager<F: FocusId> {
targets: alloc::vec::Vec<F>,
index: usize,
overlay: Option<F>,
}
impl<F: FocusId> Default for FocusManager<F> {
fn default() -> Self {
Self::new()
}
}
impl<F: FocusId> FocusManager<F> {
pub fn new() -> Self {
Self {
targets: alloc::vec::Vec::new(),
index: 0,
overlay: None,
}
}
pub fn set_targets(&mut self, targets: alloc::vec::Vec<F>) {
self.targets = targets;
self.index = 0;
}
pub fn add_target(&mut self, id: F) {
if !self.targets.contains(&id) {
self.targets.push(id);
}
}
pub fn remove_target(&mut self, id: &F) {
if let Some(pos) = self.targets.iter().position(|t| t == id) {
self.targets.remove(pos);
if self.index >= self.targets.len() && !self.targets.is_empty() {
self.index = self.targets.len() - 1;
}
}
}
pub fn current(&self) -> Option<&F> {
if let Some(overlay) = &self.overlay {
return Some(overlay);
}
self.targets.get(self.index)
}
pub fn query(&self) -> FocusQuery<'_, F> {
FocusQuery {
current: self.current(),
}
}
pub fn is_focused(&self, id: &F) -> bool {
self.current() == Some(id)
}
pub fn has_overlay(&self) -> bool {
self.overlay.is_some()
}
pub fn set_focus(&mut self, id: F) -> Result<(), FocusError> {
if self.targets.is_empty() {
return Err(FocusError::EmptyTargets);
}
if let Some(pos) = self.targets.iter().position(|t| t == &id) {
self.index = pos;
self.overlay = None;
Ok(())
} else {
Err(FocusError::TargetNotFound)
}
}
pub fn set_overlay(&mut self, id: F) {
self.overlay = Some(id);
}
pub fn clear_overlay(&mut self) {
self.overlay = None;
}
pub fn next(&mut self) {
if self.overlay.is_some() {
return;
}
if !self.targets.is_empty() && self.index < self.targets.len() - 1 {
self.index += 1;
}
}
pub fn prev(&mut self) {
if self.overlay.is_some() {
return;
}
if !self.targets.is_empty() && self.index > 0 {
self.index -= 1;
}
}
pub fn first(&mut self) {
self.index = 0;
self.overlay = None;
}
pub fn last(&mut self) {
if !self.targets.is_empty() {
self.index = self.targets.len() - 1;
}
self.overlay = None;
}
pub fn targets(&self) -> &[F] {
&self.targets
}
pub fn len(&self) -> usize {
self.targets.len()
}
pub fn is_empty(&self) -> bool {
self.targets.is_empty()
}
}

13
src/focus/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
// path_from_the_root: src/focus/mod.rs
pub mod error;
pub mod id;
pub mod manager;
pub mod query;
pub mod traits;
pub use error::FocusError;
pub use id::FocusId;
pub use manager::FocusManager;
pub use query::FocusQuery;
pub use traits::Focusable;

22
src/focus/query.rs Normal file
View File

@@ -0,0 +1,22 @@
// path_from_the_root: src/focus/query.rs
use super::id::FocusId;
#[derive(Debug, Clone, Copy)]
pub struct FocusQuery<'a, F: FocusId> {
pub current: Option<&'a F>,
}
impl<'a, F: FocusId> FocusQuery<'a, F> {
pub fn new(current: Option<&'a F>) -> Self {
Self { current }
}
pub fn is_focused(&self, id: &F) -> bool {
self.current == Some(id)
}
pub fn has_focus(&self) -> bool {
self.current.is_some()
}
}

12
src/focus/traits.rs Normal file
View File

@@ -0,0 +1,12 @@
// path_from_the_root: src/focus/traits.rs
use super::error::FocusError;
use super::id::FocusId;
pub trait Focusable<F: FocusId> {
fn focus_targets(&self) -> alloc::vec::Vec<F>;
fn on_focus_change(&mut self, _id: &F) -> Result<(), FocusError> {
Ok(())
}
}

5
src/input/action.rs Normal file
View File

@@ -0,0 +1,5 @@
// path_from_the_root: src/input/action.rs
pub trait Action: Clone + PartialEq + Eq + core::fmt::Debug {}
impl Action for ComponentAction {}

70
src/input/bindings.rs Normal file
View File

@@ -0,0 +1,70 @@
// path_from_the_root: src/input/bindings.rs
use super::action::Action;
use super::key::Key;
#[cfg(feature = "alloc")]
use hashbrown::HashSet;
#[derive(Debug, Clone)]
pub struct Bindings<A: Action> {
bindings: alloc::vec::Vec<(Key, A)>,
}
impl<A: Action> Bindings<A> {
pub fn new() -> Self {
Self {
bindings: alloc::vec::Vec::new(),
}
}
pub fn bind(&mut self, key: Key, action: A) {
self.bindings.push((key, action));
}
pub fn get(&self, key: &Key) -> Option<&A> {
self.bindings.iter().find(|(k, _)| k == key).map(|(_, a)| a)
}
pub fn remove(&mut self, key: &Key) {
self.bindings.retain(|(k, _)| k != key);
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
pub fn len(&self) -> usize {
self.bindings.len()
}
pub fn iter(&self) -> impl Iterator<Item = &(Key, A)> {
self.bindings.iter()
}
}
impl<A: Action> Default for Bindings<A> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "sequences")]
impl<A: Action + core::hash::Hash + Eq> Bindings<A> {
pub fn bind_sequence(&mut self, keys: alloc::vec::Vec<Key>, action: A) {
for key in keys {
self.bindings.push((key, action.clone()));
}
}
pub fn get_sequences(&self) -> alloc::vec::Vec<&A> {
let mut actions = alloc::vec::Vec::new();
let mut seen = HashSet::new();
for (_, action) in &self.bindings {
if seen.insert(action) {
actions.push(action);
}
}
actions
}
}

100
src/input/handler.rs Normal file
View File

@@ -0,0 +1,100 @@
// path_from_the_root: src/input/handler.rs
#[cfg(feature = "sequences")]
use super::action::Action;
#[cfg(feature = "sequences")]
use super::key::Key;
#[cfg(feature = "sequences")]
use super::result::MatchResult;
#[cfg(feature = "sequences")]
pub struct SequenceHandler<A: Action> {
sequences: alloc::vec::Vec<(alloc::vec::Vec<Key>, A)>,
current: alloc::vec::Vec<Key>,
}
#[cfg(feature = "sequences")]
impl<A: Action> SequenceHandler<A> {
pub fn new() -> Self {
Self {
sequences: alloc::vec::Vec::new(),
current: alloc::vec::Vec::new(),
}
}
pub fn bind(&mut self, keys: impl IntoIterator<Item = Key>, action: A) {
let vec: alloc::vec::Vec<Key> = keys.into_iter().collect();
self.sequences.push((vec, action));
}
pub fn handle(&mut self, key: Key) -> MatchResult<A> {
self.current.push(key);
for (seq, action) in &self.sequences {
if seq == &self.current {
let action = action.clone();
self.current.clear();
return MatchResult::Match(action);
}
}
let is_prefix = self
.sequences
.iter()
.any(|(seq, _)| seq.len() > self.current.len() && seq.starts_with(&self.current));
if is_prefix {
MatchResult::Pending
} else {
self.current.clear();
MatchResult::NoMatch
}
}
pub fn reset(&mut self) {
self.current.clear();
}
pub fn current_sequence(&self) -> &[Key] {
&self.current
}
pub fn in_sequence(&self) -> bool {
!self.current.is_empty()
}
pub fn continuations(&self) -> alloc::vec::Vec<(&Key, &[Key], &A)> {
if self.current.is_empty() {
return alloc::vec::Vec::new();
}
let current = &self.current;
let current_len = current.len();
self.sequences
.iter()
.filter_map(move |(seq, action)| {
if seq.len() > current_len && seq.starts_with(current) {
let next_key = &seq[current_len];
let remaining = &seq[current_len + 1..];
Some((next_key, remaining, action))
} else {
None
}
})
.collect()
}
pub fn all_sequences(&self) -> impl Iterator<Item = (&[Key], &A)> {
self.sequences.iter().map(|(k, a)| (k.as_slice(), a))
}
}
#[cfg(feature = "sequences")]
impl<A: Action> Default for SequenceHandler<A> {
fn default() -> Self {
Self::new()
}
}

127
src/input/key.rs Normal file
View File

@@ -0,0 +1,127 @@
// path_from_the_root: src/input/key.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyCode {
Char(char),
Enter,
Tab,
Esc,
Backspace,
Delete,
Home,
End,
PageUp,
PageDown,
Up,
Down,
Left,
Right,
F(u8),
Null,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct KeyModifiers {
pub control: bool,
pub alt: bool,
pub shift: bool,
}
impl KeyModifiers {
pub const fn new() -> Self {
Self {
control: false,
alt: false,
shift: false,
}
}
pub const fn with_control(mut self) -> Self {
self.control = true;
self
}
pub const fn with_alt(mut self) -> Self {
self.alt = true;
self
}
pub const fn with_shift(mut self) -> Self {
self.shift = true;
self
}
pub const fn is_empty(&self) -> bool {
!self.control && !self.alt && !self.shift
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Key {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl Key {
pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
pub const fn char(c: char) -> Self {
Self {
code: KeyCode::Char(c),
modifiers: KeyModifiers::new(),
}
}
pub const fn ctrl(c: char) -> Self {
Self {
code: KeyCode::Char(c),
modifiers: KeyModifiers::new().with_control(),
}
}
pub fn display_string(&self) -> alloc::string::String {
let mut out = alloc::string::String::new();
if self.modifiers.control {
out.push_str("Ctrl+");
}
if self.modifiers.alt {
out.push_str("Alt+");
}
if self.modifiers.shift {
out.push_str("Shift+");
}
match self.code {
KeyCode::Char(c) => out.push(c),
KeyCode::Enter => out.push_str("Enter"),
KeyCode::Tab => out.push_str("Tab"),
KeyCode::Esc => out.push_str("Esc"),
KeyCode::Backspace => out.push_str("Backspace"),
KeyCode::Delete => out.push_str("Delete"),
KeyCode::Up => out.push_str("Up"),
KeyCode::Down => out.push_str("Down"),
KeyCode::Left => out.push_str("Left"),
KeyCode::Right => out.push_str("Right"),
KeyCode::F(n) => {
out.push('F');
out.push(char::from_digit(n as u32, 10).unwrap_or('0'));
}
KeyCode::Home => out.push_str("Home"),
KeyCode::End => out.push_str("End"),
KeyCode::PageUp => out.push_str("PageUp"),
KeyCode::PageDown => out.push_str("PageDown"),
KeyCode::Null => out.push_str("Null"),
}
out
}
}
impl From<KeyCode> for Key {
fn from(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::new(),
}
}
}

15
src/input/mod.rs Normal file
View File

@@ -0,0 +1,15 @@
// path_from_the_root: src/input/mod.rs
pub mod action;
pub mod bindings;
pub mod handler;
pub mod key;
pub mod result;
pub use action::Action;
pub use bindings::Bindings;
pub use key::{Key, KeyCode, KeyModifiers};
pub use result::MatchResult;
#[cfg(feature = "sequences")]
pub use handler::SequenceHandler;

29
src/input/result.rs Normal file
View File

@@ -0,0 +1,29 @@
// path_from_the_root: src/input/result.rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MatchResult<A> {
Match(A),
Pending,
NoMatch,
}
impl<A> MatchResult<A> {
pub fn matched(&self) -> bool {
matches!(self, Self::Match(_))
}
pub fn pending(&self) -> bool {
matches!(self, Self::Pending)
}
pub fn no_match(&self) -> bool {
matches!(self, Self::NoMatch)
}
pub fn into_match(self) -> Option<A> {
match self {
Self::Match(a) => Some(a),
_ => None,
}
}
}

13
src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
#![no_std]
extern crate alloc;
pub mod component;
pub mod focus;
pub mod input;
pub mod prelude {
pub use crate::component::*;
pub use crate::focus::*;
pub use crate::input::*;
}

7
src/prelude.rs Normal file
View File

@@ -0,0 +1,7 @@
// path_from_the_root: src/prelude.rs
pub use crate::component::action::ComponentAction;
pub use crate::component::error::ComponentError;
pub use crate::component::Component;
pub use crate::focus::*;
pub use crate::input::*;

73
tests/bindings.rs Normal file
View File

@@ -0,0 +1,73 @@
use tui_orchestrator::input::{Bindings, Key};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(dead_code)]
enum TestAction {
Quit,
Save,
Open,
}
#[test]
fn test_bindings_new() {
let _bindings: Bindings<TestAction> = Bindings::new();
}
#[test]
fn test_bindings_bind() {
let mut bindings: Bindings<TestAction> = Bindings::new();
bindings.bind(Key::char('q'), TestAction::Quit);
assert_eq!(bindings.get(&Key::char('q')), Some(&TestAction::Quit));
}
#[test]
fn test_bindings_get_not_found() {
let mut bindings: Bindings<TestAction> = Bindings::new();
bindings.bind(Key::char('q'), TestAction::Quit);
assert_eq!(bindings.get(&Key::char('x')), None);
}
#[test]
fn test_bindings_remove() {
let mut bindings: Bindings<TestAction> = Bindings::new();
bindings.bind(Key::char('q'), TestAction::Quit);
bindings.remove(&Key::char('q'));
assert_eq!(bindings.get(&Key::char('q')), None);
}
#[test]
fn test_bindings_is_empty() {
let mut bindings: Bindings<TestAction> = Bindings::new();
assert!(bindings.is_empty());
bindings.bind(Key::char('q'), TestAction::Quit);
assert!(!bindings.is_empty());
}
#[test]
fn test_bindings_len() {
let mut bindings: Bindings<TestAction> = Bindings::new();
assert_eq!(bindings.len(), 0);
bindings.bind(Key::char('q'), TestAction::Quit);
assert_eq!(bindings.len(), 1);
bindings.bind(Key::char('s'), TestAction::Save);
assert_eq!(bindings.len(), 2);
}
#[test]
fn test_bindings_iter() {
let mut bindings: Bindings<TestAction> = Bindings::new();
bindings.bind(Key::char('q'), TestAction::Quit);
bindings.bind(Key::char('s'), TestAction::Save);
let actions: Vec<_> = bindings.iter().map(|(_, a)| *a).collect();
assert!(actions.contains(&TestAction::Quit));
assert!(actions.contains(&TestAction::Save));
}
#[test]
fn test_bindings_default() {
let _bindings: Bindings<TestAction> = Bindings::default();
}

122
tests/component_tests.rs Normal file
View File

@@ -0,0 +1,122 @@
extern crate alloc;
use tui_orchestrator::component::{Component, ComponentAction};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum TestFocus {
FieldA,
FieldB,
ButtonC,
}
#[derive(Debug, Clone)]
enum TestEvent {
ButtonCPressed,
TextTyped(char),
}
struct TestComponent {
field_a: alloc::string::String,
field_b: alloc::string::String,
}
impl Component for TestComponent {
type Focus = TestFocus;
type Action = ComponentAction;
type Event = TestEvent;
fn targets(&self) -> &[Self::Focus] {
&[
Self::Focus::FieldA,
Self::Focus::FieldB,
Self::Focus::ButtonC,
]
}
fn handle(
&mut self,
focus: &Self::Focus,
action: Self::Action,
) -> Result<Option<Self::Event>, tui_orchestrator::component::error::ComponentError> {
match (focus, action) {
(Self::Focus::ButtonC, ComponentAction::Select) => {
Ok(Some(Self::Event::ButtonCPressed))
}
_ => Ok(None),
}
}
fn on_enter(&mut self) -> Result<(), tui_orchestrator::component::error::ComponentError> {
self.field_a.clear();
self.field_b.clear();
Ok(())
}
}
#[test]
fn test_component_targets() {
let mut component = TestComponent {
field_a: alloc::string::String::new(),
field_b: alloc::string::String::new(),
};
let targets = component.targets();
assert_eq!(targets.len(), 3);
assert_eq!(targets[0], TestFocus::FieldA);
}
#[test]
fn test_component_handle_select() {
let mut component = TestComponent {
field_a: alloc::string::String::new(),
field_b: alloc::string::String::new(),
};
let focus = TestFocus::ButtonC;
let action = ComponentAction::Select;
let event = component.handle(&focus, action);
assert!(event.is_ok());
assert!(matches!(event.unwrap(), Some(TestEvent::ButtonCPressed)));
}
#[test]
fn test_component_handle_text() {
let mut component = TestComponent {
field_a: alloc::string::String::new(),
field_b: alloc::string::String::new(),
};
let focus = TestFocus::FieldA;
let ch = 'x';
let event = component.handle_text(&focus, ch);
assert!(event.is_ok());
assert!(matches!(event.unwrap(), Some(TestEvent::TextTyped('x'))));
}
#[test]
fn test_component_on_enter_clears() {
let mut component = TestComponent {
field_a: alloc::string::String::from("test"),
field_b: alloc::string::String::from("test"),
};
component.on_enter().unwrap();
assert_eq!(component.field_a.as_str(), "");
assert_eq!(component.field_b.as_str(), "");
}
#[test]
fn test_component_defaults() {
let component = TestComponent {
field_a: alloc::string::String::new(),
field_b: alloc::string::String::new(),
};
assert!(component.on_exit().is_ok());
assert!(component.on_focus(&TestFocus::FieldA).is_ok());
assert!(component.on_blur(&TestFocus::FieldA).is_ok());
assert!(component.can_navigate_forward(&TestFocus::FieldA));
assert!(component.can_navigate_backward(&TestFocus::FieldA));
}

283
tests/focus.rs Normal file
View File

@@ -0,0 +1,283 @@
extern crate alloc;
use tui_orchestrator::focus::{FocusError, FocusManager, Focusable};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[allow(dead_code)]
enum TestId {
Field(usize),
Button(&'static str),
Menu,
Dialog,
}
#[test]
fn test_focus_id_trait() {
let id1 = TestId::Button("save");
let id2 = TestId::Button("save");
assert_eq!(id1, id2);
}
#[test]
fn test_manager_new() {
let manager: FocusManager<TestId> = FocusManager::new();
assert!(manager.is_empty());
assert_eq!(manager.current(), None);
}
#[test]
fn test_manager_set_targets() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
assert_eq!(manager.len(), 3);
assert_eq!(manager.current(), Some(&TestId::Field(0)));
}
#[test]
fn test_manager_navigation() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
assert_eq!(manager.current(), Some(&TestId::Field(0)));
manager.next();
assert_eq!(manager.current(), Some(&TestId::Field(1)));
manager.next();
assert_eq!(manager.current(), Some(&TestId::Button("save")));
manager.next();
assert_eq!(manager.current(), Some(&TestId::Button("save")));
manager.prev();
assert_eq!(manager.current(), Some(&TestId::Field(1)));
manager.first();
assert_eq!(manager.current(), Some(&TestId::Field(0)));
manager.last();
assert_eq!(manager.current(), Some(&TestId::Button("save")));
}
#[test]
fn test_manager_prev_at_start() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
manager.prev();
assert_eq!(manager.current(), Some(&TestId::Field(0)));
}
#[test]
fn test_manager_set_focus() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
let result = manager.set_focus(TestId::Button("save"));
assert!(result.is_ok());
assert_eq!(manager.current(), Some(&TestId::Button("save")));
let result = manager.set_focus(TestId::Field(0));
assert!(result.is_ok());
assert_eq!(manager.current(), Some(&TestId::Field(0)));
}
#[test]
fn test_manager_set_focus_not_found() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
let result = manager.set_focus(TestId::Menu);
assert_eq!(result, Err(FocusError::TargetNotFound));
}
#[test]
fn test_manager_set_focus_empty() {
let mut manager: FocusManager<TestId> = FocusManager::new();
let result = manager.set_focus(TestId::Menu);
assert_eq!(result, Err(FocusError::EmptyTargets));
}
#[test]
fn test_manager_overlay() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
manager.set_overlay(TestId::Menu);
assert!(manager.has_overlay());
assert_eq!(manager.current(), Some(&TestId::Menu));
manager.next();
assert_eq!(manager.current(), Some(&TestId::Menu));
manager.clear_overlay();
assert!(!manager.has_overlay());
assert_eq!(manager.current(), Some(&TestId::Field(0)));
}
#[test]
fn test_manager_overlay_with_focus() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
manager.set_focus(TestId::Button("save")).unwrap();
assert_eq!(manager.current(), Some(&TestId::Button("save")));
manager.set_overlay(TestId::Menu);
assert_eq!(manager.current(), Some(&TestId::Menu));
manager.clear_overlay();
assert_eq!(manager.current(), Some(&TestId::Button("save")));
}
#[test]
fn test_manager_add_remove_target() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.add_target(TestId::Field(0));
manager.add_target(TestId::Field(1));
assert_eq!(manager.len(), 2);
assert_eq!(manager.current(), Some(&TestId::Field(0)));
manager.remove_target(&TestId::Field(0));
assert_eq!(manager.len(), 1);
assert_eq!(manager.current(), Some(&TestId::Field(1)));
}
#[test]
fn test_manager_remove_current_adjusts_index() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![TestId::Field(0), TestId::Field(1), TestId::Field(2)]);
manager.next();
assert_eq!(manager.current(), Some(&TestId::Field(1)));
manager.remove_target(&TestId::Field(1));
assert_eq!(manager.len(), 2);
assert_eq!(manager.current(), Some(&TestId::Field(2)));
}
#[test]
fn test_manager_query() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
let query = manager.query();
assert_eq!(query.current, Some(&TestId::Field(0)));
assert!(query.is_focused(&TestId::Field(0)));
assert!(!query.is_focused(&TestId::Field(1)));
assert!(query.has_focus());
}
#[test]
fn test_manager_query_no_focus() {
let manager: FocusManager<TestId> = FocusManager::new();
let query = manager.query();
assert_eq!(query.current, None);
assert!(!query.has_focus());
}
#[test]
fn test_manager_is_focused() {
let mut manager: FocusManager<TestId> = FocusManager::new();
manager.set_targets(vec![
TestId::Field(0),
TestId::Field(1),
TestId::Button("save"),
]);
assert!(manager.is_focused(&TestId::Field(0)));
assert!(!manager.is_focused(&TestId::Field(1)));
manager.next();
assert!(!manager.is_focused(&TestId::Field(0)));
assert!(manager.is_focused(&TestId::Field(1)));
}
#[test]
fn test_focusable_trait() {
struct TestComponent;
impl Focusable<TestId> for TestComponent {
fn focus_targets(&self) -> alloc::vec::Vec<TestId> {
vec![TestId::Field(0), TestId::Field(1), TestId::Button("save")]
}
}
let component = TestComponent;
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"));
}

96
tests/key.rs Normal file
View File

@@ -0,0 +1,96 @@
use tui_orchestrator::input::{Key, KeyCode, KeyModifiers};
#[test]
fn test_key_char() {
let key = Key::char('a');
assert_eq!(key.code, KeyCode::Char('a'));
assert!(key.modifiers.is_empty());
}
#[test]
fn test_key_ctrl() {
let key = Key::ctrl('s');
assert_eq!(key.code, KeyCode::Char('s'));
assert!(key.modifiers.control);
assert!(!key.modifiers.alt);
assert!(!key.modifiers.shift);
}
#[test]
fn test_key_new() {
let key = Key::new(KeyCode::Enter, KeyModifiers::new().with_alt());
assert_eq!(key.code, KeyCode::Enter);
assert!(key.modifiers.alt);
}
#[test]
fn test_key_from_keycode() {
let key = Key::from(KeyCode::Esc);
assert_eq!(key.code, KeyCode::Esc);
assert!(key.modifiers.is_empty());
}
#[test]
fn test_key_display_char() {
let key = Key::char('x');
let display = key.display_string();
assert!(display.contains('x'));
}
#[test]
fn test_key_display_ctrl() {
let key = Key::ctrl('c');
let display = key.display_string();
assert!(display.contains("Ctrl+"));
assert!(display.contains('c'));
}
#[test]
fn test_key_display_all_modifiers() {
let key = Key::new(
KeyCode::Char('a'),
KeyModifiers::new().with_control().with_alt().with_shift(),
);
let display = key.display_string();
assert!(display.contains("Ctrl+"));
assert!(display.contains("Alt+"));
assert!(display.contains("Shift+"));
}
#[test]
fn test_key_display_special() {
let key = Key::new(KeyCode::F(5), KeyModifiers::new());
let display = key.display_string();
assert!(display.contains("F5"));
}
#[test]
fn test_key_modifiers_new() {
let mods = KeyModifiers::new();
assert!(!mods.control);
assert!(!mods.alt);
assert!(!mods.shift);
}
#[test]
fn test_key_modifiers_builders() {
let mods = KeyModifiers::new().with_control().with_shift();
assert!(mods.control);
assert!(!mods.alt);
assert!(mods.shift);
}
#[test]
fn test_key_modifiers_is_empty() {
assert!(KeyModifiers::new().is_empty());
assert!(!KeyModifiers::new().with_control().is_empty());
}
#[test]
fn test_key_equality() {
let k1 = Key::char('a');
let k2 = Key::char('a');
let k3 = Key::ctrl('a');
assert_eq!(k1, k2);
assert_ne!(k1, k3);
}