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 ff643b08f15d camel-jbang - TUI validate on save for Camel YAML files
with error popup
ff643b08f15d is described below
commit ff643b08f15d694b18f2f3d8abf8bc28da8d1187
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Aug 4 18:05:58 2026 +0200
camel-jbang - TUI validate on save for Camel YAML files with error popup
Add YAML DSL schema validation when saving Camel YAML files in the editor.
Validation errors are shown in a centered popup dialog instead of the
notification bar, allowing full error messages to be read. The feature is
controlled by a "Validate on Save" toggle in Settings (default: on).
Also adds cursor line highlight in edit mode and the 'e' key shortcut
to open files directly in edit mode from the Files tab.
camel-jbang - TUI validation error popup improvements
Show validation errors in a word-wrapped popup dialog near the top of
the screen. Clean up error messages by stripping Java FQCN prefixes and
internal parser noise. Stay in edit mode when errors are found on F5 so
the popup is visible immediately. Clear validation state on file load
and edit mode transitions. Drop redundant notification when popup shows.
camel-jbang - TUI settings popup grouped layout with dividers
Organize settings into visual groups separated by dim horizontal
dividers: Appearance, Display, Editor, Network, and AI. Widen label
column to 24 chars and popup to 70 chars for better readability.
Restore full labels (Confirm Actions, Validate on Save).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml | 4 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 4 +-
.../jbang/core/commands/tui/MonitorContext.java | 1 +
.../dsl/jbang/core/commands/tui/SettingsPopup.java | 71 +++++++--
.../dsl/jbang/core/commands/tui/SourceTab.java | 2 +
.../dsl/jbang/core/commands/tui/SourceViewer.java | 163 ++++++++++++++++++++-
.../dsl/jbang/core/commands/tui/TuiSettings.java | 16 ++
.../core/commands/tui/SettingsPopupRenderTest.java | 2 +-
.../jbang/core/commands/tui/SettingsPopupTest.java | 20 ++-
.../core/commands/tui/SourceViewerEditTest.java | 2 +
.../camel/dsl/yaml/validator/YamlValidator.java | 29 +++-
11 files changed, 278 insertions(+), 36 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
index 2e37992344f9..b9d2a2a3f2fe 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
@@ -53,6 +53,10 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-diagram</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-yaml-dsl-validator</artifactId>
+ </dependency>
<dependency>
<groupId>dev.tamboui</groupId>
<artifactId>tamboui-tui</artifactId>
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 f153a4adb666..9a03b3c6244f 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
@@ -611,7 +611,9 @@ public class CamelMonitor extends CamelCommand {
}
private void applyConfirmActions() {
- ctx.confirmActions = TuiSettings.load().isConfirmActions();
+ TuiSettings settings = TuiSettings.load();
+ ctx.confirmActions = settings.isConfirmActions();
+ ctx.validateOnSave = settings.isValidateOnSave();
}
// ---- Event Handling ----
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 91fbb2b60213..71977b4963d8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -52,6 +52,7 @@ class MonitorContext {
boolean logPinVisible;
boolean ratePerMinute;
boolean confirmActions;
+ boolean validateOnSave = true;
BiConsumer<String, Boolean> notificationCallback;
BiConsumer<String, String> openMarkdownCallback;
OpenOptionsCallback openOptionsCallback;
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
index 745c4abd5618..29d6592fca7c 100644
---
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
@@ -52,15 +52,16 @@ class SettingsPopup {
private static final int ROW_LOG_PIN = 3;
private static final int ROW_RATE_PER = 4;
private static final int ROW_CONFIRM_ACTIONS = 5;
- private static final int ROW_FOLDER = 6;
- private static final int ROW_PROXY_HOST = 7;
- private static final int ROW_PROXY_PORT = 8;
- private static final int ROW_SHELL_HISTORY = 9;
- private static final int ROW_AI_PROVIDER = 10;
- private static final int ROW_AI_MODEL = 11;
- private static final int ROW_AI_URL = 12;
- private static final int ROW_AI_PROMPT_HISTORY = 13;
- private static final int ROW_COUNT = 14;
+ private static final int ROW_VALIDATE_ON_SAVE = 6;
+ private static final int ROW_FOLDER = 7;
+ private static final int ROW_PROXY_HOST = 8;
+ private static final int ROW_PROXY_PORT = 9;
+ private static final int ROW_SHELL_HISTORY = 10;
+ private static final int ROW_AI_PROVIDER = 11;
+ private static final int ROW_AI_MODEL = 12;
+ private static final int ROW_AI_URL = 13;
+ private static final int ROW_AI_PROMPT_HISTORY = 14;
+ private static final int ROW_COUNT = 15;
private static final String[] LOG_PIN_OPTIONS = { "off", "25", "50", "75"
};
private static final String[] RATE_PER_OPTIONS = { "seconds", "minutes" };
@@ -85,6 +86,7 @@ class SettingsPopup {
private int logPinIndex;
private int ratePerIndex;
private int confirmActionsIndex;
+ private int validateOnSaveIndex;
private int aiProviderIndex;
private TextInputState folderInput;
private TextInputState proxyHostInput;
@@ -161,6 +163,7 @@ class SettingsPopup {
ratePerIndex = "minutes".equals(currentRatePer) ? 1 : 0;
confirmActionsIndex = settings.isConfirmActions() ? 1 : 0;
+ validateOnSaveIndex = settings.isValidateOnSave() ? 1 : 0;
folderInput = new TextInputState(settings.getDefaultFolder() != null ?
settings.getDefaultFolder() : "");
proxyHostInput = new TextInputState(settings.getProxyHost() != null ?
settings.getProxyHost() : "");
@@ -263,6 +266,12 @@ class SettingsPopup {
}
return true;
}
+ if (selectedRow == ROW_VALIDATE_ON_SAVE) {
+ if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
+ validateOnSaveIndex = validateOnSaveIndex == 0 ? 1 : 0;
+ }
+ return true;
+ }
if (selectedRow == ROW_FOLDER) {
handleTextInput(ke, folderInput);
return true;
@@ -323,6 +332,10 @@ class SettingsPopup {
if (monitorContext != null) {
monitorContext.confirmActions = confirmActionsIndex == 1;
}
+ settings.setValidateOnSave(validateOnSaveIndex == 1 ? "true" :
"false");
+ if (monitorContext != null) {
+ monitorContext.validateOnSave = validateOnSaveIndex == 1;
+ }
settings.setDefaultFolder(stripControlChars(folderInput.text().trim()));
settings.setProxyHost(stripControlChars(proxyHostInput.text().trim()));
settings.setProxyPort(stripControlChars(proxyPortInput.text().trim()));
@@ -344,8 +357,9 @@ class SettingsPopup {
}
void render(Frame frame, Rect area) {
- int popupW = Math.min(60, area.width() - 4);
- int popupH = 2 + ROW_COUNT;
+ int dividers = 4;
+ int popupW = Math.min(70, area.width() - 4);
+ int popupH = 2 + ROW_COUNT + dividers;
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
int y = area.top() + 2;
Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height() - 2));
@@ -360,10 +374,11 @@ class SettingsPopup {
int innerX = popup.left() + 2;
int innerW = popup.width() - 4;
- int labelW = 16;
+ int labelW = 24;
int fieldW = innerW - labelW;
int rowY = popup.top() + 1;
+ // --- Appearance ---
renderLabel(frame, innerX, rowY, labelW, "Theme:", selectedRow ==
ROW_THEME);
renderValue(frame, innerX + labelW, rowY, fieldW,
ThemeMode.values()[themeIndex].label(), selectedRow == ROW_THEME);
rowY++;
@@ -376,10 +391,14 @@ class SettingsPopup {
renderValue(frame, innerX + labelW, rowY, fieldW,
currentSelectTabLabel(), selectedRow == ROW_SELECT_TAB);
rowY++;
+ renderDivider(frame, innerX, rowY, innerW);
+ rowY++;
+
+ // --- Display ---
String logPinLabel = "off".equals(LOG_PIN_OPTIONS[logPinIndex])
? "off"
: LOG_PIN_OPTIONS[logPinIndex] + "%";
- renderLabel(frame, innerX, rowY, labelW, " Log Pin:", selectedRow ==
ROW_LOG_PIN);
+ renderLabel(frame, innerX, rowY, labelW, "Log Pin:", selectedRow ==
ROW_LOG_PIN);
renderValue(frame, innerX + labelW, rowY, fieldW, logPinLabel,
selectedRow == ROW_LOG_PIN);
rowY++;
@@ -387,15 +406,28 @@ class SettingsPopup {
renderValue(frame, innerX + labelW, rowY, fieldW,
RATE_PER_OPTIONS[ratePerIndex], selectedRow == ROW_RATE_PER);
rowY++;
- renderLabel(frame, innerX, rowY, labelW, "Confirm:", selectedRow ==
ROW_CONFIRM_ACTIONS);
+ renderDivider(frame, innerX, rowY, innerW);
+ rowY++;
+
+ // --- Editor ---
+ renderLabel(frame, innerX, rowY, labelW, "Confirm Actions:",
selectedRow == ROW_CONFIRM_ACTIONS);
renderValue(frame, innerX + labelW, rowY, fieldW, confirmActionsIndex
== 1 ? "on" : "off",
selectedRow == ROW_CONFIRM_ACTIONS);
rowY++;
+ renderLabel(frame, innerX, rowY, labelW, "Validate on Save:",
selectedRow == ROW_VALIDATE_ON_SAVE);
+ renderValue(frame, innerX + labelW, rowY, fieldW, validateOnSaveIndex
== 1 ? "on" : "off",
+ selectedRow == ROW_VALIDATE_ON_SAVE);
+ rowY++;
+
renderLabel(frame, innerX, rowY, labelW, "Default Folder:",
selectedRow == ROW_FOLDER);
renderFolder(frame, innerX + labelW, rowY, fieldW, selectedRow ==
ROW_FOLDER);
rowY++;
+ renderDivider(frame, innerX, rowY, innerW);
+ rowY++;
+
+ // --- Network ---
renderLabel(frame, innerX, rowY, labelW, "Proxy Host:", selectedRow ==
ROW_PROXY_HOST);
renderTextInput(frame, innerX + labelW, rowY, fieldW, proxyHostInput,
selectedRow == ROW_PROXY_HOST, "proxy.corp.com");
@@ -411,6 +443,10 @@ class SettingsPopup {
selectedRow == ROW_SHELL_HISTORY, "(100)");
rowY++;
+ renderDivider(frame, innerX, rowY, innerW);
+ rowY++;
+
+ // --- AI ---
renderLabel(frame, innerX, rowY, labelW, "AI Provider:", selectedRow
== ROW_AI_PROVIDER);
renderValue(frame, innerX + labelW, rowY, fieldW,
AI_PROVIDERS.get(aiProviderIndex),
selectedRow == ROW_AI_PROVIDER);
@@ -433,7 +469,8 @@ class SettingsPopup {
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB ||
selectedRow == ROW_SELECT_TAB
|| selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER
- || selectedRow == ROW_CONFIRM_ACTIONS || selectedRow ==
ROW_AI_PROVIDER) {
+ || selectedRow == ROW_CONFIRM_ACTIONS || selectedRow ==
ROW_VALIDATE_ON_SAVE
+ || selectedRow == ROW_AI_PROVIDER) {
hint(spans, "Space", "cycle");
}
hint(spans, "Enter", "save");
@@ -504,6 +541,10 @@ class SettingsPopup {
return sb.toString();
}
+ private void renderDivider(Frame frame, int x, int y, int w) {
+ frame.renderWidget(Paragraph.from(Line.from(Span.styled("─".repeat(w),
Style.EMPTY.dim()))), new Rect(x, y, w, 1));
+ }
+
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));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index f0e9872cdb65..0cf36c3a9756 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -119,6 +119,7 @@ class SourceTab extends AbstractTab {
ctx.notificationCallback.accept(msg, error);
}
});
+ sourceViewer.setValidateOnSave(ctx.validateOnSave);
}
boolean isSourceViewerEditMode() {
@@ -260,6 +261,7 @@ class SourceTab extends AbstractTab {
@Override
public void render(Frame frame, Rect area) {
+ sourceViewer.setValidateOnSave(ctx.validateOnSave);
if (ctx.selectedPid == null) {
renderNoSelection(frame, area);
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index d9ce3d7d766e..0dbd55b7d9df 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.IntConsumer;
+import com.networknt.schema.Error;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
@@ -44,6 +45,7 @@ import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.tui.event.MouseEventKind;
+import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.BorderType;
import dev.tamboui.widgets.block.Borders;
@@ -132,6 +134,10 @@ class SourceViewer {
private AutocompletePopup.AutocompleteProvider autocompleteProvider;
private AutocompletePopup.ValueProvider autocompleteValueProvider;
private AutocompletePopup autocompletePopup;
+ private boolean validateOnSave = true;
+ private org.apache.camel.dsl.yaml.validator.YamlValidator yamlValidator;
+ private List<String> validationErrors;
+ private int validationErrorScroll;
private record CachedSource(
List<String> lines, List<JsonObject> codeData,
@@ -166,6 +172,10 @@ class SourceViewer {
this.autocompleteValueProvider = provider;
}
+ void setValidateOnSave(boolean validateOnSave) {
+ this.validateOnSave = validateOnSave;
+ }
+
void hide() {
exitEditMode();
visible = false;
@@ -400,6 +410,16 @@ class SourceViewer {
}
private boolean handleEditKeyEvent(KeyEvent ke) {
+ if (validationErrors != null) {
+ if (ke.isCancel() || ke.isKey(KeyCode.ENTER)) {
+ validationErrors = null;
+ } else if (ke.isUp()) {
+ validationErrorScroll = Math.max(0, validationErrorScroll - 1);
+ } else if (ke.isDown()) {
+ validationErrorScroll++;
+ }
+ return true;
+ }
if (autocompletePopup != null) {
boolean wasValueMode = autocompletePopup.isValueMode();
AutocompletePopup.Result result =
autocompletePopup.handleKeyEvent(ke);
@@ -518,6 +538,7 @@ class SourceViewer {
quickDocEnabled = false;
search.reset();
dirty = false;
+ validationErrors = null;
editMode = true;
}
@@ -526,6 +547,7 @@ class SourceViewer {
editMode = false;
editState.clear();
autocompletePopup = null;
+ validationErrors = null;
if (wasEditing && isMarkdownFile) {
markdownMode = markdownModeBeforeEdit;
}
@@ -1345,8 +1367,13 @@ class SourceViewer {
return;
}
try {
- Files.writeString(editableFile, editState.text(),
StandardCharsets.UTF_8);
+ String content = editState.text();
+ Files.writeString(editableFile, content, StandardCharsets.UTF_8);
dirty = false;
+ validateAndNotify(content);
+ if (validationErrors != null) {
+ return;
+ }
Path path = editableFile;
boolean restoreMarkdownMode = markdownModeBeforeEdit;
editMode = false;
@@ -1356,7 +1383,6 @@ class SourceViewer {
if (isMarkdownFile) {
markdownMode = restoreMarkdownMode;
}
- notifySave("Saved: " + editableFile.getFileName(), false);
} catch (IOException e) {
notifySave("Save failed: " + e.getMessage(), true);
}
@@ -1367,14 +1393,66 @@ class SourceViewer {
return;
}
try {
- Files.writeString(editableFile, editState.text(),
StandardCharsets.UTF_8);
+ String content = editState.text();
+ Files.writeString(editableFile, content, StandardCharsets.UTF_8);
dirty = false;
- notifySave("Saved: " + editableFile.getFileName(), false);
+ validateAndNotify(content);
} catch (IOException e) {
notifySave("Save failed: " + e.getMessage(), true);
}
}
+ private void validateAndNotify(String content) {
+ if (validateOnSave && isCamelYamlFile()) {
+ List<Error> errors = validateYaml(content);
+ if (errors != null && !errors.isEmpty()) {
+ List<String> msgs = new ArrayList<>();
+ for (Error error : errors) {
+ String msg = error.getMessage();
+ if (msg != null) {
+ msgs.add(cleanValidationMessage(msg));
+ }
+ }
+ if (!msgs.isEmpty()) {
+ validationErrors = msgs;
+ validationErrorScroll = 0;
+ return;
+ }
+ }
+ }
+ notifySave("Saved: " + editableFile.getFileName(), false);
+ }
+
+ private List<Error> validateYaml(String content) {
+ try {
+ if (yamlValidator == null) {
+ yamlValidator = new
org.apache.camel.dsl.yaml.validator.YamlValidator();
+ }
+ return yamlValidator.validate(content);
+ } catch (Exception e) {
+ return List.of();
+ }
+ }
+
+ private static String cleanValidationMessage(String msg) {
+ // strip FQCN prefix like "com.fasterxml...MarkedYAMLException: "
+ int colonSpace = msg.indexOf(": ");
+ if (colonSpace > 0) {
+ String prefix = msg.substring(0, colonSpace);
+ if (prefix.contains(".") && !prefix.contains(" ")) {
+ msg = msg.substring(colonSpace + 2);
+ }
+ }
+ // strip "at [Source: (StringReader); line: N, column: N]"
+ int atSource = msg.indexOf("at [Source:");
+ if (atSource > 0) {
+ msg = msg.substring(0, atSource).stripTrailing();
+ }
+ // strip "in 'reader', " prefix from snakeyaml messages
+ msg = msg.replace("in 'reader', ", "");
+ return msg;
+ }
+
private void notifySave(String message, boolean error) {
if (notificationCallback != null) {
notificationCallback.accept(message, error);
@@ -1656,6 +1734,14 @@ class SourceViewer {
.build();
textArea.renderWithCursor(inner, frame.buffer(), editState, frame);
+ // cursor line highlight
+ int cursorRelRow = editState.cursorRow() - editState.scrollRow();
+ if (cursorRelRow >= 0 && cursorRelRow < inner.height()) {
+ int screenY = inner.top() + cursorRelRow;
+ Rect lineRect = new Rect(inner.left(), screenY, inner.width(), 1);
+ frame.buffer().setStyle(lineRect, Style.EMPTY.bg(Theme.zebra()));
+ }
+
// scope line highlight — shows which EIP or uri: line the cursor
belongs to
if (isCamelYamlFile()) {
int scopeRow = findScopeLineRow(editState.cursorRow());
@@ -1674,9 +1760,78 @@ class SourceViewer {
int cursorCol = editState.cursorCol() - editState.scrollCol();
autocompletePopup.render(frame, inner, cursorRow, cursorCol);
}
+
+ if (validationErrors != null) {
+ renderValidationPopup(frame, area);
+ }
+ }
+
+ private void renderValidationPopup(Frame frame, Rect area) {
+ int popupW = Math.min(80, area.width() - 4);
+ int innerW = popupW - 2;
+
+ List<Line> allLines = new ArrayList<>();
+ for (int i = 0; i < validationErrors.size(); i++) {
+ if (i > 0) {
+ allLines.add(Line.from(Span.raw("")));
+ }
+ String msg = validationErrors.get(i);
+ wrapText(msg, innerW, allLines);
+ }
+
+ int contentH = allLines.size();
+ int popupH = Math.min(contentH + 2, area.height() - 4);
+ int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+ int y = area.top() + 2;
+ Rect popup = new Rect(x, y, popupW, popupH);
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+
+ String titleText = " " + validationErrors.size() + " Validation Error"
+ + (validationErrors.size() > 1 ? "s" : "") + " ";
+ Block block = Block.builder()
+ .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line.from(Span.styled(titleText,
Theme.error().bold()))))
+ .titleBottom(Title.from(Line.from(
+ Span.styled(" Esc", Theme.hintKey()), Span.raw(" close
"))))
+ .build();
+ frame.renderWidget(block, popup);
+ Rect inner = block.inner(popup);
+
+ int visibleLines = inner.height();
+ int clampedScroll = Math.min(validationErrorScroll, Math.max(0,
contentH - visibleLines));
+ validationErrorScroll = clampedScroll;
+ int end = Math.min(clampedScroll + visibleLines, contentH);
+
+ if (clampedScroll < end) {
+ List<Line> visible = allLines.subList(clampedScroll, end);
+ frame.renderWidget(
+
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
+ inner);
+ }
+ }
+
+ private static void wrapText(String text, int width, List<Line> out) {
+ if (width <= 0) {
+ width = 40;
+ }
+ int pos = 0;
+ while (pos < text.length()) {
+ int end = Math.min(pos + width, text.length());
+ out.add(Line.from(Span.styled(text.substring(pos, end),
Theme.error())));
+ pos = end;
+ }
+ if (text.isEmpty()) {
+ out.add(Line.from(Span.styled(text, Theme.error())));
+ }
}
void renderFooter(List<Span> spans) {
+ if (editMode && validationErrors != null) {
+ TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "scroll");
+ TuiHelper.hintLast(spans, "Esc", "close");
+ return;
+ }
if (editMode) {
TuiHelper.hint(spans, "Esc", "cancel");
TuiHelper.hint(spans, "F5", "save & close");
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
index 65ef4e5bc427..233084687d6d 100644
---
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
@@ -44,6 +44,7 @@ final class TuiSettings {
static final String PROP_SHELL_HISTORY = "camel.tui.shell.history";
static final String PROP_AI_PROMPT_HISTORY = "camel.tui.ai.promptHistory";
static final String PROP_CONFIRM_ACTIONS = "camel.tui.confirmActions";
+ static final String PROP_VALIDATE_ON_SAVE = "camel.tui.validateOnSave";
private String themeId;
private String startTab;
@@ -59,6 +60,7 @@ final class TuiSettings {
private String shellHistory;
private String aiPromptHistory;
private String confirmActions;
+ private String validateOnSave;
String getThemeId() {
return themeId;
@@ -184,6 +186,18 @@ final class TuiSettings {
return !"false".equals(confirmActions);
}
+ String getValidateOnSave() {
+ return validateOnSave;
+ }
+
+ void setValidateOnSave(String validateOnSave) {
+ this.validateOnSave = validateOnSave;
+ }
+
+ boolean isValidateOnSave() {
+ return !"false".equals(validateOnSave);
+ }
+
/**
* 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
@@ -206,6 +220,7 @@ final class TuiSettings {
settings.shellHistory =
trimToNull(TuiUserConfig.read(PROP_SHELL_HISTORY));
settings.aiPromptHistory =
trimToNull(TuiUserConfig.read(PROP_AI_PROMPT_HISTORY));
settings.confirmActions =
trimToNull(TuiUserConfig.read(PROP_CONFIRM_ACTIONS));
+ settings.validateOnSave =
trimToNull(TuiUserConfig.read(PROP_VALIDATE_ON_SAVE));
} catch (RuntimeException e) {
// best-effort: return an object with null fields on read failure
}
@@ -233,6 +248,7 @@ final class TuiSettings {
TuiUserConfig.write(PROP_SHELL_HISTORY, shellHistory);
TuiUserConfig.write(PROP_AI_PROMPT_HISTORY, aiPromptHistory);
TuiUserConfig.write(PROP_CONFIRM_ACTIONS, confirmActions);
+ TuiUserConfig.write(PROP_VALIDATE_ON_SAVE, validateOnSave);
} catch (RuntimeException e) {
// best-effort: a save failure must not disrupt the TUI
}
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
index e1683035b23a..92730c0e98ab 100644
---
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
@@ -67,7 +67,7 @@ class SettingsPopupRenderTest {
new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6,
-1)));
popup.open();
- Rect area = new Rect(0, 0, 80, 20);
+ Rect area = new Rect(0, 0, 80, 25);
Buffer buffer = Buffer.empty(area);
Frame frame = Frame.forTesting(buffer);
popup.render(frame, area);
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
index 858ae671b17a..98929a84b48d 100644
---
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
@@ -138,7 +138,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(6, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(7, popup.selectedRow());
popup.handleKeyEvent(KeyEvent.ofChar('/'));
popup.handleKeyEvent(KeyEvent.ofChar('a'));
assertEquals("/a", popup.folderText());
@@ -157,7 +158,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(6, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(7, popup.selectedRow());
popup.handleKeyEvent(KeyEvent.ofChar(0x01));
popup.handleKeyEvent(KeyEvent.ofChar(0x00));
popup.handleKeyEvent(KeyEvent.ofChar('x'));
@@ -182,7 +184,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(10, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(11, popup.selectedRow());
assertEquals("auto", popup.selectedAiProvider());
popup.handleKeyEvent(KeyEvent.ofChar(' '));
assertEquals("ollama", popup.selectedAiProvider());
@@ -214,21 +217,21 @@ class SettingsPopupTest {
popup.setTabEntries(tabs());
popup.open();
- // navigate to Shell History (row 9)
- for (int i = 0; i < 9; i++) {
+ // navigate to Shell History (row 10)
+ for (int i = 0; i < 10; i++) {
popup.handleKeyEvent(key(KeyCode.DOWN));
}
- assertEquals(9, popup.selectedRow());
+ assertEquals(10, popup.selectedRow());
for (char c : "50".toCharArray()) {
popup.handleKeyEvent(KeyEvent.ofChar(c));
}
assertEquals("50", popup.shellHistoryText());
- // navigate to AI Prompt History (row 13)
+ // navigate to AI Prompt History (row 14)
for (int i = 0; i < 4; i++) {
popup.handleKeyEvent(key(KeyCode.DOWN));
}
- assertEquals(13, popup.selectedRow());
+ assertEquals(14, popup.selectedRow());
for (char c : "200".toCharArray()) {
popup.handleKeyEvent(KeyEvent.ofChar(c));
}
@@ -262,6 +265,7 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN)); // log pin
popup.handleKeyEvent(key(KeyCode.DOWN)); // rate per
popup.handleKeyEvent(key(KeyCode.DOWN)); // confirm actions
+ popup.handleKeyEvent(key(KeyCode.DOWN)); // validate on save
popup.handleKeyEvent(key(KeyCode.DOWN)); // folder
for (char c : "/tmp/p".toCharArray()) {
popup.handleKeyEvent(KeyEvent.ofChar(c));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
index 02c344e678b6..b4b7339cc268 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
@@ -55,6 +55,7 @@ class SourceViewerEditTest {
void setUp() throws Exception {
Theme.resetForTesting();
viewer = new SourceViewer();
+ viewer.setValidateOnSave(false);
viewer.setNotificationCallback((msg, error) -> {
lastNotification.set(msg);
lastNotificationError.set(error);
@@ -380,6 +381,7 @@ class SourceViewerEditTest {
AtomicReference<List<IntegrationInfo>> data = new
AtomicReference<>(List.of(info));
MonitorContext ctx = new MonitorContext(data, new
AtomicReference<>(List.of()));
ctx.selectedPid = "1234";
+ ctx.validateOnSave = false;
SourceTab tab = new SourceTab(ctx);
tab.onTabSelected();
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
index 91770407d318..60c0659aab0b 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
@@ -66,16 +66,31 @@ public class YamlValidator {
var target = mapper.readTree(file);
return new ArrayList<>(schema.validate(target));
} catch (Exception e) {
- String msg = e.getClass().getName() + ": " + e.getMessage();
- Error error = Error.builder()
- .messageKey("parser")
- .format(new MessageFormat("{0}"))
- .arguments(msg)
- .build();
- return List.of(error);
+ return List.of(parseError(e));
}
}
+ public List<Error> validate(String content) throws Exception {
+ if (schema == null) {
+ init();
+ }
+ try {
+ var target = mapper.readTree(content);
+ return new ArrayList<>(schema.validate(target));
+ } catch (Exception e) {
+ return List.of(parseError(e));
+ }
+ }
+
+ private static Error parseError(Exception e) {
+ String msg = e.getClass().getName() + ": " + e.getMessage();
+ return Error.builder()
+ .messageKey("parser")
+ .format(new MessageFormat("{0}"))
+ .arguments(msg)
+ .build();
+ }
+
public void init() throws Exception {
String location = canonical ? LOCATION_CANONICAL : LOCATION;
var model =
mapper.readTree(YamlValidator.class.getResourceAsStream(location));