diff --git a/README.md b/README.md
index 14fcb64b..584d5842 100644
--- a/README.md
+++ b/README.md
@@ -111,14 +111,16 @@ $actual -eq $expected
| `Ctrl+R` | Reload catalog |
| `Ctrl+K` | Toggle favorites-only |
| `Alt+S` | Toggle selected-only view |
+| `Alt+N` / `Alt+P` | Jump to next/previous invalid field |
| `v` | Cycle category filter |
| `c` | Toggle current category |
| `C` | Open all categories |
| `x` | Clear selected extensions |
+| `j` `k` `h` `l` `g` `G` | Vim-style list and hierarchy motion |
| `Esc` | Unwind filter context / exit |
| `Ctrl+C` | Quit immediately |
-Full keybindings: [docs/modules/ROOT/pages/ui/keybindings.adoc](docs/modules/ROOT/pages/ui/keybindings.adoc)
+Letter shortcuts are case-insensitive, and each `Ctrl`/`Alt` shortcut also accepts its `Shift` variant. Full keybindings: [docs/modules/ROOT/pages/ui/keybindings.adoc](docs/modules/ROOT/pages/ui/keybindings.adoc)
## Requirements
diff --git a/docs/modules/ROOT/pages/architecture.adoc b/docs/modules/ROOT/pages/architecture.adoc
index b555e14c..9cbd72c7 100644
--- a/docs/modules/ROOT/pages/architecture.adoc
+++ b/docs/modules/ROOT/pages/architecture.adoc
@@ -113,7 +113,7 @@ Startup policy now lives under `application/`: `LiveStartupMetadataLoader` owns
== UI Layer Design
-The TUI layer uses an immediate-mode rendering architecture built on the https://github.com/ayagmar/tamboui[TamboUI] framework.
+The TUI layer uses an immediate-mode rendering architecture built on the https://github.com/tamboui/tamboui[TamboUI] framework.
=== Component Decomposition
@@ -172,8 +172,7 @@ CoreTuiController (Orchestration Shell)
├── UiTextConstants UI text content
├── CatalogLoadCoordinator Async catalog lifecycle + startup-overlay timing via reducer intents
├── GenerationFlowCoordinator Async generation lifecycle
- ├── GenerationStateMachine Allowed generation-state transitions
- └── UiTextInputKeys Text-input edit-key normalization
+ └── GenerationStateMachine Allowed generation-state transitions
....
Reducer-visible UI semantics are now converged for form and extension read-model state. `ProjectRequest`, `ValidationReport`, extension search/filter/read-model summaries, catalog lifecycle semantics, and overlay visibility flags update through reducer intents/effects and live in reducer-owned `UiState`. `UiRenderModel` is assembled immediately before render and carries the derived `metadataPanel`, `extensionsPanel`, `footer`, `generation`, and `startupOverlay` slices. Mutable widget internals such as `TextInputState` and `ListState` stay in adapter/runtime context rather than `UiState`.
diff --git a/docs/modules/ROOT/pages/troubleshooting.adoc b/docs/modules/ROOT/pages/troubleshooting.adoc
index 56b72956..2744d692 100644
--- a/docs/modules/ROOT/pages/troubleshooting.adoc
+++ b/docs/modules/ROOT/pages/troubleshooting.adoc
@@ -27,7 +27,7 @@ Actions:
java --enable-native-access=ALL-UNNAMED -jar target/quarkus-forge.jar
----
. Verify your terminal supports 256-color or true-color (e.g., `echo $TERM`). Recommended: `xterm-256color`, `alacritty`, `kitty`, or `wezterm`.
-. On Windows, use Windows Terminal or a recent ConEmu/Git Bash with VT mode enabled. Forge now defaults to the JLine backend on Windows to preserve arrow-key navigation; avoid forcing `tamboui.backend=panama` unless you are debugging backend behavior.
+. On Windows, run inside a real console (Windows Terminal, PowerShell, or `cmd.exe`) so the Panama backend can enable virtual-terminal input; Git Bash/MSYS `mintty` does not expose a Win32 console and leaves keys unregistered without `winpty`. Forge defaults to the Panama backend on every platform; set `tamboui.backend=jline3` (or `TAMBOUI_BACKEND=jline3`) only if you need to fall back to JLine.
[id="metadata-fallback"]
== Metadata fallback on startup
diff --git a/docs/modules/ROOT/pages/ui/keybindings.adoc b/docs/modules/ROOT/pages/ui/keybindings.adoc
index 54c3423e..59286cad 100644
--- a/docs/modules/ROOT/pages/ui/keybindings.adoc
+++ b/docs/modules/ROOT/pages/ui/keybindings.adoc
@@ -1,11 +1,13 @@
= TUI Keybindings
+Bindings build on the Vim binding set, so motion keys work alongside the arrow keys. Letter shortcuts (`v`, `x`, `f`) are case-insensitive, and every `Ctrl`/`Alt` shortcut also responds to its `Shift` variant (for example `Ctrl+R` and `Ctrl+Shift+R` both reload the catalog).
+
== Navigation
* `Tab` / `Shift+Tab`: move focus across form, search, list, and submit.
* `Up` / `Down` (or `j` / `k`): list navigation.
* `Left` / `Right` (or `h` / `l`): navigate category hierarchy.
-* `Home` / `End`: first/last row.
+* `Home` / `End` (or `g` / `G`): first/last row.
* `PgUp` / `PgDn`: previous/next category.
== Focus and Search
@@ -24,8 +26,14 @@
* `f`: favorite focused extension.
* `Ctrl+J`: jump to next favorite.
* `Ctrl+K`: toggle favorites-only filter.
+* `Alt+S`: toggle selected-only filter.
* `Ctrl+Y`: cycle preset-only filter (presets loaded from `code.quarkus.io`).
+== Form Validation
+
+* `Alt+N`: focus the next invalid field.
+* `Alt+P`: focus the previous invalid field.
+
== Global Actions
* `?`: open help overlay.
diff --git a/docs/modules/ROOT/pages/usage/tui.adoc b/docs/modules/ROOT/pages/usage/tui.adoc
index 32f230bc..54b4058d 100644
--- a/docs/modules/ROOT/pages/usage/tui.adoc
+++ b/docs/modules/ROOT/pages/usage/tui.adoc
@@ -46,7 +46,9 @@ NOTE: `--enable-native-access=ALL-UNNAMED` suppresses Panama FFM warnings from t
* `Ctrl+P`: command palette.
* `?`: help overlay.
* `Ctrl+K`: toggle favorites-only mode.
+* `Alt+S`: toggle selected-only mode.
* `Ctrl+J`: jump to next favorite.
+* `Alt+N` / `Alt+P`: jump to the next/previous invalid field.
== Submit and Exit
diff --git a/pom.xml b/pom.xml
index 2cb3f4f1..0b7662f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,12 +16,7 @@
dev/tamboui/tui/bindings/.*\\.properties$
- 0.3.0
- 0.3.0
- ${tamboui.core.version}
- ${tamboui.core.version}
- 0.3.0
- ${tamboui.tui.version}
+ 0.4.0
4.7.7
2.21.3
6.0.3
@@ -47,51 +42,14 @@
true
-
-
- ossrh-snapshots
- https://central.sonatype.com/repository/maven-snapshots/
-
- false
-
-
- true
- never
-
-
-
-
dev.tamboui
- tamboui-annotations
- ${tamboui.annotations.version}
-
-
- dev.tamboui
- tamboui-core
- ${tamboui.core.version}
-
-
- dev.tamboui
- tamboui-panama-backend
- ${tamboui.panama-backend.version}
-
-
- dev.tamboui
- tamboui-jline3-backend
- ${tamboui.jline3-backend.version}
-
-
- dev.tamboui
- tamboui-tui
- ${tamboui.tui.version}
-
-
- dev.tamboui
- tamboui-widgets
- ${tamboui.widgets.version}
+ tamboui-bom
+ ${tamboui.version}
+ pom
+ import
@@ -100,18 +58,19 @@
dev.tamboui
tamboui-tui
- ${tamboui.tui.version}
+
+
+ dev.tamboui
+ tamboui-toolkit
dev.tamboui
tamboui-panama-backend
- ${tamboui.panama-backend.version}
runtime
dev.tamboui
tamboui-jline3-backend
- ${tamboui.jline3-backend.version}
runtime
@@ -447,19 +406,21 @@
dev.tamboui
tamboui-tui
- ${tamboui.tui.version}
+ provided
+
+
+ dev.tamboui
+ tamboui-toolkit
provided
dev.tamboui
tamboui-panama-backend
- ${tamboui.panama-backend.version}
provided
dev.tamboui
tamboui-jline3-backend
- ${tamboui.jline3-backend.version}
provided
diff --git a/src/main/java/dev/ayagmar/quarkusforge/runtime/AppBindingsProfile.java b/src/main/java/dev/ayagmar/quarkusforge/runtime/AppBindingsProfile.java
index 37fee4dc..05c8833b 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/runtime/AppBindingsProfile.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/runtime/AppBindingsProfile.java
@@ -1,7 +1,6 @@
package dev.ayagmar.quarkusforge.runtime;
import dev.ayagmar.quarkusforge.ui.AppKeyActions;
-import dev.tamboui.tui.bindings.Actions;
import dev.tamboui.tui.bindings.BindingSets;
import dev.tamboui.tui.bindings.Bindings;
import dev.tamboui.tui.bindings.KeyTrigger;
@@ -10,29 +9,38 @@ final class AppBindingsProfile {
private AppBindingsProfile() {}
static Bindings bindings() {
- return BindingSets.standard().toBuilder()
- .bind(KeyTrigger.ch('j'), Actions.MOVE_DOWN)
- .bind(KeyTrigger.ch('k'), Actions.MOVE_UP)
- .bind(KeyTrigger.ch('h'), Actions.MOVE_LEFT)
- .bind(KeyTrigger.ch('l'), Actions.MOVE_RIGHT)
- .bind(KeyTrigger.ch('g'), Actions.HOME)
- .bind(KeyTrigger.ch('G'), Actions.END)
+ return BindingSets.vim().toBuilder()
.bind(KeyTrigger.ch('/'), AppKeyActions.FOCUS_EXTENSION_SEARCH)
.bind(KeyTrigger.ctrl('f'), AppKeyActions.FOCUS_EXTENSION_SEARCH)
+ .bind(KeyTrigger.ctrl('F'), AppKeyActions.FOCUS_EXTENSION_SEARCH)
.bind(KeyTrigger.ctrl('l'), AppKeyActions.FOCUS_EXTENSION_LIST)
+ .bind(KeyTrigger.ctrl('L'), AppKeyActions.FOCUS_EXTENSION_LIST)
.bind(KeyTrigger.ctrl('k'), AppKeyActions.TOGGLE_FAVORITES_FILTER)
+ .bind(KeyTrigger.ctrl('K'), AppKeyActions.TOGGLE_FAVORITES_FILTER)
.bind(KeyTrigger.ctrl('y'), AppKeyActions.CYCLE_PRESET_FILTER)
+ .bind(KeyTrigger.ctrl('Y'), AppKeyActions.CYCLE_PRESET_FILTER)
.bind(KeyTrigger.ctrl('j'), AppKeyActions.JUMP_TO_FAVORITE)
+ .bind(KeyTrigger.ctrl('J'), AppKeyActions.JUMP_TO_FAVORITE)
.bind(KeyTrigger.ctrl('r'), AppKeyActions.RELOAD_CATALOG)
+ .bind(KeyTrigger.ctrl('R'), AppKeyActions.RELOAD_CATALOG)
.bind(KeyTrigger.ctrl('e'), AppKeyActions.TOGGLE_ERROR_DETAILS)
- .bind(KeyTrigger.ch('v'), AppKeyActions.CATEGORY_FILTER_CYCLE)
+ .bind(KeyTrigger.ctrl('E'), AppKeyActions.TOGGLE_ERROR_DETAILS)
+ .bind(KeyTrigger.chIgnoreCase('v'), AppKeyActions.CATEGORY_FILTER_CYCLE)
.bind(KeyTrigger.ch('c'), AppKeyActions.TOGGLE_CATEGORY)
.bind(KeyTrigger.ch('C'), AppKeyActions.OPEN_ALL_CATEGORIES)
- .bind(KeyTrigger.ch('x'), AppKeyActions.CLEAR_SELECTED_EXTENSIONS)
- .bind(KeyTrigger.ch('f'), AppKeyActions.FAVORITE_TOGGLE)
+ .bind(KeyTrigger.chIgnoreCase('x'), AppKeyActions.CLEAR_SELECTED_EXTENSIONS)
+ .bind(KeyTrigger.chIgnoreCase('f'), AppKeyActions.FAVORITE_TOGGLE)
.bind(KeyTrigger.ch('?'), AppKeyActions.OPEN_HELP)
.bind(KeyTrigger.ctrl('p'), AppKeyActions.OPEN_COMMAND_PALETTE)
+ .bind(KeyTrigger.ctrl('P'), AppKeyActions.OPEN_COMMAND_PALETTE)
.bind(KeyTrigger.alt('g'), AppKeyActions.SUBMIT_GENERATION)
+ .bind(KeyTrigger.alt('G'), AppKeyActions.SUBMIT_GENERATION)
+ .bind(KeyTrigger.alt('s'), AppKeyActions.TOGGLE_SELECTED_FILTER)
+ .bind(KeyTrigger.alt('S'), AppKeyActions.TOGGLE_SELECTED_FILTER)
+ .bind(KeyTrigger.alt('n'), AppKeyActions.NEXT_INVALID_FIELD)
+ .bind(KeyTrigger.alt('N'), AppKeyActions.NEXT_INVALID_FIELD)
+ .bind(KeyTrigger.alt('p'), AppKeyActions.PREVIOUS_INVALID_FIELD)
+ .bind(KeyTrigger.alt('P'), AppKeyActions.PREVIOUS_INVALID_FIELD)
.build();
}
}
diff --git a/src/main/java/dev/ayagmar/quarkusforge/runtime/TuiBootstrapService.java b/src/main/java/dev/ayagmar/quarkusforge/runtime/TuiBootstrapService.java
index bde591c7..70678ba9 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/runtime/TuiBootstrapService.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/runtime/TuiBootstrapService.java
@@ -22,7 +22,6 @@
import dev.tamboui.tui.bindings.Bindings;
import java.time.Duration;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -31,10 +30,7 @@
public final class TuiBootstrapService {
private static final String BACKEND_PROPERTY_NAME = "tamboui.backend";
private static final String BACKEND_ENV_NAME = "TAMBOUI_BACKEND";
- private static final String JLINE_DUMB_PROPERTY_NAME = "org.jline.terminal.dumb";
private static final String PANAMA_BACKEND = "panama";
- private static final String JLINE3_BACKEND = "jline3";
- private static final String WINDOWS_BACKEND_PREFERENCE = JLINE3_BACKEND + "," + PANAMA_BACKEND;
private static final ReentrantLock BACKEND_PROPERTY_LOCK = new ReentrantLock();
public static final Duration STARTUP_SPLASH_MIN_DURATION = Duration.ofMillis(450);
private static final Duration TUI_TICK_RATE = Duration.ofMillis(40);
@@ -64,23 +60,18 @@ public static TuiConfig appTuiConfig() {
}
public static String defaultBackendPreference() {
- return defaultBackendPreference(System.getProperty("os.name", ""));
+ return PANAMA_BACKEND;
}
private static void configureTerminalBackendPreference() {
configureTerminalBackendPreference(
- System.getProperty(BACKEND_PROPERTY_NAME),
- System.getenv(BACKEND_ENV_NAME),
- System.getProperty("os.name", ""));
+ System.getProperty(BACKEND_PROPERTY_NAME), System.getenv(BACKEND_ENV_NAME));
}
- private static void configureTerminalBackendPreference(
- String propertyValue, String envValue, String osName) {
- String backendPreference = resolveBackendPreference(propertyValue, envValue, osName);
+ private static void configureTerminalBackendPreference(String propertyValue, String envValue) {
if (!isBackendPreferenceExplicitlyConfigured(propertyValue, envValue)) {
- System.setProperty(BACKEND_PROPERTY_NAME, backendPreference);
+ System.setProperty(BACKEND_PROPERTY_NAME, resolveBackendPreference(propertyValue, envValue));
}
- configureWindowsJlineDumbTerminalFallback(osName, backendPreference);
}
private static boolean isBackendPreferenceExplicitlyConfigured(
@@ -91,53 +82,14 @@ private static boolean isBackendPreferenceExplicitlyConfigured(
return envValue != null && !envValue.isBlank();
}
- private static String defaultBackendPreference(String osName) {
- if (isWindowsOsName(osName)) {
- return WINDOWS_BACKEND_PREFERENCE;
- }
- return PANAMA_BACKEND;
- }
-
- private static String resolveBackendPreference(
- String propertyValue, String envValue, String osName) {
+ private static String resolveBackendPreference(String propertyValue, String envValue) {
if (propertyValue != null && !propertyValue.isBlank()) {
return propertyValue;
}
if (envValue != null && !envValue.isBlank()) {
return envValue;
}
- return defaultBackendPreference(osName);
- }
-
- private static void configureWindowsJlineDumbTerminalFallback(
- String osName, String backendPreference) {
- if (!isWindowsOsName(osName) || !backendPreferenceContains(backendPreference, JLINE3_BACKEND)) {
- return;
- }
- String jlineDumbPropertyValue = System.getProperty(JLINE_DUMB_PROPERTY_NAME);
- if (jlineDumbPropertyValue != null && !jlineDumbPropertyValue.isBlank()) {
- return;
- }
- // JLine logs the Windows warning and returns a DumbTerminal. That leaves the TUI stuck,
- // so disable the dumb fallback and let Tamboui try the next backend instead.
- System.setProperty(JLINE_DUMB_PROPERTY_NAME, Boolean.FALSE.toString());
- }
-
- private static boolean backendPreferenceContains(String backendPreference, String backendName) {
- if (backendPreference == null || backendPreference.isBlank()) {
- return false;
- }
- return java.util.Arrays.stream(backendPreference.split(","))
- .map(String::trim)
- .anyMatch(backendName::equals);
- }
-
- private static boolean isWindowsOsName(String osName) {
- if (osName == null) {
- return false;
- }
- String normalized = osName.strip().toLowerCase(Locale.ROOT);
- return normalized.startsWith("windows");
+ return PANAMA_BACKEND;
}
public static void runHeadlessSmoke(RuntimeConfig runtimeConfig, DiagnosticLogger diagnostics) {
@@ -158,7 +110,6 @@ public void runInteractiveSmoke(
throws Exception {
BACKEND_PROPERTY_LOCK.lock();
String previousBackendPreference = System.getProperty(BACKEND_PROPERTY_NAME);
- String previousJlineDumbPreference = System.getProperty(JLINE_DUMB_PROPERTY_NAME);
try {
configureTerminalBackendPreference();
TuiConfig tuiConfig = appTuiConfig();
@@ -198,7 +149,6 @@ public void runInteractiveSmoke(
} finally {
try {
restoreTerminalBackendPreference(previousBackendPreference);
- restoreJlineDumbTerminalPreference(previousJlineDumbPreference);
} finally {
BACKEND_PROPERTY_LOCK.unlock();
}
@@ -217,7 +167,6 @@ public TuiSessionSummary run(
of("searchDebounceMs", Math.max(0, searchDebounceMs)));
BACKEND_PROPERTY_LOCK.lock();
String previousBackendPreference = System.getProperty(BACKEND_PROPERTY_NAME);
- String previousJlineDumbPreference = System.getProperty(JLINE_DUMB_PROPERTY_NAME);
try {
configureTerminalBackendPreference();
TuiConfig tuiConfig = appTuiConfig();
@@ -265,7 +214,6 @@ public TuiSessionSummary run(
} finally {
try {
restoreTerminalBackendPreference(previousBackendPreference);
- restoreJlineDumbTerminalPreference(previousJlineDumbPreference);
} finally {
BACKEND_PROPERTY_LOCK.unlock();
}
@@ -391,10 +339,6 @@ private static void restoreTerminalBackendPreference(String previousBackendPrefe
restoreSystemProperty(BACKEND_PROPERTY_NAME, previousBackendPreference);
}
- private static void restoreJlineDumbTerminalPreference(String previousJlineDumbPreference) {
- restoreSystemProperty(JLINE_DUMB_PROPERTY_NAME, previousJlineDumbPreference);
- }
-
private static void restoreSystemProperty(String propertyName, String previousValue) {
if (previousValue == null) {
System.clearProperty(propertyName);
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/AppKeyActions.java b/src/main/java/dev/ayagmar/quarkusforge/ui/AppKeyActions.java
index abfdd5e1..bedd803f 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/AppKeyActions.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/AppKeyActions.java
@@ -1,6 +1,5 @@
package dev.ayagmar.quarkusforge.ui;
-import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
public final class AppKeyActions {
@@ -84,10 +83,7 @@ static boolean isErrorDetailsToggleKey(KeyEvent keyEvent) {
}
static boolean isFocusExtensionSearchSlashKey(KeyEvent keyEvent) {
- return keyEvent.code() == KeyCode.CHAR
- && !keyEvent.hasCtrl()
- && !keyEvent.hasAlt()
- && keyEvent.character() == '/';
+ return !keyEvent.hasCtrl() && !keyEvent.hasAlt() && keyEvent.isChar('/');
}
static boolean isFocusExtensionSearchCtrlKey(KeyEvent keyEvent) {
@@ -107,25 +103,20 @@ static boolean isPreviousInvalidFieldKey(KeyEvent keyEvent) {
}
private static boolean hasAction(KeyEvent keyEvent, String action) {
- return keyEvent.action().filter(action::equals).isPresent();
+ return keyEvent.matches(action);
}
private static boolean isPlainChar(KeyEvent keyEvent, char lower, char upper) {
- return keyEvent.code() == KeyCode.CHAR
- && !keyEvent.hasCtrl()
+ return !keyEvent.hasCtrl()
&& !keyEvent.hasAlt()
- && (keyEvent.character() == lower || keyEvent.character() == upper);
+ && (keyEvent.isChar(lower) || keyEvent.isChar(upper));
}
private static boolean isCtrlChar(KeyEvent keyEvent, char lower, char upper) {
- return keyEvent.code() == KeyCode.CHAR
- && keyEvent.hasCtrl()
- && (keyEvent.character() == lower || keyEvent.character() == upper);
+ return keyEvent.hasCtrl() && (keyEvent.isChar(lower) || keyEvent.isChar(upper));
}
private static boolean isAltChar(KeyEvent keyEvent, char lower, char upper) {
- return keyEvent.code() == KeyCode.CHAR
- && keyEvent.hasAlt()
- && (keyEvent.character() == lower || keyEvent.character() == upper);
+ return keyEvent.hasAlt() && (keyEvent.isChar(lower) || keyEvent.isChar(upper));
}
}
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/CoreTuiController.java b/src/main/java/dev/ayagmar/quarkusforge/ui/CoreTuiController.java
index 52d3a563..32118438 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/CoreTuiController.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/CoreTuiController.java
@@ -797,12 +797,12 @@ public UiAction handleCommandPaletteKey(KeyEvent keyEvent) {
return routeIntent(
new UiIntent.CommandPaletteIntent(new UiIntent.CommandPaletteCommand.Dismiss()));
}
- if (keyEvent.isUp() || UiKeyMatchers.isVimUpKey(keyEvent)) {
+ if (keyEvent.isUp()) {
dispatchIntent(
new UiIntent.CommandPaletteIntent(new UiIntent.CommandPaletteCommand.MoveSelection(-1)));
return UiAction.handled(false);
}
- if (keyEvent.isDown() || UiKeyMatchers.isVimDownKey(keyEvent)) {
+ if (keyEvent.isDown()) {
dispatchIntent(
new UiIntent.CommandPaletteIntent(new UiIntent.CommandPaletteCommand.MoveSelection(1)));
return UiAction.handled(false);
@@ -817,8 +817,8 @@ public UiAction handleCommandPaletteKey(KeyEvent keyEvent) {
new UiIntent.CommandPaletteIntent(new UiIntent.CommandPaletteCommand.JumpEnd()));
return UiAction.handled(false);
}
- if (UiKeyMatchers.isDigitKey(keyEvent)) {
- int selected = Character.digit(keyEvent.character(), 10) - 1;
+ if (UiKeyPredicates.isDigitKey(keyEvent)) {
+ int selected = Character.digit(keyEvent.codePoint(), 10) - 1;
if (selected >= 0 && selected < UiTextConstants.COMMAND_PALETTE_ENTRIES.size()) {
dispatchIntent(
new UiIntent.CommandPaletteIntent(
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/CoreUiReducer.java b/src/main/java/dev/ayagmar/quarkusforge/ui/CoreUiReducer.java
index 4ae8806a..1e9c390e 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/CoreUiReducer.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/CoreUiReducer.java
@@ -541,12 +541,10 @@ && isUpNavigation(keyEvent)
&& state.extensions().listSelectionAtTop()) {
return reduceSharedAction(state, CommandPaletteAction.FOCUS_EXTENSION_SEARCH);
}
- if (focusTarget == FocusTarget.EXTENSION_LIST
- && (keyEvent.isLeft() || UiKeyMatchers.isVimLeftKey(keyEvent))) {
+ if (focusTarget == FocusTarget.EXTENSION_LIST && keyEvent.isLeft()) {
return reduceExtensionCommand(state, UiIntent.ExtensionCommand.HIERARCHY_LEFT);
}
- if (focusTarget == FocusTarget.EXTENSION_LIST
- && (keyEvent.isRight() || UiKeyMatchers.isVimRightKey(keyEvent))) {
+ if (focusTarget == FocusTarget.EXTENSION_LIST && keyEvent.isRight()) {
return reduceExtensionCommand(state, UiIntent.ExtensionCommand.HIERARCHY_RIGHT);
}
if (focusTarget == FocusTarget.EXTENSION_LIST && keyEvent.isPageDown()) {
@@ -649,10 +647,10 @@ private static ReduceResult reduceFocusNavigation(UiState state, KeyEvent keyEve
return moveFocus(state, focusTarget, 1);
}
if (focusTarget == FocusTarget.SUBMIT) {
- if (UiKeyMatchers.isVimDownKey(keyEvent)) {
+ if (keyEvent.isDown()) {
return moveFocus(state, focusTarget, 1);
}
- if (UiKeyMatchers.isVimUpKey(keyEvent)) {
+ if (keyEvent.isUp()) {
return moveFocus(state, focusTarget, -1);
}
}
@@ -715,18 +713,11 @@ private static UiState focusExtensionListForSharedAction(UiState state) {
}
private static boolean isExtensionNavigationKey(KeyEvent keyEvent) {
- return keyEvent.isUp()
- || UiKeyMatchers.isVimUpKey(keyEvent)
- || keyEvent.isDown()
- || UiKeyMatchers.isVimDownKey(keyEvent)
- || keyEvent.isHome()
- || UiKeyMatchers.isVimHomeKey(keyEvent)
- || keyEvent.isEnd()
- || UiKeyMatchers.isVimEndKey(keyEvent);
+ return keyEvent.isUp() || keyEvent.isDown() || keyEvent.isHome() || keyEvent.isEnd();
}
private static boolean isUpNavigation(KeyEvent keyEvent) {
- return keyEvent.isUp() || UiKeyMatchers.isVimUpKey(keyEvent);
+ return keyEvent.isUp();
}
private static ReduceResult reduceMetadataInput(
@@ -736,16 +727,7 @@ private static ReduceResult reduceMetadataInput(
if (!MetadataSelectorManager.isSelectorFocus(focusTarget)) {
return new ReduceResult(state, List.of(), UiAction.ignored());
}
- if (!keyEvent.isLeft()
- && !UiKeyMatchers.isVimLeftKey(keyEvent)
- && !keyEvent.isUp()
- && !UiKeyMatchers.isVimUpKey(keyEvent)
- && !keyEvent.isRight()
- && !UiKeyMatchers.isVimRightKey(keyEvent)
- && !keyEvent.isDown()
- && !UiKeyMatchers.isVimDownKey(keyEvent)
- && !keyEvent.isHome()
- && !keyEvent.isEnd()) {
+ if (!isMetadataSelectionKey(keyEvent)) {
return new ReduceResult(state, List.of(), UiAction.ignored());
}
if (!metadataIntent.optionsAvailable()) {
@@ -759,8 +741,7 @@ private static ReduceResult reduceMetadataInput(
private static ReduceResult reduceTextInput(UiState state, KeyEvent keyEvent) {
FocusTarget focusTarget = state.focusTarget();
- if (!UiFocusPredicates.isTextInputFocus(focusTarget)
- || !UiTextInputKeys.isSupportedEditKey(keyEvent)) {
+ if (!UiFocusPredicates.isTextInputFocus(focusTarget) || !isTextInputEditKey(keyEvent)) {
return new ReduceResult(state, List.of(), UiAction.ignored());
}
return new ReduceResult(
@@ -769,6 +750,26 @@ private static ReduceResult reduceTextInput(UiState state, KeyEvent keyEvent) {
UiAction.handled(false));
}
+ private static boolean isMetadataSelectionKey(KeyEvent keyEvent) {
+ return keyEvent.isLeft()
+ || keyEvent.isUp()
+ || keyEvent.isRight()
+ || keyEvent.isDown()
+ || keyEvent.isHome()
+ || keyEvent.isEnd();
+ }
+
+ private static boolean isTextInputEditKey(KeyEvent keyEvent) {
+ if (keyEvent.code() == KeyCode.CHAR) {
+ int codePoint = keyEvent.codePoint();
+ return !keyEvent.hasCtrl() && !keyEvent.hasAlt() && codePoint >= 32 && codePoint != 127;
+ }
+ return switch (keyEvent.code()) {
+ case BACKSPACE, DELETE, LEFT, RIGHT, HOME, END -> true;
+ default -> false;
+ };
+ }
+
private static ReduceResult reducePostGeneration(
UiState state, UiIntent.PostGenerationCommand command) {
UiState.PostGenerationView postGeneration = state.postGeneration();
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigation.java b/src/main/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigation.java
index 26e518b0..12d131d1 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigation.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigation.java
@@ -193,19 +193,19 @@ boolean handleNavigationKey(ExtensionCatalogRows rows, KeyEvent keyEvent) {
if (navigationRowIndexes.isEmpty()) {
return false;
}
- if (keyEvent.isUp() || UiKeyMatchers.isVimUpKey(keyEvent)) {
+ if (keyEvent.isUp()) {
selectPrevious(navigationRowIndexes);
return true;
}
- if (keyEvent.isDown() || UiKeyMatchers.isVimDownKey(keyEvent)) {
+ if (keyEvent.isDown()) {
selectNext(navigationRowIndexes);
return true;
}
- if (keyEvent.isHome() || UiKeyMatchers.isVimHomeKey(keyEvent)) {
+ if (keyEvent.isHome()) {
listState.select(navigationRowIndexes.getFirst());
return true;
}
- if (keyEvent.isEnd() || UiKeyMatchers.isVimEndKey(keyEvent)) {
+ if (keyEvent.isEnd()) {
listState.select(navigationRowIndexes.getLast());
return true;
}
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/InputEffects.java b/src/main/java/dev/ayagmar/quarkusforge/ui/InputEffects.java
index 0edc887c..d3971c85 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/InputEffects.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/InputEffects.java
@@ -4,7 +4,7 @@
import dev.ayagmar.quarkusforge.domain.CliPrefillMapper;
import dev.ayagmar.quarkusforge.domain.ProjectRequest;
import dev.ayagmar.quarkusforge.domain.ValidationReport;
-import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.toolkit.Toolkit;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.widgets.input.TextInputState;
import java.util.EnumMap;
@@ -64,16 +64,10 @@ void moveTextInputCursorToEnd(FocusTarget target) {
}
List applyMetadataSelectorKey(FocusTarget target, KeyEvent keyEvent) {
- if (keyEvent.isLeft()
- || UiKeyMatchers.isVimLeftKey(keyEvent)
- || keyEvent.isUp()
- || UiKeyMatchers.isVimUpKey(keyEvent)) {
+ if (keyEvent.isLeft() || keyEvent.isUp()) {
return applySelectorCycle(target, -1);
}
- if (keyEvent.isRight()
- || UiKeyMatchers.isVimRightKey(keyEvent)
- || keyEvent.isDown()
- || UiKeyMatchers.isVimDownKey(keyEvent)) {
+ if (keyEvent.isRight() || keyEvent.isDown()) {
return applySelectorCycle(target, 1);
}
if (keyEvent.isHome()) {
@@ -200,35 +194,18 @@ private static String selectorValue(ProjectRequest request, FocusTarget target)
}
private static TextInputEffectResult handleTextInputKey(TextInputState state, KeyEvent event) {
- if (state == null || !UiTextInputKeys.isSupportedEditKey(event)) {
+ if (state == null) {
return TextInputEffectResult.IGNORED;
}
- if (event.code() == KeyCode.CHAR) {
- state.insert(event.character());
- return TextInputEffectResult.MUTATED;
- }
- if (event.isDeleteBackward()) {
- state.deleteBackward();
- return TextInputEffectResult.MUTATED;
+ String previousText = state.text();
+ int previousCursorPosition = state.cursorPosition();
+ if (!Toolkit.handleTextInputKey(state, event)) {
+ return TextInputEffectResult.IGNORED;
}
- if (event.isDeleteForward()) {
- state.deleteForward();
+ if (!state.text().equals(previousText)) {
return TextInputEffectResult.MUTATED;
}
- if (event.isLeft()) {
- state.moveCursorLeft();
- return TextInputEffectResult.CURSOR_MOVED;
- }
- if (event.isRight()) {
- state.moveCursorRight();
- return TextInputEffectResult.CURSOR_MOVED;
- }
- if (event.isHome()) {
- state.moveCursorToStart();
- return TextInputEffectResult.CURSOR_MOVED;
- }
- if (event.isEnd()) {
- state.moveCursorToEnd();
+ if (state.cursorPosition() != previousCursorPosition) {
return TextInputEffectResult.CURSOR_MOVED;
}
return TextInputEffectResult.IGNORED;
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuState.java b/src/main/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuState.java
index 94277433..b09cf4b0 100644
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuState.java
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuState.java
@@ -36,10 +36,10 @@ UiIntent.PostGenerationCommand handleKey(UiState.PostGenerationView view, KeyEve
if (keyEvent.isCancel()) {
return new UiIntent.PostGenerationCommand.Quit();
}
- if (keyEvent.isUp() || UiKeyMatchers.isVimUpKey(keyEvent)) {
+ if (keyEvent.isUp()) {
return new UiIntent.PostGenerationCommand.MoveActionSelection(-1);
}
- if (keyEvent.isDown() || UiKeyMatchers.isVimDownKey(keyEvent)) {
+ if (keyEvent.isDown()) {
return new UiIntent.PostGenerationCommand.MoveActionSelection(1);
}
if (keyEvent.isFocusPrevious()) {
@@ -48,8 +48,8 @@ UiIntent.PostGenerationCommand handleKey(UiState.PostGenerationView view, KeyEve
if (keyEvent.isFocusNext()) {
return new UiIntent.PostGenerationCommand.MoveActionSelection(1);
}
- if (UiKeyMatchers.isDigitKey(keyEvent)) {
- int selected = Character.digit(keyEvent.character(), 10) - 1;
+ if (UiKeyPredicates.isDigitKey(keyEvent)) {
+ int selected = Character.digit(keyEvent.codePoint(), 10) - 1;
if (selected >= 0 && selected < view.actions().size()) {
return new UiIntent.PostGenerationCommand.SelectActionIndex(selected);
}
@@ -68,10 +68,10 @@ private UiIntent.PostGenerationCommand handleGithubVisibilityKey(KeyEvent keyEve
if (keyEvent.isCancel()) {
return new UiIntent.PostGenerationCommand.CancelGithubVisibility();
}
- if (keyEvent.isUp() || UiKeyMatchers.isVimUpKey(keyEvent)) {
+ if (keyEvent.isUp()) {
return new UiIntent.PostGenerationCommand.MoveGithubVisibilitySelection(-1);
}
- if (keyEvent.isDown() || UiKeyMatchers.isVimDownKey(keyEvent)) {
+ if (keyEvent.isDown()) {
return new UiIntent.PostGenerationCommand.MoveGithubVisibilitySelection(1);
}
if (keyEvent.isFocusPrevious()) {
@@ -80,8 +80,8 @@ private UiIntent.PostGenerationCommand handleGithubVisibilityKey(KeyEvent keyEve
if (keyEvent.isFocusNext()) {
return new UiIntent.PostGenerationCommand.MoveGithubVisibilitySelection(1);
}
- if (UiKeyMatchers.isDigitKey(keyEvent)) {
- int selected = Character.digit(keyEvent.character(), 10) - 1;
+ if (UiKeyPredicates.isDigitKey(keyEvent)) {
+ int selected = Character.digit(keyEvent.codePoint(), 10) - 1;
if (selected >= 0 && selected < UiTextConstants.GITHUB_VISIBILITY_LABELS.size()) {
return new UiIntent.PostGenerationCommand.SelectGithubVisibilityIndex(selected);
}
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/UiKeyMatchers.java b/src/main/java/dev/ayagmar/quarkusforge/ui/UiKeyMatchers.java
deleted file mode 100644
index df86e12e..00000000
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/UiKeyMatchers.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package dev.ayagmar.quarkusforge.ui;
-
-import dev.tamboui.tui.event.KeyCode;
-import dev.tamboui.tui.event.KeyEvent;
-
-final class UiKeyMatchers {
- private UiKeyMatchers() {}
-
- static boolean isVimUpKey(KeyEvent keyEvent) {
- return isPlainChar(keyEvent, 'k', 'K');
- }
-
- static boolean isVimDownKey(KeyEvent keyEvent) {
- return isPlainChar(keyEvent, 'j', 'J');
- }
-
- static boolean isVimLeftKey(KeyEvent keyEvent) {
- return isPlainChar(keyEvent, 'h', 'H');
- }
-
- static boolean isVimRightKey(KeyEvent keyEvent) {
- return isPlainChar(keyEvent, 'l', 'L');
- }
-
- static boolean isVimHomeKey(KeyEvent keyEvent) {
- return isPlainChar(keyEvent, 'g', 'g');
- }
-
- static boolean isVimEndKey(KeyEvent keyEvent) {
- return isPlainChar(keyEvent, 'G', 'G');
- }
-
- static boolean isPlainChar(KeyEvent keyEvent, char lower, char upper) {
- return keyEvent.code() == KeyCode.CHAR
- && !keyEvent.hasCtrl()
- && !keyEvent.hasAlt()
- && (keyEvent.character() == lower || keyEvent.character() == upper);
- }
-
- static boolean isDigitKey(KeyEvent keyEvent) {
- return keyEvent.code() == KeyCode.CHAR
- && !keyEvent.hasCtrl()
- && !keyEvent.hasAlt()
- && Character.isDigit(keyEvent.character());
- }
-}
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/UiKeyPredicates.java b/src/main/java/dev/ayagmar/quarkusforge/ui/UiKeyPredicates.java
new file mode 100644
index 00000000..d6f62954
--- /dev/null
+++ b/src/main/java/dev/ayagmar/quarkusforge/ui/UiKeyPredicates.java
@@ -0,0 +1,16 @@
+package dev.ayagmar.quarkusforge.ui;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+
+/** Shared key classification helpers used by menu/palette routing paths. */
+final class UiKeyPredicates {
+ private UiKeyPredicates() {}
+
+ static boolean isDigitKey(KeyEvent keyEvent) {
+ return keyEvent.code() == KeyCode.CHAR
+ && !keyEvent.hasCtrl()
+ && !keyEvent.hasAlt()
+ && Character.isDigit(keyEvent.codePoint());
+ }
+}
diff --git a/src/main/java/dev/ayagmar/quarkusforge/ui/UiTextInputKeys.java b/src/main/java/dev/ayagmar/quarkusforge/ui/UiTextInputKeys.java
deleted file mode 100644
index 409863cd..00000000
--- a/src/main/java/dev/ayagmar/quarkusforge/ui/UiTextInputKeys.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package dev.ayagmar.quarkusforge.ui;
-
-import dev.tamboui.tui.event.KeyCode;
-import dev.tamboui.tui.event.KeyEvent;
-
-/** Centralized predicate for text-input edit keys accepted by UI text fields. */
-final class UiTextInputKeys {
- private UiTextInputKeys() {}
-
- static boolean isSupportedEditKey(KeyEvent event) {
- if (event.code() == KeyCode.CHAR && !event.hasCtrl() && !event.hasAlt()) {
- return true;
- }
- return event.isDeleteBackward()
- || event.isDeleteForward()
- || event.isLeft()
- || event.isRight()
- || event.isHome()
- || event.isEnd();
- }
-}
diff --git a/src/test/java/dev/ayagmar/quarkusforge/TuiBootstrapServiceTest.java b/src/test/java/dev/ayagmar/quarkusforge/TuiBootstrapServiceTest.java
index bf4e77de..5173abd6 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/TuiBootstrapServiceTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/TuiBootstrapServiceTest.java
@@ -41,28 +41,10 @@ class TuiBootstrapServiceTest {
// ── static accessors ──────────────────────────────────────────────
@Test
- void defaultBackendPreferenceUsesPanamaOnNonWindows() {
- systemProperties.set("os.name", "Linux");
+ void defaultBackendPreferenceIsPanamaOnAllPlatforms() {
assertThat(TuiBootstrapService.defaultBackendPreference()).isEqualTo("panama");
}
- @Test
- void defaultBackendPreferenceDoesNotTreatDarwinAsWindows() {
- systemProperties.set("os.name", "Darwin");
- assertThat(TuiBootstrapService.defaultBackendPreference()).isEqualTo("panama");
- }
-
- @Test
- void defaultBackendPreferenceUsesJlineThenPanamaOnWindows() {
- systemProperties.set("os.name", "Windows 11");
- assertThat(TuiBootstrapService.defaultBackendPreference()).isEqualTo("jline3,panama");
- }
-
- @Test
- void defaultBackendPreferenceHandlesNullOsName() throws Exception {
- assertThat(invokeDefaultBackendPreference(null)).isEqualTo("panama");
- }
-
@Test
void startupSplashMinDurationIs450ms() {
assertThat(TuiBootstrapService.STARTUP_SPLASH_MIN_DURATION).isEqualTo(Duration.ofMillis(450));
@@ -88,69 +70,14 @@ void appTuiConfigIncludesBindings() {
@Test
void configureTerminalBackendPreferenceSetsDefaultOnlyWhenUnset() throws Exception {
- systemProperties.clear("org.jline.terminal.dumb");
-
- invokeConfigureTerminalBackendPreference(null, null, "Linux");
+ invokeConfigureTerminalBackendPreference(null, null);
assertThat(System.getProperty("tamboui.backend")).isEqualTo("panama");
- assertThat(System.getProperty("org.jline.terminal.dumb")).isNull();
System.setProperty("tamboui.backend", "custom");
- invokeConfigureTerminalBackendPreference("custom", null, "Linux");
+ invokeConfigureTerminalBackendPreference("custom", null);
assertThat(System.getProperty("tamboui.backend")).isEqualTo("custom");
- assertThat(System.getProperty("org.jline.terminal.dumb")).isNull();
- }
-
- @Test
- void configureTerminalBackendPreferenceUsesWindowsFallbackChainAndDisablesJlineDumbTerminal()
- throws Exception {
- invokeConfigureTerminalBackendPreference(null, null, "Windows 11");
- assertThat(System.getProperty("tamboui.backend")).isEqualTo("jline3,panama");
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("false");
- }
-
- @Test
- void configureTerminalBackendPreferenceDisablesJlineDumbTerminalForExplicitWindowsJline()
- throws Exception {
- systemProperties.clear("tamboui.backend");
-
- invokeConfigureTerminalBackendPreference("jline3", null, "Windows 11");
-
- assertThat(System.getProperty("tamboui.backend")).isNull();
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("false");
- }
-
- @Test
- void configureTerminalBackendPreferenceDisablesJlineDumbTerminalForEnvProvidedWindowsJline()
- throws Exception {
- systemProperties.clear("tamboui.backend");
-
- invokeConfigureTerminalBackendPreference(null, "jline3", "Windows 11");
-
- assertThat(System.getProperty("tamboui.backend")).isNull();
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("false");
- }
-
- @Test
- void
- configureTerminalBackendPreferenceLeavesJlineDumbTerminalUntouchedWhenWindowsBackendSkipsJline()
- throws Exception {
- systemProperties.clear("org.jline.terminal.dumb");
-
- invokeConfigureTerminalBackendPreference("panama", null, "Windows 11");
-
- assertThat(System.getProperty("org.jline.terminal.dumb")).isNull();
- }
-
- @Test
- void configureTerminalBackendPreferenceDoesNotOverrideExplicitJlineDumbTerminalSetting()
- throws Exception {
- systemProperties.set("org.jline.terminal.dumb", "true");
-
- invokeConfigureTerminalBackendPreference(null, null, "Windows 11");
-
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("true");
}
@Test
@@ -164,18 +91,9 @@ void backendPreferenceIsExplicitlyConfiguredOnlyForNonBlankProperty() throws Exc
@Test
void resolveBackendPreferencePrefersPropertyThenEnvThenDefault() throws Exception {
- assertThat(invokeResolveBackendPreference("jline3", "panama", "Windows 11"))
- .isEqualTo("jline3");
- assertThat(invokeResolveBackendPreference(" ", "panama", "Windows 11")).isEqualTo("panama");
- assertThat(invokeResolveBackendPreference(null, null, "Windows 11")).isEqualTo("jline3,panama");
- }
-
- @Test
- void backendPreferenceContainsHandlesBlankAndCommaSeparatedEntries() throws Exception {
- assertThat(invokeBackendPreferenceContains(null, "jline3")).isFalse();
- assertThat(invokeBackendPreferenceContains(" ", "jline3")).isFalse();
- assertThat(invokeBackendPreferenceContains(" panama , jline3 ", "jline3")).isTrue();
- assertThat(invokeBackendPreferenceContains(" panama ", "jline3")).isFalse();
+ assertThat(invokeResolveBackendPreference("jline3", "panama")).isEqualTo("jline3");
+ assertThat(invokeResolveBackendPreference(" ", "ansi")).isEqualTo("ansi");
+ assertThat(invokeResolveBackendPreference(null, null)).isEqualTo("panama");
}
@Test
@@ -223,27 +141,8 @@ void restoreTerminalBackendPreferenceRestoresPreviousValue() throws Exception {
}
@Test
- void restoreJlineDumbTerminalPreferenceClearsPropertyWhenPreviousValueMissing() throws Exception {
- systemProperties.set("org.jline.terminal.dumb", "false");
-
- invokeRestoreJlineDumbTerminalPreference(null);
-
- assertThat(System.getProperty("org.jline.terminal.dumb")).isNull();
- }
-
- @Test
- void restoreJlineDumbTerminalPreferenceRestoresPreviousValue() throws Exception {
- systemProperties.set("org.jline.terminal.dumb", "false");
-
- invokeRestoreJlineDumbTerminalPreference("true");
-
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("true");
- }
-
- @Test
- void runInteractiveSmokeRestoresJlineDumbTerminalPreferenceWhenBackendCreationFails() {
+ void runInteractiveSmokeRestoresBackendPreferenceWhenBackendCreationFails() {
systemProperties.set("tamboui.backend", "missing-backend");
- systemProperties.set("org.jline.terminal.dumb", "true");
assertThatThrownBy(
() ->
@@ -253,13 +152,11 @@ void runInteractiveSmokeRestoresJlineDumbTerminalPreferenceWhenBackendCreationFa
.hasMessageContaining("missing-backend");
assertThat(System.getProperty("tamboui.backend")).isEqualTo("missing-backend");
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("true");
}
@Test
- void runRestoresJlineDumbTerminalPreferenceWhenBackendCreationFails() {
+ void runRestoresBackendPreferenceWhenBackendCreationFails() {
systemProperties.set("tamboui.backend", "missing-backend");
- systemProperties.set("org.jline.terminal.dumb", "true");
assertThatThrownBy(
() ->
@@ -269,7 +166,6 @@ void runRestoresJlineDumbTerminalPreferenceWhenBackendCreationFails() {
.hasMessageContaining("missing-backend");
assertThat(System.getProperty("tamboui.backend")).isEqualTo("missing-backend");
- assertThat(System.getProperty("org.jline.terminal.dumb")).isEqualTo("true");
}
@Test
@@ -324,12 +220,12 @@ void runHeadlessSmokePropagatesCatalogLoadFailure() {
}
private static void invokeConfigureTerminalBackendPreference(
- String propertyValue, String envValue, String osName) throws Exception {
+ String propertyValue, String envValue) throws Exception {
Method method =
TuiBootstrapService.class.getDeclaredMethod(
- "configureTerminalBackendPreference", String.class, String.class, String.class);
+ "configureTerminalBackendPreference", String.class, String.class);
method.setAccessible(true);
- method.invoke(null, propertyValue, envValue, osName);
+ method.invoke(null, propertyValue, envValue);
}
private static void invokeConfigureTerminalBackendPreference() throws Exception {
@@ -348,29 +244,13 @@ private static boolean invokeIsBackendPreferenceExplicitlyConfigured(
return (boolean) method.invoke(null, propertyValue, envValue);
}
- private static String invokeDefaultBackendPreference(String osName) throws Exception {
- Method method =
- TuiBootstrapService.class.getDeclaredMethod("defaultBackendPreference", String.class);
- method.setAccessible(true);
- return (String) method.invoke(null, osName);
- }
-
- private static String invokeResolveBackendPreference(
- String propertyValue, String envValue, String osName) throws Exception {
- Method method =
- TuiBootstrapService.class.getDeclaredMethod(
- "resolveBackendPreference", String.class, String.class, String.class);
- method.setAccessible(true);
- return (String) method.invoke(null, propertyValue, envValue, osName);
- }
-
- private static boolean invokeBackendPreferenceContains(
- String backendPreference, String backendName) throws Exception {
+ private static String invokeResolveBackendPreference(String propertyValue, String envValue)
+ throws Exception {
Method method =
TuiBootstrapService.class.getDeclaredMethod(
- "backendPreferenceContains", String.class, String.class);
+ "resolveBackendPreference", String.class, String.class);
method.setAccessible(true);
- return (boolean) method.invoke(null, backendPreference, backendName);
+ return (String) method.invoke(null, propertyValue, envValue);
}
private static String invokeResolvePresetStreamKey(
@@ -391,15 +271,6 @@ private static void invokeRestoreTerminalBackendPreference(String previousBacken
method.invoke(null, previousBackendPreference);
}
- private static void invokeRestoreJlineDumbTerminalPreference(String previousJlineDumbPreference)
- throws Exception {
- Method method =
- TuiBootstrapService.class.getDeclaredMethod(
- "restoreJlineDumbTerminalPreference", String.class);
- method.setAccessible(true);
- method.invoke(null, previousJlineDumbPreference);
- }
-
private RuntimeConfig runtimeConfigForTests() {
return new RuntimeConfig(
URI.create("http://127.0.0.1"),
diff --git a/src/test/java/dev/ayagmar/quarkusforge/cli/QuarkusForgeCliBindingsProfileTest.java b/src/test/java/dev/ayagmar/quarkusforge/cli/QuarkusForgeCliBindingsProfileTest.java
index afcab301..789ca536 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/cli/QuarkusForgeCliBindingsProfileTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/cli/QuarkusForgeCliBindingsProfileTest.java
@@ -23,49 +23,75 @@ void appTuiConfigUsesAppBindingsProfile() {
void profilePreservesNavigationBindings() {
Bindings bindings = TuiBootstrapService.appBindingsProfile();
- assertThat(bindings.actionFor(KeyEvent.ofChar('j'))).contains(Actions.MOVE_DOWN);
- assertThat(bindings.actionFor(KeyEvent.ofChar('k'))).contains(Actions.MOVE_UP);
- assertThat(bindings.actionFor(KeyEvent.ofChar('h'))).contains(Actions.MOVE_LEFT);
- assertThat(bindings.actionFor(KeyEvent.ofChar('l'))).contains(Actions.MOVE_RIGHT);
- assertThat(bindings.actionFor(KeyEvent.ofChar('g'))).contains(Actions.HOME);
- assertThat(bindings.actionFor(KeyEvent.ofChar('G'))).contains(Actions.END);
+ assertThat(bindings.matches(KeyEvent.ofChar('j'), Actions.MOVE_DOWN)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('k'), Actions.MOVE_UP)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('h'), Actions.MOVE_LEFT)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('l'), Actions.MOVE_RIGHT)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('g'), Actions.HOME)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('G'), Actions.END)).isTrue();
}
@Test
void profilePreservesGlobalShortcutBindings() {
Bindings bindings = TuiBootstrapService.appBindingsProfile();
- assertThat(bindings.actionFor(KeyEvent.ofChar('/')))
- .contains(AppKeyActions.FOCUS_EXTENSION_SEARCH);
- assertThat(bindings.actionFor(KeyEvent.ofChar('f', KeyModifiers.CTRL)))
- .contains(AppKeyActions.FOCUS_EXTENSION_SEARCH);
- assertThat(bindings.actionFor(KeyEvent.ofChar('l', KeyModifiers.CTRL)))
- .contains(AppKeyActions.FOCUS_EXTENSION_LIST);
- assertThat(bindings.actionFor(KeyEvent.ofChar('r', KeyModifiers.CTRL)))
- .contains(AppKeyActions.RELOAD_CATALOG);
- assertThat(bindings.actionFor(KeyEvent.ofChar('p', KeyModifiers.CTRL)))
- .contains(AppKeyActions.OPEN_COMMAND_PALETTE);
- assertThat(bindings.actionFor(KeyEvent.ofChar('g', KeyModifiers.ALT)))
- .contains(AppKeyActions.SUBMIT_GENERATION);
+ assertThat(bindings.matches(KeyEvent.ofChar('/'), AppKeyActions.FOCUS_EXTENSION_SEARCH))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('f', KeyModifiers.CTRL), AppKeyActions.FOCUS_EXTENSION_SEARCH))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('l', KeyModifiers.CTRL), AppKeyActions.FOCUS_EXTENSION_LIST))
+ .isTrue();
+ assertThat(
+ bindings.matches(KeyEvent.ofChar('r', KeyModifiers.CTRL), AppKeyActions.RELOAD_CATALOG))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('p', KeyModifiers.CTRL), AppKeyActions.OPEN_COMMAND_PALETTE))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('g', KeyModifiers.ALT), AppKeyActions.SUBMIT_GENERATION))
+ .isTrue();
}
@Test
void profilePreservesExtensionShortcutBindings() {
Bindings bindings = TuiBootstrapService.appBindingsProfile();
- assertThat(bindings.actionFor(KeyEvent.ofChar('v')))
- .contains(AppKeyActions.CATEGORY_FILTER_CYCLE);
- assertThat(bindings.actionFor(KeyEvent.ofChar('c'))).contains(AppKeyActions.TOGGLE_CATEGORY);
- assertThat(bindings.actionFor(KeyEvent.ofChar('C')))
- .contains(AppKeyActions.OPEN_ALL_CATEGORIES);
- assertThat(bindings.actionFor(KeyEvent.ofChar('x')))
- .contains(AppKeyActions.CLEAR_SELECTED_EXTENSIONS);
- assertThat(bindings.actionFor(KeyEvent.ofChar('f'))).contains(AppKeyActions.FAVORITE_TOGGLE);
- assertThat(bindings.actionFor(KeyEvent.ofChar('k', KeyModifiers.CTRL)))
- .contains(AppKeyActions.TOGGLE_FAVORITES_FILTER);
- assertThat(bindings.actionFor(KeyEvent.ofChar('j', KeyModifiers.CTRL)))
- .contains(AppKeyActions.JUMP_TO_FAVORITE);
- assertThat(bindings.actionFor(KeyEvent.ofChar('e', KeyModifiers.CTRL)))
- .contains(AppKeyActions.TOGGLE_ERROR_DETAILS);
+ assertThat(bindings.matches(KeyEvent.ofChar('v'), AppKeyActions.CATEGORY_FILTER_CYCLE))
+ .isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('c'), AppKeyActions.TOGGLE_CATEGORY)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('C'), AppKeyActions.OPEN_ALL_CATEGORIES)).isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('x'), AppKeyActions.CLEAR_SELECTED_EXTENSIONS))
+ .isTrue();
+ assertThat(bindings.matches(KeyEvent.ofChar('f'), AppKeyActions.FAVORITE_TOGGLE)).isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('k', KeyModifiers.CTRL), AppKeyActions.TOGGLE_FAVORITES_FILTER))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('j', KeyModifiers.CTRL), AppKeyActions.JUMP_TO_FAVORITE))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('e', KeyModifiers.CTRL), AppKeyActions.TOGGLE_ERROR_DETAILS))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('s', KeyModifiers.ALT), AppKeyActions.TOGGLE_SELECTED_FILTER))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('n', KeyModifiers.ALT), AppKeyActions.NEXT_INVALID_FIELD))
+ .isTrue();
+ assertThat(
+ bindings.matches(
+ KeyEvent.ofChar('p', KeyModifiers.ALT), AppKeyActions.PREVIOUS_INVALID_FIELD))
+ .isTrue();
}
}
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/AppKeyActionsTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/AppKeyActionsTest.java
index 147d08c6..b4e4b633 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/AppKeyActionsTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/ui/AppKeyActionsTest.java
@@ -13,9 +13,11 @@ class AppKeyActionsTest {
@Test
void keyChecksPreserveRawShortcutParity() {
assertThat(AppKeyActions.isCatalogReloadKey(KeyEvent.ofChar('r', KeyModifiers.CTRL))).isTrue();
+ assertThat(AppKeyActions.isCatalogReloadKey(KeyEvent.ofChar('R', KeyModifiers.CTRL))).isTrue();
assertThat(AppKeyActions.isGenerateShortcutKey(KeyEvent.ofChar('g', KeyModifiers.ALT)))
.isTrue();
assertThat(AppKeyActions.isFavoriteToggleKey(KeyEvent.ofChar('f'))).isTrue();
+ assertThat(AppKeyActions.isFavoriteToggleKey(KeyEvent.ofChar('F'))).isTrue();
assertThat(AppKeyActions.isSelectedOnlyFilterToggleKey(KeyEvent.ofChar('s', KeyModifiers.ALT)))
.isTrue();
assertThat(AppKeyActions.isSelectedOnlyFilterToggleKey(KeyEvent.ofChar('S', KeyModifiers.ALT)))
@@ -35,6 +37,8 @@ void keyChecksPreserveRawShortcutParity() {
.isTrue();
assertThat(AppKeyActions.isPreviousInvalidFieldKey(KeyEvent.ofChar('p', KeyModifiers.ALT)))
.isTrue();
+ assertThat(AppKeyActions.isCatalogReloadKey(KeyEvent.ofChar('z'))).isFalse();
+ assertThat(AppKeyActions.isFavoriteToggleKey(KeyEvent.ofChar('f', KeyModifiers.ALT))).isFalse();
assertThat(AppKeyActions.isGenerateShortcutKey(KeyEvent.ofChar('g', KeyModifiers.CTRL)))
.isFalse();
assertThat(AppKeyActions.isHelpOverlayToggleKey(KeyEvent.ofChar('?'))).isTrue();
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/CoreTuiShellPilotTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/CoreTuiShellPilotTest.java
index 1c7d2209..09ba8411 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/CoreTuiShellPilotTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/ui/CoreTuiShellPilotTest.java
@@ -11,6 +11,7 @@
import dev.ayagmar.quarkusforge.domain.MetadataCompatibilityContext;
import dev.ayagmar.quarkusforge.domain.ProjectRequest;
import dev.ayagmar.quarkusforge.domain.ProjectRequestValidator;
+import dev.ayagmar.quarkusforge.runtime.TuiBootstrapService;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.KeyModifiers;
@@ -27,6 +28,10 @@
class CoreTuiShellPilotTest {
@RegisterExtension final SystemPropertyExtension systemProperties = new SystemPropertyExtension();
+ private static KeyEvent appChar(char character) {
+ return KeyEvent.ofChar(character, TuiBootstrapService.appBindingsProfile());
+ }
+
@Test
void focusTraversalCyclesWithTabAndShiftTab() {
CoreTuiController controller = UiControllerTestHarness.controller();
@@ -79,9 +84,9 @@ void vimListMotionsJAndKNavigateCatalogRows() {
UiControllerTestHarness.moveFocusTo(controller, FocusTarget.EXTENSION_LIST);
String firstId = controller.focusedListExtensionId();
- controller.onEvent(KeyEvent.ofChar('j'));
+ controller.onEvent(appChar('j'));
String secondId = controller.focusedListExtensionId();
- controller.onEvent(KeyEvent.ofChar('k'));
+ controller.onEvent(appChar('k'));
assertThat(secondId).isNotEqualTo(firstId);
assertThat(controller.focusedListExtensionId()).isEqualTo(firstId);
@@ -461,7 +466,7 @@ void commandPaletteRunsSelectedAction() {
assertThat(controller.focusTarget()).isEqualTo(FocusTarget.GROUP_ID);
controller.onEvent(KeyEvent.ofChar('p', KeyModifiers.CTRL));
- controller.onEvent(KeyEvent.ofChar('j'));
+ controller.onEvent(appChar('j'));
controller.onEvent(KeyEvent.ofKey(KeyCode.ENTER));
assertThat(controller.commandPaletteVisible()).isFalse();
@@ -482,6 +487,22 @@ void commandPaletteDigitShortcutRunsSelectedAction() {
assertThat(controller.statusMessage()).contains("Focus moved to extensionList");
}
+ @Test
+ void commandPaletteHandlesNavigationKeysAndIgnoresModifiedDigits() {
+ CoreTuiController controller = UiControllerTestHarness.controller();
+
+ controller.onEvent(KeyEvent.ofChar('p', KeyModifiers.CTRL));
+ controller.onEvent(KeyEvent.ofKey(KeyCode.DOWN));
+ controller.onEvent(KeyEvent.ofKey(KeyCode.UP));
+ controller.onEvent(KeyEvent.ofKey(KeyCode.HOME));
+ controller.onEvent(KeyEvent.ofKey(KeyCode.END));
+ controller.onEvent(KeyEvent.ofChar('2', KeyModifiers.CTRL));
+ controller.onEvent(KeyEvent.ofChar('2', KeyModifiers.ALT));
+
+ assertThat(controller.commandPaletteVisible()).isTrue();
+ assertThat(controller.focusTarget()).isEqualTo(FocusTarget.GROUP_ID);
+ }
+
@Test
void shortcutAndPaletteSharedQuickActionsStayInSync() {
CoreTuiController controller = UiControllerTestHarness.controller();
@@ -609,11 +630,11 @@ void submitFocusSupportsVimJkTraversal() {
CoreTuiController controller = UiControllerTestHarness.controller();
UiControllerTestHarness.moveFocusTo(controller, FocusTarget.SUBMIT);
- controller.onEvent(KeyEvent.ofChar('k'));
+ controller.onEvent(appChar('k'));
assertThat(controller.focusTarget()).isEqualTo(FocusTarget.EXTENSION_LIST);
UiControllerTestHarness.moveFocusTo(controller, FocusTarget.SUBMIT);
- controller.onEvent(KeyEvent.ofChar('j'));
+ controller.onEvent(appChar('j'));
assertThat(controller.focusTarget()).isEqualTo(FocusTarget.GROUP_ID);
}
@@ -1112,11 +1133,11 @@ void leftAndRightSupportSectionHierarchyNavigation() {
assertThat(controller.focusedListExtensionId()).isEmpty();
assertThat(controller.statusMessage()).contains("Moved to section: Core");
- controller.onEvent(KeyEvent.ofChar('l'));
+ controller.onEvent(appChar('l'));
assertThat(controller.focusedListExtensionId()).isEqualTo("io.quarkus:quarkus-arc");
assertThat(controller.statusMessage()).contains("Moved to first item in section: Core");
- controller.onEvent(KeyEvent.ofChar('h'));
+ controller.onEvent(appChar('h'));
controller.onEvent(KeyEvent.ofKey(KeyCode.LEFT));
assertThat(controller.statusMessage()).contains("Closed category: Core");
assertThat(UiControllerTestHarness.renderToString(controller)).contains("▶ Core ");
@@ -1217,7 +1238,7 @@ void categoryCloseCanBeUndoneWithSameKeyAndListNavigationRemainsPredictable() {
assertThat(UiControllerTestHarness.renderToString(controller)).contains("CDI");
assertThat(UiControllerTestHarness.renderToString(controller)).contains("▼ Core");
- controller.onEvent(KeyEvent.ofChar('j'));
+ controller.onEvent(appChar('j'));
assertThat(controller.focusedListExtensionId()).isEqualTo("io.quarkus:quarkus-arc");
}
@@ -1240,9 +1261,9 @@ void allCollapsedCategoriesStillSupportHeaderNavigationAndReopen() {
UiControllerTestHarness.moveFocusTo(controller, FocusTarget.EXTENSION_LIST);
controller.onEvent(KeyEvent.ofChar('c'));
- controller.onEvent(KeyEvent.ofChar('j'));
+ controller.onEvent(appChar('j'));
controller.onEvent(KeyEvent.ofChar('c'));
- controller.onEvent(KeyEvent.ofChar('j'));
+ controller.onEvent(appChar('j'));
controller.onEvent(KeyEvent.ofChar('c'));
String allCollapsed = UiControllerTestHarness.renderToString(controller);
@@ -1254,7 +1275,7 @@ void allCollapsedCategoriesStillSupportHeaderNavigationAndReopen() {
assertThat(allCollapsed).doesNotContain("JDBC PostgreSQL");
assertThat(controller.focusedListExtensionId()).isEmpty();
- controller.onEvent(KeyEvent.ofChar('k'));
+ controller.onEvent(appChar('k'));
controller.onEvent(KeyEvent.ofChar('c'));
assertThat(controller.statusMessage()).contains("Opened category: Web");
@@ -1309,7 +1330,7 @@ void metadataSelectorsCycleFromLoadedOptionsAndBlockFreeTextEdits() {
// Selector uses "Platform:" prefix in compact mode
assertThat(UiControllerTestHarness.renderToString(controller)).contains("Platform:");
- controller.onEvent(KeyEvent.ofChar('l'));
+ controller.onEvent(appChar('l'));
assertThat(controller.request().platformStream()).isEqualTo("io.quarkus.platform:3.20");
assertThat(controller.uiState().request().platformStream())
.isEqualTo("io.quarkus.platform:3.20");
@@ -1318,7 +1339,7 @@ void metadataSelectorsCycleFromLoadedOptionsAndBlockFreeTextEdits() {
assertThat(controller.validation().errors().getFirst().field()).isEqualTo("compatibility");
UiControllerTestHarness.moveFocusTo(controller, FocusTarget.JAVA_VERSION);
- controller.onEvent(KeyEvent.ofChar('h'));
+ controller.onEvent(appChar('h'));
assertThat(controller.request().javaVersion()).isEqualTo("21");
assertThat(controller.uiState().request().javaVersion()).isEqualTo("21");
assertThat(controller.validation().isValid()).isTrue();
@@ -1329,7 +1350,7 @@ void metadataSelectorsCycleFromLoadedOptionsAndBlockFreeTextEdits() {
controller.onEvent(KeyEvent.ofChar('x'));
assertThat(controller.request().buildTool()).isEqualTo(originalBuildTool);
- controller.onEvent(KeyEvent.ofChar('l'));
+ controller.onEvent(appChar('l'));
assertThat(controller.request().buildTool()).isEqualTo("gradle");
assertThat(controller.uiState().request().buildTool()).isEqualTo("gradle");
assertThat(controller.statusMessage()).isEqualTo("Build tool selected: gradle");
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/CoreUiReducerTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/CoreUiReducerTest.java
index 5390ff9e..db3e722b 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/CoreUiReducerTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/ui/CoreUiReducerTest.java
@@ -2,8 +2,10 @@
import static org.assertj.core.api.Assertions.assertThat;
+import dev.tamboui.tui.bindings.BindingSets;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -593,18 +595,34 @@ void extensionStateUpdatedIntentMakesExtensionViewReducerOwned() {
@Test
void extensionInteractionIntentRoutesListMovementThroughEffect() {
+ KeyEvent keyEvent = vimChar('j');
ReduceResult result =
reducer.reduce(
stateWithFocus(baseState(), FocusTarget.EXTENSION_LIST),
- new UiIntent.ExtensionInteractionIntent(KeyEvent.ofChar('j')));
+ new UiIntent.ExtensionInteractionIntent(keyEvent));
assertThat(result.action()).isEqualTo(UiAction.handled(false));
assertThat(result.effects())
- .containsExactly(new UiEffect.ApplyExtensionNavigationKey(KeyEvent.ofChar('j')));
+ .containsExactly(new UiEffect.ApplyExtensionNavigationKey(keyEvent));
assertThat(result.nextState())
.isEqualTo(stateWithFocus(baseState(), FocusTarget.EXTENSION_LIST));
}
+ @Test
+ void extensionInteractionIntentRoutesArrowNavigationKeysThroughEffect() {
+ UiState state = stateWithFocus(baseState(), FocusTarget.EXTENSION_LIST);
+
+ for (KeyCode keyCode : List.of(KeyCode.DOWN, KeyCode.HOME, KeyCode.END)) {
+ KeyEvent keyEvent = KeyEvent.ofKey(keyCode);
+ ReduceResult result =
+ reducer.reduce(state, new UiIntent.ExtensionInteractionIntent(keyEvent));
+
+ assertThat(result.action()).isEqualTo(UiAction.handled(false));
+ assertThat(result.effects())
+ .containsExactly(new UiEffect.ApplyExtensionNavigationKey(keyEvent));
+ }
+ }
+
@Test
void extensionInteractionIntentMovesSearchFocusToListOnDown() {
ReduceResult result =
@@ -626,7 +644,7 @@ void extensionInteractionIntentMovesTopListSelectionBackToSearch() {
UiState.ExtensionView.snapshot(7, 7, 0, false, false, "", "", "", "", true, true));
ReduceResult result =
- reducer.reduce(state, new UiIntent.ExtensionInteractionIntent(KeyEvent.ofChar('k')));
+ reducer.reduce(state, new UiIntent.ExtensionInteractionIntent(vimChar('k')));
assertThat(result.action()).isEqualTo(UiAction.handled(false));
assertThat(result.effects())
@@ -635,6 +653,10 @@ void extensionInteractionIntentMovesTopListSelectionBackToSearch() {
assertThat(result.nextState().statusMessage()).isEqualTo("Focus moved to extensionSearch");
}
+ private static KeyEvent vimChar(char character) {
+ return KeyEvent.ofChar(character, BindingSets.vim());
+ }
+
@Test
void extensionInteractionIntentTogglesSectionHeadersThroughSharedActionPath() {
UiState state =
@@ -762,15 +784,19 @@ void metadataIntentIsIgnoredWhenSelectorHasNoOptions() {
@Test
void metadataIntentWithOptionsProducesSelectorEffect() {
- ReduceResult result =
- reducer.reduce(
- stateWithFocus(baseState(), FocusTarget.BUILD_TOOL),
- new UiIntent.MetadataInputIntent(
- KeyEvent.ofKey(KeyCode.LEFT), FocusTarget.BUILD_TOOL, true));
-
- assertThat(result.action()).isEqualTo(UiAction.handled(false));
- assertThat(result.effects()).hasSize(1);
- assertThat(result.effects().getFirst()).isInstanceOf(UiEffect.ApplyMetadataSelectorKey.class);
+ UiState state = stateWithFocus(baseState(), FocusTarget.BUILD_TOOL);
+
+ for (KeyCode keyCode :
+ List.of(KeyCode.LEFT, KeyCode.UP, KeyCode.RIGHT, KeyCode.DOWN, KeyCode.HOME, KeyCode.END)) {
+ KeyEvent keyEvent = KeyEvent.ofKey(keyCode);
+ ReduceResult result =
+ reducer.reduce(
+ state, new UiIntent.MetadataInputIntent(keyEvent, FocusTarget.BUILD_TOOL, true));
+
+ assertThat(result.action()).isEqualTo(UiAction.handled(false));
+ assertThat(result.effects())
+ .containsExactly(new UiEffect.ApplyMetadataSelectorKey(FocusTarget.BUILD_TOOL, keyEvent));
+ }
}
@Test
@@ -815,12 +841,27 @@ void textInputIntentUsesReducerFocusInsteadOfIntentFocus() {
@Test
void textInputIntentWithUnsupportedKeyIsIgnored() {
+ for (KeyEvent keyEvent :
+ List.of(
+ KeyEvent.ofChar('a', KeyModifiers.CTRL),
+ KeyEvent.ofChar('a', KeyModifiers.ALT),
+ KeyEvent.ofChar((char) 31),
+ KeyEvent.ofChar((char) 127))) {
+ ReduceResult result =
+ reducer.reduce(
+ baseState(), new UiIntent.TextInputIntent(keyEvent, FocusTarget.ARTIFACT_ID));
+
+ assertThat(result.action()).isEqualTo(UiAction.ignored());
+ assertThat(result.effects()).isEmpty();
+ }
+ }
+
+ @Test
+ void textInputIntentWithNavigationKeyIsIgnored() {
ReduceResult result =
reducer.reduce(
- baseState(),
- new UiIntent.TextInputIntent(
- KeyEvent.ofChar('a', dev.tamboui.tui.event.KeyModifiers.CTRL),
- FocusTarget.ARTIFACT_ID));
+ stateWithFocus(baseState(), FocusTarget.ARTIFACT_ID),
+ new UiIntent.TextInputIntent(KeyEvent.ofKey(KeyCode.UP), FocusTarget.ARTIFACT_ID));
assertThat(result.action()).isEqualTo(UiAction.ignored());
assertThat(result.effects()).isEmpty();
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigationTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigationTest.java
index 2c8e3f7b..e3cb35ce 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigationTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/ui/ExtensionCatalogNavigationTest.java
@@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import dev.tamboui.tui.bindings.BindingSets;
import dev.tamboui.tui.event.KeyEvent;
import java.util.List;
import java.util.Set;
@@ -99,21 +100,25 @@ void keyboardNavigationUsesSectionHeadersWhenFocusedAndSelectableRowsOtherwise()
ExtensionCatalogNavigation navigation = new ExtensionCatalogNavigation();
ExtensionCatalogRows rows = rows(false);
- assertThat(navigation.handleNavigationKey(rows, KeyEvent.ofChar('j'))).isTrue();
+ assertThat(navigation.handleNavigationKey(rows, vimChar('j'))).isTrue();
assertThat(navigation.selectedRow()).isEqualTo(1);
assertThat(navigation.isSelectionAtTop(rows)).isTrue();
- assertThat(navigation.handleNavigationKey(rows, KeyEvent.ofChar('G'))).isTrue();
+ assertThat(navigation.handleNavigationKey(rows, vimChar('G'))).isTrue();
assertThat(navigation.selectedRow()).isEqualTo(5);
assertThat(navigation.isSelectionAtTop(rows)).isFalse();
navigation.listState().select(2);
- assertThat(navigation.handleNavigationKey(rows, KeyEvent.ofChar('g'))).isTrue();
+ assertThat(navigation.handleNavigationKey(rows, vimChar('g'))).isTrue();
assertThat(navigation.selectedRow()).isZero();
assertThat(navigation.handleNavigationKey(rows, KeyEvent.ofChar('x'))).isFalse();
}
+ private static KeyEvent vimChar(char character) {
+ return KeyEvent.ofChar(character, BindingSets.vim());
+ }
+
private static ExtensionCatalogRows rows(boolean collapseWeb) {
ExtensionCatalogRows rows = new ExtensionCatalogRows();
ExtensionCatalogFilters filters = new ExtensionCatalogFilters("");
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/InputEffectsTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/InputEffectsTest.java
index 1fdbc1c4..a5eab7f1 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/InputEffectsTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/ui/InputEffectsTest.java
@@ -72,6 +72,55 @@ void cursorMovementDoesNotTriggerSubmitRecoveryOrSearchRefresh() {
assertThat(fixture.callbacks.dispatchedIntents).isEmpty();
}
+ @Test
+ void noOpDeleteDoesNotTriggerSubmitRecoveryOrSearchRefresh() {
+ TestFixture fixture = new TestFixture();
+ String artifactId = fixture.inputStates.get(FocusTarget.ARTIFACT_ID).text();
+
+ List intents =
+ fixture.inputEffects.applyTextInputKey(
+ FocusTarget.ARTIFACT_ID, KeyEvent.ofKey(KeyCode.DELETE));
+
+ assertThat(intents).isEmpty();
+ assertThat(fixture.inputStates.get(FocusTarget.ARTIFACT_ID).text()).isEqualTo(artifactId);
+ assertThat(fixture.callbacks.submitRecoveryCalls).isZero();
+ assertThat(fixture.callbacks.scheduledQuery).isNull();
+ assertThat(fixture.callbacks.dispatchedIntents).isEmpty();
+ }
+
+ @Test
+ void unsupportedTextInputKeyDoesNotTriggerSubmitRecoveryOrSearchRefresh() {
+ TestFixture fixture = new TestFixture();
+ fixture.inputStates.remove(FocusTarget.BUILD_TOOL);
+
+ List intents =
+ fixture.inputEffects.applyTextInputKey(FocusTarget.ARTIFACT_ID, KeyEvent.ofKey(KeyCode.UP));
+ List nonTextInputIntents =
+ fixture.inputEffects.applyTextInputKey(FocusTarget.BUILD_TOOL, KeyEvent.ofChar('x'));
+
+ assertThat(intents).isEmpty();
+ assertThat(nonTextInputIntents).isEmpty();
+ assertThat(fixture.callbacks.submitRecoveryCalls).isZero();
+ assertThat(fixture.callbacks.scheduledQuery).isNull();
+ assertThat(fixture.callbacks.dispatchedIntents).isEmpty();
+ }
+
+ @Test
+ void metadataSelectorEffectHandlesVerticalNavigationKeys() {
+ for (KeyCode keyCode : List.of(KeyCode.UP, KeyCode.DOWN)) {
+ TestFixture fixture = new TestFixture();
+
+ List intents =
+ fixture.inputEffects.applyMetadataSelectorKey(
+ FocusTarget.BUILD_TOOL, KeyEvent.ofKey(keyCode));
+
+ assertThat(intents).hasSize(3);
+ assertThat(intents.getFirst()).isInstanceOf(UiIntent.FormStateUpdatedIntent.class);
+ assertThat(intents.get(1)).isInstanceOf(UiIntent.StatusMessageIntent.class);
+ assertThat(intents.get(2)).isInstanceOf(UiIntent.SubmitEditRecoveryIntent.class);
+ }
+ }
+
@Test
void metadataSelectorEffectCyclesBuildToolAndEmitsStatus() {
TestFixture fixture = new TestFixture();
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuStateTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuStateTest.java
index 9859e3a9..137ac488 100644
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuStateTest.java
+++ b/src/test/java/dev/ayagmar/quarkusforge/ui/PostGenerationMenuStateTest.java
@@ -108,14 +108,24 @@ void githubVisibilityMenuReturnsVisibilityCommandsWithoutMutation() {
true, true, 0, 0, UiTextConstants.postGenerationActions(List.of()), null, "cmd", null);
UiIntent.PostGenerationCommand down = menuState.handleKey(view, KeyEvent.ofKey(KeyCode.DOWN));
+ UiIntent.PostGenerationCommand up = menuState.handleKey(view, KeyEvent.ofKey(KeyCode.UP));
UiIntent.PostGenerationCommand escape =
menuState.handleKey(view, KeyEvent.ofKey(KeyCode.ESCAPE));
UiIntent.PostGenerationCommand choosePublic = menuState.handleKey(view, KeyEvent.ofChar('2'));
+ UiIntent.PostGenerationCommand ctrlDigit =
+ menuState.handleKey(view, KeyEvent.ofChar('2', KeyModifiers.CTRL));
+ UiIntent.PostGenerationCommand altDigit =
+ menuState.handleKey(view, KeyEvent.ofChar('2', KeyModifiers.ALT));
+ UiIntent.PostGenerationCommand letter = menuState.handleKey(view, KeyEvent.ofChar('x'));
assertThat(down).isEqualTo(new UiIntent.PostGenerationCommand.MoveGithubVisibilitySelection(1));
+ assertThat(up).isEqualTo(new UiIntent.PostGenerationCommand.MoveGithubVisibilitySelection(-1));
assertThat(escape).isInstanceOf(UiIntent.PostGenerationCommand.CancelGithubVisibility.class);
assertThat(choosePublic)
.isEqualTo(new UiIntent.PostGenerationCommand.SelectGithubVisibilityIndex(1));
+ assertThat(ctrlDigit).isInstanceOf(UiIntent.PostGenerationCommand.Noop.class);
+ assertThat(altDigit).isInstanceOf(UiIntent.PostGenerationCommand.Noop.class);
+ assertThat(letter).isInstanceOf(UiIntent.PostGenerationCommand.Noop.class);
assertThat(view.githubVisibilitySelection()).isZero();
}
}
diff --git a/src/test/java/dev/ayagmar/quarkusforge/ui/UiKeyMatchersTest.java b/src/test/java/dev/ayagmar/quarkusforge/ui/UiKeyMatchersTest.java
deleted file mode 100644
index cae77513..00000000
--- a/src/test/java/dev/ayagmar/quarkusforge/ui/UiKeyMatchersTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package dev.ayagmar.quarkusforge.ui;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import dev.tamboui.tui.event.KeyCode;
-import dev.tamboui.tui.event.KeyEvent;
-import dev.tamboui.tui.event.KeyModifiers;
-import org.junit.jupiter.api.Test;
-
-class UiKeyMatchersTest {
- @Test
- void plainCharMatchesOnlyCharWithoutModifiers() {
- assertThat(UiKeyMatchers.isPlainChar(KeyEvent.ofChar('k'), 'k', 'K')).isTrue();
- assertThat(UiKeyMatchers.isPlainChar(KeyEvent.ofChar('K'), 'k', 'K')).isTrue();
- assertThat(UiKeyMatchers.isPlainChar(KeyEvent.ofChar('k', KeyModifiers.CTRL), 'k', 'K'))
- .isFalse();
- assertThat(UiKeyMatchers.isPlainChar(KeyEvent.ofChar('k', KeyModifiers.ALT), 'k', 'K'))
- .isFalse();
- assertThat(UiKeyMatchers.isPlainChar(KeyEvent.ofKey(KeyCode.UP), 'k', 'K')).isFalse();
- }
-
- @Test
- void vimMovementChecksMatchConfiguredCharacters() {
- assertThat(UiKeyMatchers.isVimUpKey(KeyEvent.ofChar('k'))).isTrue();
- assertThat(UiKeyMatchers.isVimDownKey(KeyEvent.ofChar('j'))).isTrue();
- assertThat(UiKeyMatchers.isVimLeftKey(KeyEvent.ofChar('h'))).isTrue();
- assertThat(UiKeyMatchers.isVimRightKey(KeyEvent.ofChar('l'))).isTrue();
- assertThat(UiKeyMatchers.isVimHomeKey(KeyEvent.ofChar('g'))).isTrue();
- assertThat(UiKeyMatchers.isVimEndKey(KeyEvent.ofChar('G'))).isTrue();
- assertThat(UiKeyMatchers.isVimUpKey(KeyEvent.ofChar('k', KeyModifiers.CTRL))).isFalse();
- }
-
- @Test
- void digitKeyRequiresPlainCharacterDigit() {
- assertThat(UiKeyMatchers.isDigitKey(KeyEvent.ofChar('7'))).isTrue();
- assertThat(UiKeyMatchers.isDigitKey(KeyEvent.ofChar('7', KeyModifiers.CTRL))).isFalse();
- assertThat(UiKeyMatchers.isDigitKey(KeyEvent.ofChar('x'))).isFalse();
- assertThat(UiKeyMatchers.isDigitKey(KeyEvent.ofKey(KeyCode.DOWN))).isFalse();
- }
-}