This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 31c0a7e0fdff CAMEL-23867: camel tui - Add settings dialog
31c0a7e0fdff is described below
commit 31c0a7e0fdff0b6ca3ea391bf6787516d1911aa1
Author: Adriano Machado <[email protected]>
AuthorDate: Thu Jul 9 13:45:18 2026 -0400
CAMEL-23867: camel tui - Add settings dialog
Closes #24545
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Co-Authored-By: Claude Sonnet 5 <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 16 +-
.../ROOT/pages/camel-jbang-configuration.adoc | 10 +-
.../modules/ROOT/pages/camel-jbang-tui.adoc | 27 +-
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../dsl/jbang/core/common/CommandLineHelper.java | 81 +++++-
.../common/CommandLineHelperMigrationTest.java | 185 ++++++++++++
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 31 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 25 ++
.../jbang/core/commands/tui/FolderInputPopup.java | 40 +--
.../dsl/jbang/core/commands/tui/SettingsPopup.java | 315 +++++++++++++++++++++
.../camel/dsl/jbang/core/commands/tui/Theme.java | 15 +-
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../dsl/jbang/core/commands/tui/TuiSettings.java | 101 +++++++
.../dsl/jbang/core/commands/tui/TuiUserConfig.java | 98 +++++++
.../src/main/resources/tui/themes/theme.md | 30 +-
.../core/commands/tui/FolderInputPopupTest.java | 88 ++++++
.../core/commands/tui/SettingsPopupRenderTest.java | 81 ++++++
.../jbang/core/commands/tui/SettingsPopupTest.java | 226 +++++++++++++++
.../jbang/core/commands/tui/TuiSettingsTest.java | 169 +++++++++++
.../jbang/core/commands/tui/TuiUserConfigTest.java | 173 +++++++++++
.../infra/cli/services/CliLocalProcessService.java | 2 +-
21 files changed, 1664 insertions(+), 51 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 0d647fdf0131..ed6963bf53cb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -40,7 +40,21 @@ with empty `nodes`/`edges` arrays if there are no routes to
connect, so its pres
the dump has completed (previously it was silently skipped when there were no
routes).
The Camel TUI (`camel tui`) accepts `--theme=dark` or `--theme=light` to
choose the color palette at startup.
-When omitted, the persisted `camel.tui.theme` value from
`.camel-jbang-user.properties` is used.
+When omitted, the persisted `camel.tui.theme` value from
`.camel-cli.properties` is used.
+
+The Camel TUI now has a *Settings...* entry in the *F2* actions menu for
choosing the theme, the
+starting tab, and the default run-from-folder. These are stored under
`camel.tui.*` keys
+(`camel.tui.theme`, `camel.tui.startTab`, `camel.tui.defaultFolder`) in the
Camel CLI configuration
+file. Each key is read from and written back to the file where it currently
lives: a key present in
+the local `./camel-cli.properties` stays local (project override), while every
other key defaults to
+the global `~/.camel-cli.properties`.
+
+The Camel CLI user configuration file has been renamed from
`camel-jbang-user.properties` to
+`camel-cli.properties` (global `~/.camel-cli.properties`, local
`./camel-cli.properties`). The dot
+convention is unchanged: the global file is a hidden dotfile, the local file
is visible. On first
+run the CLI automatically renames a pre-existing global
`~/.camel-jbang-user.properties` to
+`~/.camel-cli.properties` and a pre-existing local
`./camel-jbang-user.properties` to
+`./camel-cli.properties`; an existing new file is never overwritten.
=== camel-langchain4j-agent
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
index 286e856eed95..f6c9408aa0f6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
@@ -74,11 +74,17 @@ camel run *
Camel CLI uses two configuration files:
-- Global: `~/.camel-jbang-user.properties`
-- Local: `./camel-jbang-user.properties` (takes precedence)
+- Global: `~/.camel-cli.properties`
+- Local: `./camel-cli.properties` (takes precedence)
Commands target global by default. Use `--global=false` for local
configuration.
+NOTE: These files were previously named `.camel-jbang-user.properties`
(global) and
+`camel-jbang-user.properties` (local). On first run the CLI automatically
renames a pre-existing
+global `~/.camel-jbang-user.properties` to `~/.camel-cli.properties` and a
pre-existing local
+`./camel-jbang-user.properties` to `./camel-cli.properties`. An existing new
file is never
+overwritten.
+
IMPORTANT: A set version via `camel config set` cannot be combined with
`--camel-version` on the command line.
=== Set, unset, list, and get
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 38e0253bc3cb..c8da5fb018fb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -274,6 +274,7 @@ Press *F2* to open the actions menu with quick access to
common operations:
| Run Doctor | Check your environment: Java version, JBang, Maven, Docker,
port conflicts, disk space.
| Reset Stats | Reset all statistics and metrics for the selected integration.
| Stop All | Stop running integrations and/or infrastructure services.
+| Settings... | Change the theme, the starting tab, and the default
run-from-folder.
| Take Screenshot | Export the current screen as SVG, text, or ANSI art.
| Start/Stop Tape Recording | Record your session as a `.tape` file for demos.
|===
@@ -311,14 +312,36 @@ to switch at runtime.
Pass `--theme=dark` or `--theme=light` on the command line to pick the palette
for
the current session. The CLI value overrides the persisted preference from
-`.camel-jbang-user.properties`; runtime toggles and the config file still
apply on
+`.camel-cli.properties`; runtime toggles and the config file still apply on
later launches when `--theme` is omitted.
The brand orange accent is identical in both themes; status colors (success,
warning, error) and borders adapt for readability on dark and light terminals.
Your choice is remembered: it is saved as `camel.tui.theme` (`dark` or `light`)
-in `.camel-jbang-user.properties` and restored the next time you open the TUI.
+in `.camel-cli.properties` and restored the next time you open the TUI.
+
+== Settings
+
+Open the *F2* actions menu and choose *Settings...* to change TUI preferences
in one place:
+
+* *Theme* -- cycle between *dark* and *light* (applied immediately on save).
+* *Starting Tab* -- the tab shown when the TUI launches; any tab (primary or
under *More*) can be
+ chosen. Defaults to *Overview*.
+* *Default Folder* -- the folder pre-filled in *Run from Folder*. The most
recently used folder
+ still takes precedence; this default is used only when there is no
remembered folder.
+
+Use *↑*/*↓* to move between rows, *Space* (or *←*/*→*) to cycle the theme and
starting tab, type to
+edit the default folder, *Enter* to save, and *Esc* to cancel.
+
+Settings are stored under `camel.tui.*` keys (`camel.tui.theme`,
`camel.tui.startTab`,
+`camel.tui.defaultFolder`) in the Camel CLI configuration file. Each key is
read from and
+written back to the file where it currently lives: a key present in the local
+`./camel-cli.properties` is treated as a project-level override and stays
local, while every
+other key defaults to the global `~/.camel-cli.properties`. This means a
project can
+deliberately pin a starting tab in its local config without redirecting your
personal theme
+into the project file. See xref:camel-jbang-configuration.adoc[Configuration]
for details
+on the global and local files.
== Keyboard Shortcuts
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 53e2bea5afa2..93625b9906df 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -249,6 +249,7 @@ public class CamelJBangMain implements Callable<Integer> {
return new String[] { v };
});
+ CommandLineHelper.migrateLegacyUserConfig(out);
CommandLineHelper.augmentWithUserConfiguration(commandLine);
preExecute(commandLine, args);
int exitCode = commandLine.execute(args);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
index 6e8b56e5e288..44440aba6077 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelper.java
@@ -37,8 +37,10 @@ import picocli.CommandLine;
public final class CommandLineHelper {
private static volatile Path homeDir =
Paths.get(HomeHelper.resolveHomeDir());
- public static final String USER_CONFIG = ".camel-jbang-user.properties";
- public static final String LOCAL_USER_CONFIG =
"camel-jbang-user.properties";
+ public static final String USER_CONFIG = ".camel-cli.properties";
+ public static final String LOCAL_USER_CONFIG = "camel-cli.properties";
+ public static final String LEGACY_USER_CONFIG =
".camel-jbang-user.properties";
+ public static final String LEGACY_LOCAL_USER_CONFIG =
"camel-jbang-user.properties";
public static final String CAMEL_DIR = ".camel";
public static final String CAMEL_JBANG_WORK_DIR = ".camel-jbang";
@@ -46,6 +48,79 @@ public final class CommandLineHelper {
super();
}
+ /**
+ * Migrates any pre-existing legacy user configuration file ({@value
#LEGACY_USER_CONFIG}) to the new name
+ * ({@value #USER_CONFIG}), for both the user home directory and the
current working directory.
+ * <p>
+ * The migration is idempotent and best-effort: it renames when only the
legacy file exists, merges non-conflicting
+ * legacy keys into the current file when the current file is newer or
same-age, and leaves both files untouched
+ * when the legacy file is newer.
+ */
+ public static void migrateLegacyUserConfig() {
+ migrateLegacyUserConfig(new Printer.SystemOutPrinter());
+ }
+
+ public static void migrateLegacyUserConfig(Printer printer) {
+ migrateLegacyUserConfig(homeDir.resolve(LEGACY_USER_CONFIG),
homeDir.resolve(USER_CONFIG), printer);
+ migrateLegacyUserConfig(Paths.get(LEGACY_LOCAL_USER_CONFIG),
Paths.get(LOCAL_USER_CONFIG), printer);
+ }
+
+ private static void migrateLegacyUserConfig(Path legacy, Path current,
Printer printer) {
+ try {
+ if (!Files.isRegularFile(legacy)) {
+ return;
+ }
+ if (!Files.exists(current)) {
+ Files.move(legacy, current);
+ printer.println("Migrated legacy settings: " +
legacy.getFileName() + " -> " + current.getFileName());
+ return;
+ }
+
+ long legacyModified = Files.getLastModifiedTime(legacy).toMillis();
+ long currentModified =
Files.getLastModifiedTime(current).toMillis();
+ if (currentModified < legacyModified) {
+ printer.println("Note: legacy settings file " +
legacy.getFileName()
+ + " is newer than " + current.getFileName()
+ + ". Both files left unchanged. Remove the
legacy file manually if no longer needed.");
+ return;
+ }
+
+ Properties legacyProps = new OrderedProperties();
+ try (InputStream is = Files.newInputStream(legacy)) {
+ legacyProps.load(is);
+ }
+ Properties currentProps = new OrderedProperties();
+ try (InputStream is = Files.newInputStream(current)) {
+ currentProps.load(is);
+ }
+ boolean merged = false;
+ for (String key : legacyProps.stringPropertyNames()) {
+ if (currentProps.getProperty(key) == null) {
+ currentProps.setProperty(key,
legacyProps.getProperty(key));
+ merged = true;
+ }
+ }
+ if (merged) {
+ try (OutputStream os = Files.newOutputStream(current)) {
+ currentProps.store(os, null);
+ }
+ printer.println("Merged legacy settings from " +
legacy.getFileName() + " into " + current.getFileName());
+ }
+ Files.delete(legacy);
+ printer.println("Removed legacy settings file: " +
legacy.getFileName());
+ } catch (IOException e) {
+ // best-effort: a migration hiccup must never block the CLI
+ }
+ }
+
+ /**
+ * Whether a local user configuration file ({@value #LOCAL_USER_CONFIG})
exists in the current working directory.
+ * Callers use this to decide whether to read/write the local file instead
of the user home file.
+ */
+ public static boolean hasLocalUserConfig() {
+ return Files.exists(Paths.get(LOCAL_USER_CONFIG));
+ }
+
public static void augmentWithUserConfiguration(CommandLine commandLine) {
Path file = getUserConfigurationFile();
if (Files.isRegularFile(file) && Files.exists(file)) {
@@ -167,7 +242,7 @@ public final class CommandLineHelper {
@Override
public String defaultValue(CommandLine.Model.ArgSpec argSpec) throws
Exception {
- // all plugin commands should not support default values from
camel-jbang-user.properties
+ // all plugin commands should not support default values from the
user configuration file
CommandLine.Model.CommandSpec parent = argSpec.command().parent();
if (parent != null && parent.name().equals("plugin")) {
return null;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelperMigrationTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelperMigrationTest.java
new file mode 100644
index 000000000000..e4e8cb996477
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CommandLineHelperMigrationTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the one-time migration of the legacy user configuration file
({@value CommandLineHelper#LEGACY_USER_CONFIG})
+ * to the current name ({@value CommandLineHelper#USER_CONFIG}). The migration
must be idempotent, must preserve the
+ * current file on conflicts, and must be a no-op when nothing is present.
+ */
+@Isolated
+class CommandLineHelperMigrationTest {
+
+ private String originalHome;
+
+ @AfterEach
+ void tearDown() {
+ if (originalHome != null) {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+ }
+
+ private void useHome(Path dir) {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(dir.toString());
+ }
+
+ @Test
+ void renamesLegacyHomeFileToNewName(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+ Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(legacy, "camel.tui.theme=light\n");
+
+ CommandLineHelper.migrateLegacyUserConfig();
+
+ assertFalse(Files.exists(legacy), "legacy file should have been
renamed away");
+ assertTrue(Files.exists(current), "new file should exist after
migration");
+ assertEquals("camel.tui.theme=light\n", Files.readString(current),
"content must be preserved verbatim");
+ }
+
+ @Test
+ void mergesLegacyIntoCurrentWhenCurrentIsNewerOrSameAge(@TempDir Path
tempDir) throws Exception {
+ useHome(tempDir);
+ Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+ Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(legacy, "camel.tui.theme=light\nold.key=legacy\n");
+ Files.writeString(current, "camel.tui.theme=dark\n");
+
+ CommandLineHelper.migrateLegacyUserConfig();
+
+ assertFalse(Files.exists(legacy), "legacy file should be removed after
merge");
+ String content = Files.readString(current);
+ assertTrue(content.contains("camel.tui.theme=dark"), "current file
must win on conflicting keys");
+ assertTrue(content.contains("old.key=legacy"), "legacy-only keys must
be merged into the current file");
+ }
+
+ @Test
+ void leavesLegacyAloneWhenItIsNewer(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+ Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(current, "camel.tui.theme=dark\n");
+ Files.setLastModifiedTime(current, FileTime.fromMillis(1000));
+ Files.writeString(legacy, "camel.tui.theme=light\nold.key=legacy\n");
+ Files.setLastModifiedTime(legacy, FileTime.fromMillis(5000));
+
+ CommandLineHelper.migrateLegacyUserConfig();
+
+ assertTrue(Files.exists(legacy), "legacy file must remain when it is
newer");
+ assertTrue(Files.exists(current), "current file must remain when it is
older");
+ assertEquals("camel.tui.theme=light\nold.key=legacy\n",
Files.readString(legacy));
+ assertEquals("camel.tui.theme=dark\n", Files.readString(current));
+ }
+
+ @Test
+ void printsDiagnosticMessages(@TempDir Path tempDir) throws Exception {
+ useHome(tempDir);
+ Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+ Files.writeString(legacy, "camel.tui.theme=light\n");
+ CapturingPrinter printer = new CapturingPrinter();
+
+ CommandLineHelper.migrateLegacyUserConfig(printer);
+
+ assertTrue(printer.lines.stream().anyMatch(line ->
line.contains("Migrated legacy settings")));
+ }
+
+ @Test
+ void noOpWhenNeitherFilePresent(@TempDir Path tempDir) {
+ useHome(tempDir);
+
+ assertDoesNotThrow(() -> CommandLineHelper.migrateLegacyUserConfig());
+
assertFalse(Files.exists(tempDir.resolve(CommandLineHelper.USER_CONFIG)),
+ "no new file should be created when there is nothing to
migrate");
+ }
+
+ @Test
+ void renamesLegacyLocalFileToNewName(@TempDir Path tempDir) throws
Exception {
+ // the historical local file lived in the working directory under the
visible (no leading dot) name
+ useHome(tempDir);
+ Path legacy = Path.of(CommandLineHelper.LEGACY_LOCAL_USER_CONFIG);
+ Path current = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ Files.deleteIfExists(legacy);
+ Files.deleteIfExists(current);
+ Files.writeString(legacy, "camel.tui.theme=light\n");
+ try {
+ CommandLineHelper.migrateLegacyUserConfig();
+
+ assertFalse(Files.exists(legacy), "legacy local file should have
been renamed away");
+ assertTrue(Files.exists(current), "new local file should exist
after migration");
+ assertEquals("camel.tui.theme=light\n", Files.readString(current),
"content must be preserved verbatim");
+ } finally {
+ Files.deleteIfExists(legacy);
+ Files.deleteIfExists(current);
+ }
+ }
+
+ @Test
+ void isIdempotentOnRepeatedRuns(@TempDir Path tempDir) throws Exception {
+ useHome(tempDir);
+ Path legacy = tempDir.resolve(CommandLineHelper.LEGACY_USER_CONFIG);
+ Path current = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(legacy, "camel.tui.theme=light\n");
+
+ CommandLineHelper.migrateLegacyUserConfig();
+ CommandLineHelper.migrateLegacyUserConfig();
+
+ assertFalse(Files.exists(legacy));
+ assertTrue(Files.exists(current));
+ assertEquals("camel.tui.theme=light\n", Files.readString(current));
+ }
+
+ private static final class CapturingPrinter implements Printer {
+ private final List<String> lines = new ArrayList<>();
+
+ @Override
+ public void println() {
+ lines.add("");
+ }
+
+ @Override
+ public void println(String line) {
+ lines.add(line);
+ }
+
+ @Override
+ public void print(String output) {
+ lines.add(output);
+ }
+
+ @Override
+ public void printf(String format, Object... args) {
+ lines.add(String.format(format, args));
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index eb9ec12a4d07..43da367fb87f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -62,6 +62,7 @@ class ActionsPopup {
STOP_ALL,
SHELL,
SCREEN_SUBMENU,
+ SETTINGS,
MCP_SUBMENU,
BACK,
SCREENSHOT,
@@ -112,6 +113,7 @@ class ActionsPopup {
private final GotoTabPopup gotoTabPopup = new GotoTabPopup();
private final ThemePopup themePopup = new ThemePopup();
+ private final SettingsPopup settingsPopup = new SettingsPopup();
private final DocViewerPopup docViewerPopup = new DocViewerPopup();
private final ExampleBrowserPopup exampleBrowserPopup;
@@ -212,6 +214,11 @@ class ActionsPopup {
void setGotoTabSupport(List<TabRegistry.TabEntry> entries, Runnable
callback) {
gotoTabPopup.setTabEntries(entries, callback);
+ settingsPopup.setTabEntries(entries);
+ }
+
+ void setClearScreenAction(Runnable clearScreen) {
+ settingsPopup.setClearScreen(clearScreen);
}
void setMcpEnabled(
@@ -267,7 +274,7 @@ class ActionsPopup {
Action.SEND_MESSAGE, Action.RUN_EXAMPLE, Action.RUN_FOLDER,
Action.RUN_INFRA, Action.BROWSE_FILES,
Action.STOP_ALL));
flat.add(null);
- flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS,
Action.SCREEN_SUBMENU));
+ flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS,
Action.SCREEN_SUBMENU, Action.SETTINGS));
if (mcpEnabled) {
flat.add(null);
flat.add(Action.MCP_SUBMENU);
@@ -331,7 +338,7 @@ class ActionsPopup {
}
boolean isVisible() {
- return showActionsMenu || themePopup.isVisible() ||
gotoTabPopup.isVisible()
+ return showActionsMenu || themePopup.isVisible() ||
gotoTabPopup.isVisible() || settingsPopup.isVisible()
|| exampleBrowserPopup.isVisible()
|| folderInputPopup.isVisible()
|| runOptionsForm.isVisible()
@@ -411,6 +418,7 @@ class ActionsPopup {
labels.add("Run Doctor");
labels.add("Reset Stats");
labels.add("Screen...");
+ labels.add("Settings...");
if (mcpEnabled) {
labels.add("───");
labels.add("AI & MCP...");
@@ -435,6 +443,7 @@ class ActionsPopup {
currentSubmenu = null;
themePopup.close();
gotoTabPopup.close();
+ settingsPopup.close();
exampleBrowserPopup.close();
folderInputPopup.close();
runOptionsForm.close();
@@ -549,6 +558,13 @@ class ActionsPopup {
}
return true;
}
+ if (settingsPopup.isVisible()) {
+ settingsPopup.handleKeyEvent(ke);
+ if (!settingsPopup.isVisible() && ke.isCancel()) {
+ showActionsMenu = true;
+ }
+ return true;
+ }
if (showActionsMenu) {
if (ke.isCancel()) {
if (currentSubmenu != null) {
@@ -599,6 +615,9 @@ class ActionsPopup {
} else if (action == Action.GOTO_TAB) {
showActionsMenu = false;
gotoTabPopup.open();
+ } else if (action == Action.SETTINGS) {
+ showActionsMenu = false;
+ settingsPopup.open();
} else if (action == Action.SWITCH_INTEGRATION) {
if (hasMultipleIntegrations()) {
showActionsMenu = false;
@@ -783,6 +802,9 @@ class ActionsPopup {
if (gotoTabPopup.isVisible()) {
gotoTabPopup.render(frame, area);
}
+ if (settingsPopup.isVisible()) {
+ settingsPopup.render(frame, area);
+ }
if (showActionsMenu) {
renderActionsMenu(frame, area);
}
@@ -870,6 +892,10 @@ class ActionsPopup {
themePopup.renderFooter(spans);
return;
}
+ if (settingsPopup.isVisible()) {
+ settingsPopup.renderFooter(spans);
+ return;
+ }
if (gotoTabPopup.isVisible()) {
hint(spans, "type", "filter");
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
@@ -940,6 +966,7 @@ class ActionsPopup {
items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCTOR, "Run
Doctor")));
items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.RESET, "Reset
Stats")));
items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.COMPUTER,
"Screen...")));
+ items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.SETTINGS,
"Settings...")));
// Group 3: MCP (conditional)
if (mcpEnabled) {
items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 0ebba077758d..fce2f707c15c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -488,10 +488,13 @@ public class CamelMonitor extends CamelCommand {
actionsPopup.setScheduler(tui.scheduler());
actionsPopup.setResetScreenAction(() -> tui.terminal().clear());
actionsPopup.setThemeRefreshAction(() -> tui.terminal().clear());
+ actionsPopup.setClearScreenAction(() -> tui.terminal().clear());
actionsPopup.setCamelAnimationAction(() ->
playBuiltinAnimation("camel"));
// Preload diagram data if an integration was auto-selected
tabRegistry.routesTab().preloadDiagram();
tabRegistry.diagramTab().preloadDiagram();
+ // Open on the configured starting tab (if any), now that all tab
wiring is in place
+ applyStartingTab();
// Intercept Ctrl+C: quit the TUI cleanly instead of letting
// the JVM tear down the classloader while we're still running
Signal.handle(new Signal("INT"), sig -> tui.quit());
@@ -510,6 +513,28 @@ public class CamelMonitor extends CamelCommand {
return 0;
}
+ /**
+ * Selects the tab configured as the starting tab in {@link TuiSettings}
(if any), resolving the stored programmatic
+ * name against {@link TabRegistry#allTabEntries()}. Unset, unknown, or
the default "Overview" leaves the initial
+ * {@code TAB_OVERVIEW} selection untouched. Reuses the same selection
logic as the Go-to callback.
+ */
+ private void applyStartingTab() {
+ String startTab = TuiSettings.load().getStartTab();
+ if (startTab == null || startTab.isBlank() ||
"Overview".equals(startTab)) {
+ return;
+ }
+ for (TabRegistry.TabEntry entry : tabRegistry.allTabEntries()) {
+ if (entry.name().equals(startTab)) {
+ if (entry.moreIndex() >= 0) {
+ tabRegistry.selectMoreTab(entry.moreIndex());
+ } else {
+ tabRegistry.handleTabKey(entry.tabIndex(), ctx,
dataService);
+ }
+ return;
+ }
+ }
+ }
+
// ---- Event Handling ----
private boolean handleEvent(Event event, TuiRunner runner) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
index 6cd85a92e040..a0f04643c371 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
@@ -37,9 +37,7 @@ import dev.tamboui.widgets.block.Borders;
import dev.tamboui.widgets.input.TextInput;
import dev.tamboui.widgets.input.TextInputState;
import dev.tamboui.widgets.paragraph.Paragraph;
-import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
-import org.apache.camel.dsl.jbang.core.common.Printer;
class FolderInputPopup {
@@ -99,6 +97,10 @@ class FolderInputPopup {
return detectedPomPath;
}
+ String getInputText() {
+ return inputState != null ? inputState.text() : null;
+ }
+
void clearSelection() {
selectedFolder = null;
detectedPomPath = null;
@@ -111,10 +113,14 @@ class FolderInputPopup {
void open() {
showInput = true;
String lastFolder = loadLastFolder();
- if (lastFolder == null) {
- lastFolder = System.getProperty("user.dir");
+ String initialFolder;
+ if (lastFolder != null) {
+ initialFolder = lastFolder;
+ } else {
+ String defaultFolder = TuiSettings.load().getDefaultFolder();
+ initialFolder = defaultFolder != null ? defaultFolder :
System.getProperty("user.dir");
}
- inputState = new TextInputState(lastFolder != null ? lastFolder : "");
+ inputState = new TextInputState(initialFolder);
historyIndex = -1;
folderBrowser.setOnSelect(path -> {
inputState = new TextInputState(path);
@@ -290,7 +296,10 @@ class FolderInputPopup {
} else if (ke.isEnd()) {
inputState.moveCursorToEnd();
} else if (ke.code() == KeyCode.CHAR) {
- inputState.insert(ke.string().charAt(0));
+ char ch = ke.string().charAt(0);
+ if (ch >= 0x20 && ch != 0x7F) {
+ inputState.insert(ch);
+ }
}
}
@@ -362,27 +371,18 @@ class FolderInputPopup {
}
private static String loadLastFolder() {
- String[] holder = { null };
try {
- CommandLineHelper.loadProperties(props -> {
- holder[0] = props.getProperty(PROP_LAST_FOLDER);
- });
+ return TuiUserConfig.read(PROP_LAST_FOLDER);
} catch (RuntimeException e) {
- // ignore
+ return null;
}
- return holder[0];
}
private static void persistLastFolder(String folder) {
try {
- CommandLineHelper.createPropertyFile(false);
- CommandLineHelper.loadProperties(props -> {
- props.setProperty(PROP_LAST_FOLDER, folder);
- CommandLineHelper.storeProperties(props,
- new Printer.QuietPrinter(new
Printer.SystemOutPrinter()), false);
- });
- } catch (Exception e) {
- // ignore
+ TuiUserConfig.write(PROP_LAST_FOLDER, folder);
+ } catch (RuntimeException e) {
+ // best-effort: a persist failure must not disrupt the TUI
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
new file mode 100644
index 000000000000..d601a21f58d7
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -0,0 +1,315 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
+
+/**
+ * Central settings dialog for the Camel TUI. Exposes user preferences (theme,
starting tab, default run-from-folder)
+ * backed by {@link TuiSettings}. Loading, mutating and saving go through a
single {@code TuiSettings} object so all
+ * {@code camel.tui.*} keys are written together. Cycling the theme previews
it instantly (like {@link ThemePopup}),
+ * Enter persists the previewed theme, Esc reverts it; the starting tab is
applied on the next TUI launch.
+ */
+class SettingsPopup {
+
+ private static final int ROW_THEME = 0;
+ private static final int ROW_START_TAB = 1;
+ private static final int ROW_FOLDER = 2;
+ private static final int ROW_COUNT = 3;
+
+ private boolean visible;
+ private int selectedRow;
+
+ private TuiSettings settings;
+ private int themeIndex;
+ private int startTabIndex;
+ private TextInputState folderInput;
+ private List<String> tabNames = new ArrayList<>();
+
+ private List<TabRegistry.TabEntry> tabEntries;
+ private Runnable clearScreen;
+
+ void setTabEntries(List<TabRegistry.TabEntry> tabEntries) {
+ this.tabEntries = tabEntries;
+ }
+
+ void setClearScreen(Runnable clearScreen) {
+ this.clearScreen = clearScreen;
+ }
+
+ boolean isVisible() {
+ return visible;
+ }
+
+ void open() {
+ settings = TuiSettings.load();
+
+ List<String> themeIds = ThemeMode.ids();
+ String currentTheme = settings.getThemeId() != null ?
settings.getThemeId() : Theme.mode();
+ themeIndex = Math.max(0, themeIds.indexOf(currentTheme));
+
+ tabNames = new ArrayList<>();
+ if (tabEntries != null) {
+ for (TabRegistry.TabEntry entry : tabEntries) {
+ tabNames.add(entry.name());
+ }
+ }
+ String currentStartTab = settings.getStartTab() != null ?
settings.getStartTab() : "Overview";
+ int idx = tabNames.indexOf(currentStartTab);
+ startTabIndex = idx >= 0 ? idx : 0;
+
+ folderInput = new TextInputState(settings.getDefaultFolder() != null ?
settings.getDefaultFolder() : "");
+ selectedRow = ROW_THEME;
+ visible = true;
+ }
+
+ void close() {
+ if (visible) {
+ Theme.revertPreview();
+ refreshTheme();
+ }
+ visible = false;
+ }
+
+ boolean handleKeyEvent(KeyEvent ke) {
+ if (!visible) {
+ return false;
+ }
+ if (ke.isCancel()) {
+ Theme.revertPreview();
+ refreshTheme();
+ visible = false;
+ return true;
+ }
+ if (ke.isConfirm()) {
+ save();
+ return true;
+ }
+ if (ke.isUp() || ke.isFocusPrevious()) {
+ selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
+ return true;
+ }
+ if (ke.isDown() || ke.isFocusNext()) {
+ selectedRow = (selectedRow + 1) % ROW_COUNT;
+ return true;
+ }
+ if (selectedRow == ROW_THEME) {
+ int count = ThemeMode.ids().size();
+ int previous = themeIndex;
+ if (ke.isChar(' ') || ke.isRight()) {
+ themeIndex = (themeIndex + 1) % count;
+ } else if (ke.isLeft()) {
+ themeIndex = (themeIndex - 1 + count) % count;
+ }
+ if (themeIndex != previous) {
+ Theme.preview(ThemeMode.ids().get(themeIndex));
+ refreshTheme();
+ }
+ return true;
+ }
+ if (selectedRow == ROW_START_TAB) {
+ if (!tabNames.isEmpty()) {
+ if (ke.isChar(' ') || ke.isRight()) {
+ startTabIndex = (startTabIndex + 1) % tabNames.size();
+ } else if (ke.isLeft()) {
+ startTabIndex = (startTabIndex - 1 + tabNames.size()) %
tabNames.size();
+ }
+ }
+ return true;
+ }
+ if (selectedRow == ROW_FOLDER) {
+ handleTextInput(ke, folderInput);
+ return true;
+ }
+ return true;
+ }
+
+ private void save() {
+ String selectedThemeId = ThemeMode.ids().get(themeIndex);
+ settings.setThemeId(selectedThemeId);
+ if (!tabNames.isEmpty()) {
+ settings.setStartTab(tabNames.get(startTabIndex));
+ }
+
settings.setDefaultFolder(stripControlChars(folderInput.text().trim()));
+ settings.save();
+ if (Theme.mode().equals(selectedThemeId)) {
+ // Already active via live preview (or unchanged): just persist
and clear the preview marker.
+ Theme.confirmPreview();
+ } else {
+ // Row was never touched but disk/session state disagrees (e.g. a
--theme CLI override); force it.
+ Theme.applyStartupMode(selectedThemeId);
+ }
+ refreshTheme();
+ visible = false;
+ }
+
+ void render(Frame frame, Rect area) {
+ int popupW = Math.min(64, area.width() - 4);
+ int popupH = 2 + ROW_COUNT;
+ int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+ int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
+ Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Settings ")
+ .build();
+ frame.renderWidget(block, popup);
+
+ int innerX = popup.left() + 2;
+ int innerW = popup.width() - 4;
+ int labelW = 16;
+ int fieldW = innerW - labelW;
+ int rowY = popup.top() + 1;
+
+ renderLabel(frame, innerX, rowY, labelW, "Theme:", selectedRow ==
ROW_THEME);
+ renderValue(frame, innerX + labelW, rowY, fieldW,
ThemeMode.values()[themeIndex].label(), selectedRow == ROW_THEME);
+ rowY++;
+
+ renderLabel(frame, innerX, rowY, labelW, "Starting Tab:", selectedRow
== ROW_START_TAB);
+ renderValue(frame, innerX + labelW, rowY, fieldW, currentTabLabel(),
selectedRow == ROW_START_TAB);
+ rowY++;
+
+ renderLabel(frame, innerX, rowY, labelW, "Default Folder:",
selectedRow == ROW_FOLDER);
+ renderFolder(frame, innerX + labelW, rowY, fieldW, selectedRow ==
ROW_FOLDER);
+ }
+
+ void renderFooter(List<Span> spans) {
+ hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+ if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB) {
+ hint(spans, "Space", "cycle");
+ }
+ hint(spans, "Enter", "save");
+ hintLast(spans, "Esc", "cancel");
+ }
+
+ // ---- Helpers ----
+
+ private void refreshTheme() {
+ if (clearScreen != null) {
+ clearScreen.run();
+ }
+ }
+
+ private String currentTabLabel() {
+ if (tabEntries == null || tabEntries.isEmpty()) {
+ return "Overview";
+ }
+ TabRegistry.TabEntry entry = tabEntries.get(Math.min(startTabIndex,
tabEntries.size() - 1));
+ return entry.icon() + " " + entry.name();
+ }
+
+ private void handleTextInput(KeyEvent ke, TextInputState active) {
+ if (ke.isDeleteBackward()) {
+ active.deleteBackward();
+ } else if (ke.isDeleteForward()) {
+ active.deleteForward();
+ } else if (ke.isLeft()) {
+ active.moveCursorLeft();
+ } else if (ke.isRight()) {
+ active.moveCursorRight();
+ } else if (ke.isHome()) {
+ active.moveCursorToStart();
+ } else if (ke.isEnd()) {
+ active.moveCursorToEnd();
+ } else if (ke.code() == KeyCode.CHAR) {
+ char ch = ke.string().charAt(0);
+ if (ch >= 0x20 && ch != 0x7F) {
+ active.insert(ch);
+ }
+ }
+ }
+
+ private static String stripControlChars(String s) {
+ if (s == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ if (ch >= 0x20 && ch != 0x7F) {
+ sb.append(ch);
+ }
+ }
+ return sb.toString();
+ }
+
+ private void renderLabel(Frame frame, int x, int y, int w, String label,
boolean selected) {
+ Style style = selected ? Style.EMPTY.bold() : Style.EMPTY.dim();
+ frame.renderWidget(Paragraph.from(Line.from(Span.styled(label,
style))), new Rect(x, y, w, 1));
+ }
+
+ private void renderValue(Frame frame, int x, int y, int w, String text,
boolean selected) {
+ Style style = selected ? Style.EMPTY.bold() : Style.EMPTY;
+ frame.renderWidget(Paragraph.from(Line.from(Span.styled("[" + text +
"]", style))), new Rect(x, y, w, 1));
+ }
+
+ private void renderFolder(Frame frame, int x, int y, int w, boolean
active) {
+ Rect area = new Rect(x, y, w, 1);
+ if (active) {
+ TextInput textInput = TextInput.builder()
+ .cursorStyle(Style.EMPTY.reversed())
+ .placeholder("/path/to/folder")
+ .build();
+ frame.renderStatefulWidget(textInput, area, folderInput);
+ } else {
+ String text = folderInput.text();
+ Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(text.isEmpty() ? "(none)" : text, style))),
area);
+ }
+ }
+
+ // ---- Test accessors ----
+
+ int selectedRow() {
+ return selectedRow;
+ }
+
+ String selectedThemeId() {
+ return ThemeMode.ids().get(themeIndex);
+ }
+
+ String selectedStartTab() {
+ return tabNames.isEmpty() ? null : tabNames.get(startTabIndex);
+ }
+
+ String folderText() {
+ return folderInput != null ? folderInput.text() : "";
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
index 2cfa6dc25ee0..a91368c54a89 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/Theme.java
@@ -31,8 +31,6 @@ import dev.tamboui.css.Styleable;
import dev.tamboui.css.engine.StyleEngine;
import dev.tamboui.style.Color;
import dev.tamboui.style.Style;
-import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
-import org.apache.camel.dsl.jbang.core.common.Printer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -524,9 +522,7 @@ public final class Theme {
private static boolean tryLoadPersistedMode(ThemeMode[] result) {
try {
- CommandLineHelper.loadProperties(props -> {
- ThemeMode.parse(props.getProperty(PROP_KEY)).ifPresent(parsed
-> result[0] = parsed);
- });
+ ThemeMode.parse(TuiUserConfig.read(PROP_KEY)).ifPresent(parsed ->
result[0] = parsed);
return true;
} catch (RuntimeException ex) {
logPersistedReadFallbackOnce(ex);
@@ -536,13 +532,8 @@ public final class Theme {
private static void persist(ThemeMode newMode) {
try {
- CommandLineHelper.createPropertyFile(false);
- CommandLineHelper.loadProperties(props -> {
- props.setProperty(PROP_KEY, newMode.id());
- CommandLineHelper.storeProperties(props,
- new Printer.QuietPrinter(new
Printer.SystemOutPrinter()), false);
- });
- } catch (Exception ex) {
+ TuiUserConfig.write(PROP_KEY, newMode.id());
+ } catch (RuntimeException ex) {
logPersistedWriteFallbackOnce(ex);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 6c89c4ce9f46..048d61f1ae0a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -86,6 +86,7 @@ final class TuiIcons {
static final String KEY = "🔑";
static final String TIP = "💡";
static final String COMPUTER = "💻";
+ static final String SETTINGS = "🧰";
static final String HELP = "❔";
static final String THINKING = "🤔";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
new file mode 100644
index 000000000000..784bd4fb196e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+/**
+ * User-facing preferences for the Camel TUI, persisted under {@code
camel.tui.*} keys in the CLI user configuration
+ * files. The object is loaded as a whole ({@link #load()}), mutated in
memory, and written back ({@link #save()}),
+ * giving every setting one place to live and future settings a natural home.
+ * <p>
+ * Persistence goes through {@link TuiUserConfig}, which applies per-key
precedence between the local and global config
+ * files: a key a project has deliberately committed to the local file is read
from and written back to the local file,
+ * while a user's personal preferences default to the global home file.
+ */
+final class TuiSettings {
+
+ static final String PROP_THEME = "camel.tui.theme";
+ static final String PROP_START_TAB = "camel.tui.startTab";
+ static final String PROP_DEFAULT_FOLDER = "camel.tui.defaultFolder";
+
+ private String themeId;
+ private String startTab;
+ private String defaultFolder;
+
+ String getThemeId() {
+ return themeId;
+ }
+
+ void setThemeId(String themeId) {
+ this.themeId = themeId;
+ }
+
+ String getStartTab() {
+ return startTab;
+ }
+
+ void setStartTab(String startTab) {
+ this.startTab = startTab;
+ }
+
+ String getDefaultFolder() {
+ return defaultFolder;
+ }
+
+ void setDefaultFolder(String defaultFolder) {
+ this.defaultFolder = defaultFolder;
+ }
+
+ /**
+ * Loads the current settings, resolving each key with per-key
local/global precedence via {@link TuiUserConfig}.
+ * Unset keys yield {@code null} fields; a read failure yields an object
with {@code null} fields rather than
+ * throwing, matching the best-effort behavior elsewhere in the TUI.
+ */
+ static TuiSettings load() {
+ TuiSettings settings = new TuiSettings();
+ try {
+ settings.themeId = trimToNull(TuiUserConfig.read(PROP_THEME));
+ settings.startTab = trimToNull(TuiUserConfig.read(PROP_START_TAB));
+ settings.defaultFolder =
trimToNull(TuiUserConfig.read(PROP_DEFAULT_FOLDER));
+ } catch (RuntimeException e) {
+ // best-effort: return an object with null fields on read failure
+ }
+ return settings;
+ }
+
+ /**
+ * Writes every field back, routing each key to its home file (local when
it is a project override, otherwise the
+ * global file) via {@link TuiUserConfig}. A blank/{@code null} field
removes its key so the file never carries
+ * empty values. Failures are swallowed so a save hiccup never disrupts
the TUI.
+ */
+ void save() {
+ try {
+ TuiUserConfig.write(PROP_THEME, themeId);
+ TuiUserConfig.write(PROP_START_TAB, startTab);
+ TuiUserConfig.write(PROP_DEFAULT_FOLDER, defaultFolder);
+ } catch (RuntimeException e) {
+ // best-effort: a save failure must not disrupt the TUI
+ }
+ }
+
+ private static String trimToNull(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
new file mode 100644
index 000000000000..5017040e0079
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.Printer;
+
+/**
+ * Central read/write for the Camel TUI's per-user preferences ({@code
camel.tui.*}) across the Camel CLI configuration
+ * files, applying <em>per-key</em> precedence between the local ({@code
./camel-cli.properties}) and global
+ * ({@code ~/.camel-cli.properties}) files.
+ * <p>
+ * A key's home is the file where it currently lives: a key present in the
local file is treated as a project-level
+ * override and is both read from and written back to the local file; every
other key is personal state and is read from
+ * and written to the global file. This keeps a project's deliberately
committed overrides (for example a pinned
+ * starting tab) intact, while never redirecting a user's personal preferences
(theme, last folder, ...) into an
+ * unrelated project's config just because a local file happens to exist for
other CLI settings.
+ * <p>
+ * Note: if the same key exists in <em>both</em> files (an unusual hand-edited
state), the local copy wins for reads and
+ * is the one updated on writes; the stale global copy is left as-is.
+ */
+final class TuiUserConfig {
+
+ private TuiUserConfig() {
+ }
+
+ /**
+ * Reads a single {@code camel.tui.*} key. Returns the local value when
the key is present in the local file,
+ * otherwise the global value, otherwise {@code null}. Propagates a {@link
RuntimeException} on an I/O failure so
+ * the caller can decide whether to fall back or log.
+ */
+ static String read(String key) {
+ boolean local = livesInLocal(key);
+ String[] holder = { null };
+ CommandLineHelper.loadProperties(props -> holder[0] =
props.getProperty(key), local);
+ return holder[0];
+ }
+
+ /**
+ * Writes a single {@code camel.tui.*} key back to its home file (the
local file when the key is already a local
+ * override, otherwise the global file). A blank or {@code null} value
removes the key. Other keys in the target
+ * file are preserved. Propagates a {@link RuntimeException} on failure so
the caller can log.
+ */
+ static void write(String key, String value) {
+ boolean local = livesInLocal(key);
+ try {
+ CommandLineHelper.createPropertyFile(local);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ String trimmed = trimToNull(value);
+ CommandLineHelper.loadProperties(props -> {
+ if (trimmed == null) {
+ props.remove(key);
+ } else {
+ props.setProperty(key, trimmed);
+ }
+ CommandLineHelper.storeProperties(props,
+ new Printer.QuietPrinter(new Printer.SystemOutPrinter()),
local);
+ }, local);
+ }
+
+ private static boolean livesInLocal(String key) {
+ if (!CommandLineHelper.hasLocalUserConfig()) {
+ return false;
+ }
+ boolean[] present = { false };
+ // Use getProperty (overridden by OrderedProperties) instead of
containsKey
+ // (inherited from Hashtable and always empty in OrderedProperties).
+ CommandLineHelper.loadProperties(props -> present[0] =
props.getProperty(key) != null, true);
+ return present[0];
+ }
+
+ private static String trimToNull(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
index ce3a4479c4a0..027afa416e1a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/themes/theme.md
@@ -1,7 +1,7 @@
# Camel TUI Theme Reference
This document describes the CSS token system used by the Camel TUI.
-Every `.tcss` theme file must define all 27 tokens listed below.
+Every `.tcss` theme file must define all 28 tokens listed below.
Missing tokens cause a startup validation error.
## File Format
@@ -52,13 +52,14 @@ $brand: #F69123;
| `info` | fg | Informational accent (counts, prompts) |
| `notice` | fg | Secondary accent, typically purple (e.g., TRACE log level) |
-### Content (3 tokens)
+### Content (4 tokens)
| Token | Type | Purpose |
|-------|------|---------|
| `label` | fg | Field labels, section headers, key hints, table headers |
| `change` | fg | Changed-value indicator in trace diffs |
| `search-match` | fg+bg | Search/find match highlight |
+| `mnemonic` | fg+bold+underline | Keyboard shortcut mnemonic in tab headers
and menus |
### Diagram (8 tokens)
@@ -66,12 +67,12 @@ $brand: #F69123;
|-------|------|---------|
| `diagram-border` | fg | Box-drawing borders in route diagrams |
| `diagram-id` | fg | Route ID text |
-| `diagram-from` | fg | "from" EIP nodes |
-| `diagram-to` | fg | "to", "enrich", "marshal", "transform" nodes |
-| `diagram-choice` | fg | "choice", "when", "otherwise" nodes |
-| `diagram-action` | fg | "bean", "process", "log", "script" nodes |
-| `diagram-eip` | fg | Routing EIPs (split, aggregate, multicast, etc.) |
-| `diagram-default` | fg | Fallback for unknown EIP types |
+| `diagram-from` | fg | `from` EIP nodes |
+| `diagram-to` | fg | `to`, `enrich`, `marshal`, `transform` nodes |
+| `diagram-choice` | fg | `choice`, `when`, `otherwise` nodes |
+| `diagram-action` | fg | `bean`, `process`, `log`, `script` nodes |
+| `diagram-eip` | fg | Routing EIPs (`split`, `aggregate`, `multicast`, etc.) |
+| `diagram-default` | fg | Fallback for unspecified EIP types |
## Design Guidelines
@@ -96,3 +97,16 @@ When creating a new theme:
|-------|------|-------------|
| `dark` | `dark.tcss` | VS Code-inspired dark palette (default) |
| `light` | `light.tcss` | GitHub-inspired light palette |
+| `dracula` | `dracula.tcss` | Dracula purple-and-pink-on-dark palette |
+| `nord` | `nord.tcss` | Nord arctic, bluish dark palette |
+| `solarized-dark` | `solarized-dark.tcss` | Solarized dark palette |
+| `solarized-light` | `solarized-light.tcss` | Solarized light palette |
+| `gruvbox-dark` | `gruvbox-dark.tcss` | Gruvbox retro-groove dark palette |
+| `catppuccin-mocha` | `catppuccin-mocha.tcss` | Catppuccin Mocha pastel dark
palette |
+| `catppuccin-latte` | `catppuccin-latte.tcss` | Catppuccin Latte pastel light
palette |
+| `tokyo-night` | `tokyo-night.tcss` | Tokyo Night neon-on-dark palette |
+| `rose-pine` | `rose-pine.tcss` | Rosé Pine muted dark palette |
+| `kanagawa` | `kanagawa.tcss` | Kanagawa Japanese-wave-inspired dark palette |
+| `everforest` | `everforest.tcss` | Everforest warm, green-forest dark
palette |
+| `monochrome` | `monochrome.tcss` | Grayscale palette, no color, brightness
only |
+| `crt` | `crt.tcss` | Retro green-phosphor terminal palette |
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopupTest.java
new file mode 100644
index 000000000000..95f2fdc9400f
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopupTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Verifies the pre-filled folder precedence of {@link
FolderInputPopup#open()}. The documented contract is that the
+ * most recently used folder wins, the configured default folder is used only
when there is no remembered folder, and
+ * {@code user.dir} is the final fallback.
+ */
+@Isolated
+class FolderInputPopupTest {
+
+ private String originalHome;
+
+ @AfterEach
+ void tearDown() {
+ if (originalHome != null) {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+ }
+
+ private void useHome(Path dir) {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(dir.toString());
+ }
+
+ @Test
+ void lastFolderWinsOverDefaultFolder(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
+
"camel.tui.defaultFolder=/opt/default\ncamel.tui.lastFolder=/home/user/recent\n");
+
+ FolderInputPopup popup = new FolderInputPopup(null);
+ popup.open();
+
+ assertEquals("/home/user/recent", popup.getInputText(),
+ "the most recently used folder must take precedence over the
configured default");
+ }
+
+ @Test
+ void defaultFolderUsedWhenNoLastFolder(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
+ "camel.tui.defaultFolder=/opt/default\n");
+
+ FolderInputPopup popup = new FolderInputPopup(null);
+ popup.open();
+
+ assertEquals("/opt/default", popup.getInputText(),
+ "the configured default folder must be used when there is no
remembered folder");
+ }
+
+ @Test
+ void fallsBackToUserDirWhenNothingConfigured(@TempDir Path tempDir) {
+ useHome(tempDir);
+
+ FolderInputPopup popup = new FolderInputPopup(null);
+ popup.open();
+
+ assertEquals(System.getProperty("user.dir"), popup.getInputText(),
+ "user.dir must be the final fallback when neither a last
folder nor a default is set");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
new file mode 100644
index 000000000000..a61d9305461e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Renders {@link SettingsPopup} into a virtual buffer and asserts the dialog
shows its title and all three setting
+ * rows, mirroring the other {@code *RenderTest} classes in this package.
+ */
+@Isolated
+class SettingsPopupRenderTest {
+
+ private String originalHome;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ }
+
+ @AfterEach
+ void tearDown() {
+ Theme.resetForTesting();
+ if (originalHome != null) {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+ }
+
+ private void useHome(Path dir) {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(dir.toString());
+ }
+
+ @Test
+ void rendersTitleAndAllThreeSettingRows(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(List.of(
+ new TabRegistry.TabEntry("🐪", "Overview", "overview", "1", 0,
-1),
+ new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6,
-1)));
+ popup.open();
+
+ Rect area = new Rect(0, 0, 80, 20);
+ Buffer buffer = Buffer.empty(area);
+ Frame frame = Frame.forTesting(buffer);
+ popup.render(frame, area);
+ String rendered = TuiTestHelper.bufferToString(buffer);
+
+ assertTrue(rendered.contains("Settings"), "the popup title should be
shown");
+ assertTrue(rendered.contains("Theme"), "the Theme row should be
shown");
+ assertTrue(rendered.contains("Starting Tab"), "the Starting Tab row
should be shown");
+ assertTrue(rendered.contains("Default Folder"), "the Default Folder
row should be shown");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
new file mode 100644
index 000000000000..7e30d6b60bb8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Drives {@link SettingsPopup} through key events to verify it seeds from
persisted {@link TuiSettings}, cycles theme
+ * and starting-tab values, edits the folder field, persists on Enter
(applying the theme live), and discards on Esc.
+ * Assertions check concrete persisted values, not merely non-null, so a
change in save/apply logic will fail the test.
+ */
+@Isolated
+class SettingsPopupTest {
+
+ private String originalHome;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ }
+
+ @AfterEach
+ void tearDown() {
+ Theme.resetForTesting();
+ if (originalHome != null) {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+ }
+
+ private void useHome(Path dir) {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(dir.toString());
+ }
+
+ private static List<TabRegistry.TabEntry> tabs() {
+ return List.of(
+ new TabRegistry.TabEntry("🐪", "Overview", "overview", "1", 0,
-1),
+ new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6, -1),
+ new TabRegistry.TabEntry("☕", "Beans", "beans", "B", 9, 0));
+ }
+
+ private static KeyEvent key(KeyCode code) {
+ return KeyEvent.ofKey(code, KeyModifiers.NONE);
+ }
+
+ @Test
+ void openSeedsCurrentValuesFromSettings(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
+
"camel.tui.theme=light\ncamel.tui.startTab=Health\ncamel.tui.defaultFolder=/tmp/foo\n");
+
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+
+ assertTrue(popup.isVisible());
+ assertEquals("light", popup.selectedThemeId());
+ assertEquals("Health", popup.selectedStartTab());
+ assertEquals("/tmp/foo", popup.folderText());
+ }
+
+ @Test
+ void themeCyclerWrapsAroundValues(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+
+ assertEquals("dark", popup.selectedThemeId(), "with no config, theme
defaults to the active mode (dark)");
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ assertEquals("light", popup.selectedThemeId());
+
+ int themeCount = ThemeMode.ids().size();
+ for (int i = 0; i < themeCount - 1; i++) {
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ }
+ assertEquals("dark", popup.selectedThemeId(), "cycling through every
theme wraps back to the first value");
+ }
+
+ @Test
+ void startingTabCyclerWrapsAcrossAllTabs(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(1, popup.selectedRow());
+ assertEquals("Overview", popup.selectedStartTab());
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ assertEquals("Health", popup.selectedStartTab());
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ assertEquals("Beans", popup.selectedStartTab());
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ assertEquals("Overview", popup.selectedStartTab(), "cycling wraps past
the last tab");
+ }
+
+ @Test
+ void folderFieldAcceptsTypedText(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(2, popup.selectedRow());
+ popup.handleKeyEvent(KeyEvent.ofChar('/'));
+ popup.handleKeyEvent(KeyEvent.ofChar('a'));
+ assertEquals("/a", popup.folderText());
+ }
+
+ @Test
+ void folderFieldRejectsControlCharacters(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(2, popup.selectedRow());
+ popup.handleKeyEvent(KeyEvent.ofChar(0x01));
+ popup.handleKeyEvent(KeyEvent.ofChar(0x00));
+ popup.handleKeyEvent(KeyEvent.ofChar('x'));
+
+ assertEquals("x", popup.folderText());
+ }
+
+ @Test
+ void enterPersistsSelectionsAndAppliesThemeLive(@TempDir Path tempDir)
throws Exception {
+ useHome(tempDir);
+ // Exercise the real persistence path (disk read/write) against the
isolated home directory.
+ Theme.resetForTesting(false);
+
+ boolean[] cleared = { false };
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.setClearScreen(() -> cleared[0] = true);
+ popup.open();
+
+ popup.handleKeyEvent(KeyEvent.ofChar(' ')); // theme dark -> light
+ assertEquals("light", popup.selectedThemeId());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ popup.handleKeyEvent(KeyEvent.ofChar(' ')); // starting tab Overview
-> Health
+ assertEquals("Health", popup.selectedStartTab());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ for (char c : "/tmp/p".toCharArray()) {
+ popup.handleKeyEvent(KeyEvent.ofChar(c));
+ }
+
+ popup.handleKeyEvent(key(KeyCode.ENTER));
+
+ assertFalse(popup.isVisible(), "Enter closes the popup");
+ assertTrue(cleared[0], "a theme change on save must trigger a screen
clear");
+ assertEquals("light", Theme.mode(), "the theme must be applied live");
+
+ TuiSettings persisted = TuiSettings.load();
+ assertEquals("light", persisted.getThemeId());
+ assertEquals("Health", persisted.getStartTab());
+ assertEquals("/tmp/p", persisted.getDefaultFolder());
+ }
+
+ @Test
+ void escapeDiscardsInFormChanges(@TempDir Path tempDir) throws Exception {
+ useHome(tempDir);
+ Files.writeString(tempDir.resolve(CommandLineHelper.USER_CONFIG),
"camel.tui.theme=dark\n");
+
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+ popup.handleKeyEvent(KeyEvent.ofChar(' ')); // cycle theme dark ->
light, but do not save
+ popup.handleKeyEvent(key(KeyCode.ESCAPE));
+
+ assertFalse(popup.isVisible());
+ assertEquals("dark", TuiSettings.load().getThemeId(), "Esc must not
persist the in-form change");
+ assertEquals("dark", Theme.mode(), "Esc must revert the live preview
back to the theme active before open()");
+ }
+
+ @Test
+ void themeCyclingPreviewsLiveWithoutClosingThePopup(@TempDir Path tempDir)
{
+ useHome(tempDir);
+ boolean[] cleared = { false };
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.setClearScreen(() -> cleared[0] = true);
+ popup.open();
+
+ assertEquals("dark", Theme.mode());
+ popup.handleKeyEvent(KeyEvent.ofChar(' ')); // theme dark -> light
+
+ assertTrue(popup.isVisible(), "cycling the theme row must not close
the dialog");
+ assertEquals("light", Theme.mode(), "the previewed theme must apply
immediately, without saving");
+ assertTrue(cleared[0], "the screen must refresh so the live preview is
visible");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
new file mode 100644
index 000000000000..fd4fe0f4cff8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies {@link TuiSettings} load/mutate/save semantics against real
(isolated) disk I/O: the full round-trip of all
+ * fields, blank-clears-the-key behavior, resilience to a missing file, and
local-config precedence.
+ */
+@Isolated
+class TuiSettingsTest {
+
+ private String originalHome;
+
+ @AfterEach
+ void tearDown() {
+ if (originalHome != null) {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+ }
+
+ private void useHome(Path dir) {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(dir.toString());
+ }
+
+ @Test
+ void roundTripPreservesAllFields(@TempDir Path tempDir) {
+ useHome(tempDir);
+
+ TuiSettings settings = TuiSettings.load();
+ settings.setThemeId("light");
+ settings.setStartTab("Health");
+ settings.setDefaultFolder("/tmp/projects");
+ settings.save();
+
+ TuiSettings loaded = TuiSettings.load();
+ assertEquals("light", loaded.getThemeId());
+ assertEquals("Health", loaded.getStartTab());
+ assertEquals("/tmp/projects", loaded.getDefaultFolder());
+ }
+
+ @Test
+ void blankFieldRemovesItsKeyButKeepsOthers(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+
+ TuiSettings settings = TuiSettings.load();
+ settings.setThemeId("dark");
+ settings.setDefaultFolder("/tmp/x");
+ settings.save();
+
+ TuiSettings second = TuiSettings.load();
+ assertEquals("/tmp/x", second.getDefaultFolder());
+ second.setDefaultFolder(" ");
+ second.save();
+
+ TuiSettings third = TuiSettings.load();
+ assertNull(third.getDefaultFolder(), "a blank folder must clear its
key");
+ assertEquals("dark", third.getThemeId(), "unrelated keys must remain");
+
+ String content =
Files.readString(tempDir.resolve(CommandLineHelper.USER_CONFIG));
+ assertFalse(content.contains("camel.tui.defaultFolder"), "the cleared
key must not be written back");
+ }
+
+ @Test
+ void missingFileYieldsNullFieldsWithoutThrowing(@TempDir Path tempDir) {
+ useHome(tempDir);
+
+ TuiSettings settings = TuiSettings.load();
+ assertNull(settings.getThemeId());
+ assertNull(settings.getStartTab());
+ assertNull(settings.getDefaultFolder());
+ }
+
+ @Test
+ void defaultFolderIsLoadedWhenPresentAlongsideLastFolder(@TempDir Path
tempDir) throws Exception {
+ useHome(tempDir);
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+
+ Files.writeString(globalFile,
+
"camel.tui.defaultFolder=/projects/myapp\ncamel.tui.lastFolder=/tmp/other\n");
+
+ TuiSettings loaded = TuiSettings.load();
+ assertEquals("/projects/myapp", loaded.getDefaultFolder());
+
+ Files.writeString(globalFile, "camel.tui.lastFolder=/tmp/other\n");
+
+ TuiSettings withoutDefaultFolder = TuiSettings.load();
+ assertNull(withoutDefaultFolder.getDefaultFolder());
+ }
+
+ @Test
+ void localOverrideIsReadAndWrittenBackLocally(@TempDir Path tempDir)
throws Exception {
+ // A project has deliberately pinned startTab in the local file.
+ // Editing it via the dialog should update the local file, not the
global file.
+ useHome(tempDir);
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ Files.writeString(localFile, "camel.tui.startTab=Health\n");
+ assertTrue(CommandLineHelper.hasLocalUserConfig());
+
+ TuiSettings loaded = TuiSettings.load();
+ assertEquals("Health", loaded.getStartTab(), "load must read the
local override");
+
+ loaded.setStartTab("Trace");
+ loaded.save();
+
+ String localContent = Files.readString(localFile);
+ assertTrue(localContent.contains("camel.tui.startTab=Trace"),
+ "save must write a locally-overridden key back to the
local file");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+
+ @Test
+ void newKeyGoesToGlobalEvenWhenLocalFileExists(@TempDir Path tempDir)
throws Exception {
+ // A project has a local config for other CLI settings, but no
camel.tui.defaultFolder.
+ // Saving defaultFolder must NOT dirty the project-local file.
+ useHome(tempDir);
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ Files.writeString(localFile, "camel.jbang.runtime=spring-boot\n");
+ assertTrue(CommandLineHelper.hasLocalUserConfig());
+
+ TuiSettings loaded = TuiSettings.load();
+ loaded.setDefaultFolder("/tmp/example");
+ loaded.save();
+
+ String localContent = Files.readString(localFile);
+ assertFalse(localContent.contains("camel.tui.defaultFolder"),
+ "personal TUI state must not be written to the
project-local file");
+
assertTrue(Files.exists(tempDir.resolve(CommandLineHelper.USER_CONFIG)),
+ "the global file must be created for the personal
setting");
+ String globalContent =
Files.readString(tempDir.resolve(CommandLineHelper.USER_CONFIG));
+
assertTrue(globalContent.contains("camel.tui.defaultFolder=/tmp/example"),
+ "personal TUI state must be written to the global file");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
new file mode 100644
index 000000000000..ab88f752bd6c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiUserConfigTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the per-key local/global routing in {@link TuiUserConfig}. These
tests encode the two Codex adversarial
+ * review findings as regression guards:
+ * <ol>
+ * <li>A local config that only carries non-TUI keys must not shadow global
TUI preferences.</li>
+ * <li>Writing a new TUI key must go to the global file, not into the
project-local config.</li>
+ * </ol>
+ */
+@Isolated
+class TuiUserConfigTest {
+
+ private String originalHome;
+
+ @AfterEach
+ void tearDown() {
+ if (originalHome != null) {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+ }
+
+ private void useHome(Path dir) {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(dir.toString());
+ }
+
+ @Test
+ void readFallsBackToGlobalWhenKeyAbsentFromLocal(@TempDir Path tempDir)
throws Exception {
+ useHome(tempDir);
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(globalFile, "camel.tui.theme=light\n");
+
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ Files.writeString(localFile, "camel.jbang.runtime=spring-boot\n");
+
+ assertEquals("light", TuiUserConfig.read("camel.tui.theme"),
+ "global theme must not be shadowed by an unrelated local
config file");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+
+ @Test
+ void readPrefersLocalWhenKeyExistsLocally(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(globalFile, "camel.tui.startTab=Overview\n");
+
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ Files.writeString(localFile, "camel.tui.startTab=Health\n");
+
+ assertEquals("Health", TuiUserConfig.read("camel.tui.startTab"),
+ "a local override must win over the global value");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+
+ @Test
+ void writeRoutesToGlobalForNewKey(@TempDir Path tempDir) throws Exception {
+ useHome(tempDir);
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ Files.writeString(localFile, "camel.jbang.runtime=spring-boot\n");
+
+ TuiUserConfig.write("camel.tui.theme", "dark");
+
+ String localContent = Files.readString(localFile);
+ assertFalse(localContent.contains("camel.tui.theme"),
+ "writing a new TUI key must NOT dirty the project-local
config");
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ assertTrue(Files.exists(globalFile), "the global file must be
created");
+ String globalContent = Files.readString(globalFile);
+ assertTrue(globalContent.contains("camel.tui.theme=dark"),
+ "the new key must land in the global file");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+
+ @Test
+ void writeUpdatesLocalForExistingLocalKey(@TempDir Path tempDir) throws
Exception {
+ useHome(tempDir);
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ Files.writeString(localFile, "camel.tui.startTab=Health\n");
+
+ TuiUserConfig.write("camel.tui.startTab", "Trace");
+
+ String localContent = Files.readString(localFile);
+ assertTrue(localContent.contains("camel.tui.startTab=Trace"),
+ "an existing local override must be updated in the local
file");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+
+ @Test
+ void writeUpdatesLocalForExistingLocalKeyWithEmptyValue(@TempDir Path
tempDir) throws Exception {
+ useHome(tempDir);
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(globalFile, "camel.tui.startTab=Overview\n");
+
+ Path localFile = Path.of(CommandLineHelper.LOCAL_USER_CONFIG);
+ try {
+ // An explicitly-set but empty-valued local key must still be
recognized as a local override
+ // (an empty property line loads as "", not null).
+ Files.writeString(localFile, "camel.tui.startTab=\n");
+
+ TuiUserConfig.write("camel.tui.startTab", "Trace");
+
+ String localContent = Files.readString(localFile);
+ assertTrue(localContent.contains("camel.tui.startTab=Trace"),
+ "an existing empty-valued local override must be updated
in the local file, not the global one");
+ String globalContent = Files.readString(globalFile);
+ assertFalse(globalContent.contains("camel.tui.startTab=Trace"),
+ "the write must not leak into the global file");
+ } finally {
+ Files.deleteIfExists(localFile);
+ }
+ }
+
+ @Test
+ void readReturnsNullWhenKeyAbsentFromBothFiles(@TempDir Path tempDir) {
+ useHome(tempDir);
+ assertNull(TuiUserConfig.read("camel.tui.theme"));
+ }
+
+ @Test
+ void writeBlankRemovesKey(@TempDir Path tempDir) throws Exception {
+ useHome(tempDir);
+ Path globalFile = tempDir.resolve(CommandLineHelper.USER_CONFIG);
+ Files.writeString(globalFile, "camel.tui.theme=dark\n");
+
+ TuiUserConfig.write("camel.tui.theme", " ");
+
+ String content = Files.readString(globalFile);
+ assertFalse(content.contains("camel.tui.theme"), "a blank value must
remove the key");
+ }
+}
diff --git
a/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
b/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
index d24d1766cb4e..7fe9312b3ff8 100644
---
a/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
+++
b/test-infra/camel-test-infra-cli/src/main/java/org/apache/camel/test/infra/cli/services/CliLocalProcessService.java
@@ -492,7 +492,7 @@ public class CliLocalProcessService implements CliService {
}
private Path getUserPropertiesFile() {
- return Path.of(System.getProperty("user.home"),
".camel-jbang-user.properties");
+ return Path.of(System.getProperty("user.home"),
".camel-cli.properties");
}
private void backupUserFiles() {