Recreate repository due to Git object corruption (all files preserved)
This commit is contained in:
1
.git_corrupt_backup/HEAD
Normal file
1
.git_corrupt_backup/HEAD
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ref: refs/heads/main
|
||||||
11
.git_corrupt_backup/config
Normal file
11
.git_corrupt_backup/config
Normal 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/*
|
||||||
1
.git_corrupt_backup/description
Normal file
1
.git_corrupt_backup/description
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Unnamed repository; everything before the `;` is the name of the repository.
|
||||||
25
.git_corrupt_backup/hooks/applypatch-msg.sample
Normal file
25
.git_corrupt_backup/hooks/applypatch-msg.sample
Normal 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.
|
||||||
|
:
|
||||||
25
.git_corrupt_backup/hooks/commit-msg.sample
Normal file
25
.git_corrupt_backup/hooks/commit-msg.sample
Normal 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
|
||||||
|
}
|
||||||
1
.git_corrupt_backup/hooks/docs.url
Normal file
1
.git_corrupt_backup/hooks/docs.url
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://git-scm.com/docs/githooks
|
||||||
16
.git_corrupt_backup/hooks/fsmonitor-watchman.sample
Normal file
16
.git_corrupt_backup/hooks/fsmonitor-watchman.sample
Normal 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
|
||||||
12
.git_corrupt_backup/hooks/post-update.sample
Normal file
12
.git_corrupt_backup/hooks/post-update.sample
Normal 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
|
||||||
27
.git_corrupt_backup/hooks/pre-applypatch.sample
Normal file
27
.git_corrupt_backup/hooks/pre-applypatch.sample
Normal 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.
|
||||||
|
:
|
||||||
19
.git_corrupt_backup/hooks/pre-commit.sample
Normal file
19
.git_corrupt_backup/hooks/pre-commit.sample
Normal 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
|
||||||
16
.git_corrupt_backup/hooks/pre-merge-commit.sample
Normal file
16
.git_corrupt_backup/hooks/pre-merge-commit.sample
Normal 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.
|
||||||
|
:
|
||||||
46
.git_corrupt_backup/hooks/pre-push.sample
Normal file
46
.git_corrupt_backup/hooks/pre-push.sample
Normal 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
|
||||||
40
.git_corrupt_backup/hooks/pre-rebase.sample
Normal file
40
.git_corrupt_backup/hooks/pre-rebase.sample
Normal 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
|
||||||
54
.git_corrupt_backup/hooks/prepare-commit-msg.sample
Normal file
54
.git_corrupt_backup/hooks/prepare-commit-msg.sample
Normal 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
BIN
.git_corrupt_backup/index
Normal file
Binary file not shown.
5
.git_corrupt_backup/info/exclude
Normal file
5
.git_corrupt_backup/info/exclude
Normal 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/
|
||||||
0
.git_corrupt_backup/logs/HEAD
Normal file
0
.git_corrupt_backup/logs/HEAD
Normal file
0
.git_corrupt_backup/logs/refs/heads/main
Normal file
0
.git_corrupt_backup/logs/refs/heads/main
Normal file
1
.git_corrupt_backup/opencode
Normal file
1
.git_corrupt_backup/opencode
Normal file
@@ -0,0 +1 @@
|
|||||||
|
777b68e48a535cf1c87c2b4889b78efc439dd52c
|
||||||
28
.git_corrupt_backup/packed-refs
Normal file
28
.git_corrupt_backup/packed-refs
Normal 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
|
||||||
1
.git_corrupt_backup/refs/heads/main
Normal file
1
.git_corrupt_backup/refs/heads/main
Normal file
@@ -0,0 +1 @@
|
|||||||
|
6f24966c51a784ecb168bc3455953b41e454ed51
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
komp_ac_client/
|
||||||
|
target/
|
||||||
|
|
||||||
32
AGENTS.md
Normal file
32
AGENTS.md
Normal 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 5–10 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
39
Cargo.lock
generated
Normal 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
16
Cargo.toml
Normal 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
734
INPUT_PIPELINE_MIGRATION.md
Normal 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
582
INTEGRATION_GUIDE.md
Normal 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
540
PLAN.md
Normal 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
431
PROGRESS.md
Normal 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
322
README.md
Normal 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
497
REDESIGN.md
Normal 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
57
examples/focus_example.rs
Normal 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
19
src/component/action.rs
Normal 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
7
src/component/error.rs
Normal 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
9
src/component/mod.rs
Normal 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
50
src/component/trait.rs
Normal 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
8
src/focus/error.rs
Normal 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
5
src/focus/id.rs
Normal 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
136
src/focus/manager.rs
Normal 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
13
src/focus/mod.rs
Normal 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
22
src/focus/query.rs
Normal 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
12
src/focus/traits.rs
Normal 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
5
src/input/action.rs
Normal 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
70
src/input/bindings.rs
Normal 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
100
src/input/handler.rs
Normal 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
127
src/input/key.rs
Normal 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
15
src/input/mod.rs
Normal 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
29
src/input/result.rs
Normal 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
13
src/lib.rs
Normal 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
7
src/prelude.rs
Normal 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
73
tests/bindings.rs
Normal 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
122
tests/component_tests.rs
Normal 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
283
tests/focus.rs
Normal 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
96
tests/key.rs
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user