You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/07/19 15:14:53 UTC

(camel) branch fix/CAMEL-23913 created (now 00c8573c6f39)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a change to branch fix/CAMEL-23913
in repository https://gitbox.apache.org/repos/asf/camel.git


      at 00c8573c6f39 CAMEL-23913: camel-tui - Add Catalog tab and remove standalone catalog command

This branch includes the following new commits:

     new 00c8573c6f39 CAMEL-23913: camel-tui - Add Catalog tab and remove standalone catalog command

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-23913: camel-tui - Add Catalog tab and remove standalone catalog command

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-23913
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 00c8573c6f39342ff13420765ea5c27950d42d5d
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sun Jul 19 17:14:39 2026 +0200

    CAMEL-23913: camel-tui - Add Catalog tab and remove standalone catalog command
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../jbang/core/commands/tui/CamelCatalogTui.java   | 738 ---------------------
 .../dsl/jbang/core/commands/tui/CatalogTab.java    | 535 +++++++++++++++
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |   3 +
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   1 +
 .../dsl/jbang/core/commands/tui/TuiPlugin.java     |   3 +-
 5 files changed, 540 insertions(+), 740 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java
deleted file mode 100644
index 0f736f5e64ce..000000000000
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java
+++ /dev/null
@@ -1,738 +0,0 @@
-/*
- * 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.Collections;
-import java.util.List;
-import java.util.Map;
-
-import dev.tamboui.layout.Constraint;
-import dev.tamboui.layout.Layout;
-import dev.tamboui.layout.Rect;
-import dev.tamboui.style.Overflow;
-import dev.tamboui.style.Style;
-import dev.tamboui.terminal.Frame;
-import dev.tamboui.text.CharWidth;
-import dev.tamboui.text.Line;
-import dev.tamboui.text.Span;
-import dev.tamboui.text.Text;
-import dev.tamboui.tui.TuiRunner;
-import dev.tamboui.tui.event.Event;
-import dev.tamboui.tui.event.KeyCode;
-import dev.tamboui.tui.event.KeyEvent;
-import dev.tamboui.widgets.block.Block;
-import dev.tamboui.widgets.block.BorderType;
-import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.paragraph.Paragraph;
-import dev.tamboui.widgets.table.Cell;
-import dev.tamboui.widgets.table.Row;
-import dev.tamboui.widgets.table.Table;
-import dev.tamboui.widgets.table.TableState;
-import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
-import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
-import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
-import org.apache.camel.util.json.JsonArray;
-import org.apache.camel.util.json.JsonObject;
-import org.apache.camel.util.json.Jsoner;
-import picocli.CommandLine.Command;
-import sun.misc.Signal;
-
-@Command(name = "catalog",
-         description = "Interactive TUI catalog browser",
-         sortOptions = false)
-public class CamelCatalogTui extends CamelCommand {
-
-    private static final int FOCUS_LIST = 0;
-    private static final int FOCUS_OPTIONS = 1;
-
-    // Catalog data
-    private List<ComponentInfo> allComponents = Collections.emptyList();
-    private List<ComponentInfo> filteredComponents = Collections.emptyList();
-
-    // UI state
-    private final TableState listTableState = new TableState();
-    private final TableState optionsTableState = new TableState();
-    private int focus = FOCUS_LIST;
-    private final StringBuilder componentFilter = new StringBuilder();
-    private final StringBuilder optionFilter = new StringBuilder();
-    private boolean componentFullText;
-    private boolean optionFullText;
-    private int descriptionScroll;
-
-    // All options for selected component (unfiltered)
-    private List<OptionInfo> allOptionsUnfiltered = Collections.emptyList();
-    // Filtered options displayed in table
-    private List<OptionInfo> filteredOptions = Collections.emptyList();
-
-    private ClassLoader classLoader;
-
-    public CamelCatalogTui(CamelJBangMain main, ClassLoader classLoader) {
-        super(main);
-        this.classLoader = classLoader;
-    }
-
-    @Override
-    public Integer doCall() throws Exception {
-        // to make ServiceLoader work with tamboui for downloaded JARs
-        Thread.currentThread().setContextClassLoader(classLoader);
-
-        loadCatalog();
-
-        try (var tui = TuiBackendHelper.createTuiRunner()) {
-            Signal.handle(new Signal("INT"), sig -> tui.quit());
-            tui.run(this::handleEvent, this::render);
-        }
-        return 0;
-    }
-
-    // ---- Catalog Loading ----
-
-    @SuppressWarnings("unchecked")
-    private void loadCatalog() {
-        CamelCatalog catalog = new DefaultCamelCatalog();
-        List<String> names = new ArrayList<>(catalog.findComponentNames());
-        Collections.sort(names);
-
-        allComponents = new ArrayList<>();
-        for (String name : names) {
-            try {
-                String json = catalog.componentJSonSchema(name);
-                if (json == null) {
-                    continue;
-                }
-                JsonObject root = (JsonObject) Jsoner.deserialize(json);
-                JsonObject component = (JsonObject) root.get("component");
-                if (component == null) {
-                    continue;
-                }
-
-                ComponentInfo info = new ComponentInfo();
-                info.name = name;
-                info.title = component.getStringOrDefault("title", name);
-                info.description = component.getStringOrDefault("description", "");
-                info.label = component.getStringOrDefault("label", "");
-                info.groupId = component.getStringOrDefault("groupId", "");
-                info.artifactId = component.getStringOrDefault("artifactId", "");
-                info.version = component.getStringOrDefault("version", "");
-                info.scheme = component.getStringOrDefault("scheme", name);
-                info.syntax = component.getStringOrDefault("syntax", "");
-                info.deprecated = component.getBooleanOrDefault("deprecated", false);
-
-                // Parse properties — separate by kind
-                JsonObject properties = (JsonObject) root.get("properties");
-                if (properties != null) {
-                    for (Map.Entry<String, Object> entry : properties.entrySet()) {
-                        JsonObject prop = (JsonObject) entry.getValue();
-                        OptionInfo opt = parseOption(entry.getKey(), prop);
-                        String kind = prop.getStringOrDefault("kind", "parameter");
-                        opt.kind = kind;
-                        if ("property".equals(kind)) {
-                            info.componentOptions.add(opt);
-                        } else {
-                            info.endpointOptions.add(opt);
-                        }
-                    }
-                }
-
-                allComponents.add(info);
-            } catch (Exception e) {
-                // skip unparseable components
-            }
-        }
-
-        applyComponentFilter();
-        if (!filteredComponents.isEmpty()) {
-            listTableState.select(0);
-            updateSelectedComponent();
-        }
-    }
-
-    private OptionInfo parseOption(String name, JsonObject prop) {
-        OptionInfo opt = new OptionInfo();
-        opt.name = name;
-        opt.type = prop.getStringOrDefault("type", "");
-        opt.required = prop.getBooleanOrDefault("required", false);
-        opt.defaultValue = prop.getStringOrDefault("defaultValue", "");
-        opt.description = prop.getStringOrDefault("description", "");
-        opt.group = prop.getStringOrDefault("group", "");
-        opt.enumValues = "";
-        Object enums = prop.get("enum");
-        if (enums instanceof JsonArray arr) {
-            List<String> vals = new ArrayList<>();
-            for (Object v : arr) {
-                vals.add(String.valueOf(v));
-            }
-            opt.enumValues = String.join(", ", vals);
-        }
-        return opt;
-    }
-
-    private void applyComponentFilter() {
-        String filter = componentFilter.toString().toLowerCase();
-        if (filter.isEmpty()) {
-            filteredComponents = new ArrayList<>(allComponents);
-        } else {
-            filteredComponents = new ArrayList<>();
-            for (ComponentInfo c : allComponents) {
-                if (componentFullText) {
-                    if (c.name.toLowerCase().contains(filter)
-                            || c.title.toLowerCase().contains(filter)
-                            || c.description.toLowerCase().contains(filter)
-                            || c.label.toLowerCase().contains(filter)) {
-                        filteredComponents.add(c);
-                    }
-                } else {
-                    if (c.name.toLowerCase().contains(filter)) {
-                        filteredComponents.add(c);
-                    }
-                }
-            }
-        }
-        if (!filteredComponents.isEmpty()) {
-            listTableState.select(0);
-        } else {
-            listTableState.clearSelection();
-        }
-        updateSelectedComponent();
-    }
-
-    private void applyOptionFilter() {
-        String filter = optionFilter.toString().toLowerCase();
-        if (filter.isEmpty()) {
-            filteredOptions = new ArrayList<>(allOptionsUnfiltered);
-        } else {
-            filteredOptions = new ArrayList<>();
-            for (OptionInfo o : allOptionsUnfiltered) {
-                if (optionFullText) {
-                    if (o.name.toLowerCase().contains(filter)
-                            || o.description.toLowerCase().contains(filter)
-                            || o.group.toLowerCase().contains(filter)) {
-                        filteredOptions.add(o);
-                    }
-                } else {
-                    if (o.name.toLowerCase().contains(filter)) {
-                        filteredOptions.add(o);
-                    }
-                }
-            }
-        }
-        if (!filteredOptions.isEmpty()) {
-            optionsTableState.select(0);
-        } else {
-            optionsTableState.clearSelection();
-        }
-        descriptionScroll = 0;
-    }
-
-    private void updateSelectedComponent() {
-        Integer sel = listTableState.selected();
-        if (sel != null && sel >= 0 && sel < filteredComponents.size()) {
-            ComponentInfo info = filteredComponents.get(sel);
-            allOptionsUnfiltered = new ArrayList<>();
-            allOptionsUnfiltered.addAll(info.endpointOptions);
-            allOptionsUnfiltered.addAll(info.componentOptions);
-        } else {
-            allOptionsUnfiltered = Collections.emptyList();
-        }
-        optionFilter.setLength(0);
-        optionFullText = false;
-        applyOptionFilter();
-        descriptionScroll = 0;
-    }
-
-    // ---- Event Handling ----
-
-    private boolean handleEvent(Event event, TuiRunner runner) {
-        if (event instanceof KeyEvent ke) {
-            // Quit
-            if (ke.isQuit()) {
-                runner.quit();
-                return true;
-            }
-
-            // Escape: clear filter first, then go back, then quit
-            if (ke.isCancel()) {
-                if (focus == FOCUS_OPTIONS && (!optionFilter.isEmpty() || optionFullText)) {
-                    optionFilter.setLength(0);
-                    optionFullText = false;
-                    applyOptionFilter();
-                    return true;
-                }
-                if (focus == FOCUS_OPTIONS) {
-                    focus = FOCUS_LIST;
-                    descriptionScroll = 0;
-                    return true;
-                }
-                if (!componentFilter.isEmpty() || componentFullText) {
-                    componentFilter.setLength(0);
-                    componentFullText = false;
-                    applyComponentFilter();
-                    return true;
-                }
-                runner.quit();
-                return true;
-            }
-
-            // Backspace: delete from active filter
-            if (ke.isDeleteBackward()) {
-                if (focus == FOCUS_LIST && !componentFilter.isEmpty()) {
-                    componentFilter.deleteCharAt(componentFilter.length() - 1);
-                    applyComponentFilter();
-                } else if (focus == FOCUS_OPTIONS && !optionFilter.isEmpty()) {
-                    optionFilter.deleteCharAt(optionFilter.length() - 1);
-                    applyOptionFilter();
-                }
-                return true;
-            }
-
-            // Panel switching — only when no active filter on current panel
-            if (ke.isFocusNext()) {
-                if (focus == FOCUS_LIST) {
-                    focus = FOCUS_OPTIONS;
-                } else {
-                    focus = FOCUS_LIST;
-                }
-                descriptionScroll = 0;
-                return true;
-            }
-            if (ke.isRight() && focus == FOCUS_LIST) {
-                focus = FOCUS_OPTIONS;
-                descriptionScroll = 0;
-                return true;
-            }
-            if (ke.isLeft() && focus == FOCUS_OPTIONS) {
-                focus = FOCUS_LIST;
-                descriptionScroll = 0;
-                return true;
-            }
-
-            // Enter drills into options
-            if (ke.isConfirm() && focus == FOCUS_LIST) {
-                focus = FOCUS_OPTIONS;
-                descriptionScroll = 0;
-                return true;
-            }
-
-            // Navigation
-            if (ke.isUp()) {
-                if (focus == FOCUS_LIST) {
-                    listTableState.selectPrevious();
-                    updateSelectedComponent();
-                } else {
-                    optionsTableState.selectPrevious();
-                    descriptionScroll = 0;
-                }
-                return true;
-            }
-            if (ke.isDown()) {
-                if (focus == FOCUS_LIST) {
-                    listTableState.selectNext(filteredComponents.size());
-                    updateSelectedComponent();
-                } else {
-                    optionsTableState.selectNext(filteredOptions.size());
-                    descriptionScroll = 0;
-                }
-                return true;
-            }
-            if (ke.isPageUp()) {
-                descriptionScroll = Math.max(0, descriptionScroll - 5);
-                return true;
-            }
-            if (ke.isPageDown()) {
-                descriptionScroll += 5;
-                return true;
-            }
-
-            // '/' toggles full-text search mode
-            if (ke.isChar('/')) {
-                if (focus == FOCUS_LIST) {
-                    componentFullText = !componentFullText;
-                    applyComponentFilter();
-                } else {
-                    optionFullText = !optionFullText;
-                    applyOptionFilter();
-                }
-                return true;
-            }
-
-            // Typing filters the active panel
-            if (ke.code() == KeyCode.CHAR) {
-                if (focus == FOCUS_LIST) {
-                    componentFilter.append(ke.string());
-                    applyComponentFilter();
-                } else {
-                    optionFilter.append(ke.string());
-                    applyOptionFilter();
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-
-    // ---- Rendering ----
-
-    private void render(Frame frame) {
-        Rect area = frame.area();
-
-        // Layout: header (3) + top panels (fill) + separator (1) + description (40%) + footer (1)
-        List<Rect> mainChunks = Layout.vertical()
-                .constraints(
-                        Constraint.length(3),
-                        Constraint.percentage(50),
-                        Constraint.length(1),
-                        Constraint.fill(),
-                        Constraint.length(1))
-                .split(area);
-
-        renderHeader(frame, mainChunks.get(0));
-        renderTopPanels(frame, mainChunks.get(1));
-        renderSeparator(frame, mainChunks.get(2));
-        renderDescription(frame, mainChunks.get(3));
-        renderFooter(frame, mainChunks.get(4));
-    }
-
-    private void renderHeader(Frame frame, Rect area) {
-        Line titleLine = Line.from(
-                Span.styled(" Camel Catalog", Style.EMPTY.fg(Theme.accent()).bold()),
-                Span.raw("  "),
-                Span.styled(filteredComponents.size() + "/" + allComponents.size() + " components",
-                        Style.EMPTY.fg(Theme.accent())));
-
-        Block headerBlock = Block.builder()
-                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                .title(" Apache Camel ")
-                .build();
-
-        frame.renderWidget(
-                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
-                area);
-    }
-
-    private void renderTopPanels(Frame frame, Rect area) {
-        // Split horizontally: component list (30%) + options table (70%)
-        List<Rect> chunks = Layout.horizontal()
-                .constraints(Constraint.percentage(30), Constraint.percentage(70))
-                .split(area);
-
-        renderComponentList(frame, chunks.get(0));
-        renderOptionsTable(frame, chunks.get(1));
-    }
-
-    private void renderComponentList(Frame frame, Rect area) {
-        List<Row> rows = new ArrayList<>();
-        for (ComponentInfo comp : filteredComponents) {
-            Style nameStyle = comp.deprecated
-                    ? Theme.error().dim()
-                    : Style.EMPTY.fg(Theme.accent());
-            String label = comp.name;
-            if (comp.deprecated) {
-                label = label + " (deprecated)";
-            }
-            rows.add(Row.from(Cell.from(Span.styled(label, nameStyle))));
-        }
-
-        if (rows.isEmpty()) {
-            rows.add(Row.from(Cell.from(Span.styled("No matching components", Style.EMPTY.dim()))));
-        }
-
-        Style borderStyle = focus == FOCUS_LIST
-                ? Style.EMPTY.fg(Theme.accent())
-                : Style.EMPTY;
-
-        String modePrefix = componentFullText ? "/" : "";
-        String listTitle = componentFilter.isEmpty() && !componentFullText
-                ? " Components "
-                : " Components [" + modePrefix + componentFilter + "] ";
-
-        Table table = Table.builder()
-                .rows(rows)
-                .widths(Constraint.fill())
-                .highlightStyle(Theme.selectionBg())
-                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
-                .block(Block.builder()
-                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .borderStyle(borderStyle)
-                        .title(listTitle)
-                        .build())
-                .build();
-
-        frame.renderStatefulWidget(table, area, listTableState);
-    }
-
-    private void renderOptionsTable(Frame frame, Rect area) {
-        Style borderStyle = focus == FOCUS_OPTIONS
-                ? Style.EMPTY.fg(Theme.accent())
-                : Style.EMPTY;
-
-        String optModePrefix = optionFullText ? "/" : "";
-        String optTitle = optionFilter.isEmpty() && !optionFullText
-                ? " Options "
-                : " Options [" + optModePrefix + optionFilter + "] ";
-
-        if (filteredOptions.isEmpty()) {
-            String emptyMsg = allOptionsUnfiltered.isEmpty() ? " Select a component" : " No matching options";
-            frame.renderWidget(
-                    Paragraph.builder()
-                            .text(Text.from(Line.from(
-                                    Span.styled(emptyMsg, Style.EMPTY.dim()))))
-                            .block(Block.builder()
-                                    .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                                    .borderStyle(borderStyle)
-                                    .title(optTitle)
-                                    .build())
-                            .build(),
-                    area);
-            return;
-        }
-
-        List<Row> rows = new ArrayList<>();
-        for (OptionInfo opt : filteredOptions) {
-            rows.add(optionToRow(opt));
-        }
-
-        Row header = Row.from(
-                Cell.from(Span.styled("NAME", Style.EMPTY.bold())),
-                Cell.from(Span.styled("TYPE", Style.EMPTY.bold())),
-                Cell.from(Span.styled("REQ", Style.EMPTY.bold())),
-                Cell.from(Span.styled("DEFAULT", Style.EMPTY.bold())),
-                Cell.from(Span.styled("KIND", Style.EMPTY.bold())));
-
-        Table table = Table.builder()
-                .rows(rows)
-                .header(header)
-                .widths(
-                        Constraint.length(25),
-                        Constraint.length(12),
-                        Constraint.length(4),
-                        Constraint.length(12),
-                        Constraint.fill())
-                .highlightStyle(Theme.selectionBg())
-                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
-                .block(Block.builder()
-                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .borderStyle(borderStyle)
-                        .title(optTitle)
-                        .build())
-                .build();
-
-        frame.renderStatefulWidget(table, area, optionsTableState);
-    }
-
-    private void renderSeparator(Frame frame, Rect area) {
-        frame.renderWidget(
-                Block.builder()
-                        .borders(Borders.TOP_ONLY)
-                        .borderStyle(Theme.muted())
-                        .build(),
-                area);
-    }
-
-    private void renderDescription(Frame frame, Rect area) {
-        List<Line> lines = new ArrayList<>();
-        String title;
-        int wrapWidth = Math.max(20, area.width() - 4);
-
-        if (focus == FOCUS_OPTIONS) {
-            // Show selected option detail
-            Integer sel = optionsTableState.selected();
-            if (sel != null && sel >= 0 && sel < filteredOptions.size()) {
-                OptionInfo opt = filteredOptions.get(sel);
-                title = " " + opt.name + " ";
-
-                List<String[]> fields = new ArrayList<>();
-                fields.add(new String[] { "Name", opt.name });
-                if (opt.kind != null && !opt.kind.isEmpty()) {
-                    fields.add(new String[] { "Kind", opt.kind });
-                }
-                fields.add(new String[] { "Type", opt.type });
-                fields.add(new String[] { "Required", opt.required ? "Yes" : "No" });
-                if (!opt.defaultValue.isEmpty()) {
-                    fields.add(new String[] { "Default", opt.defaultValue });
-                }
-                if (opt.group != null && !opt.group.isEmpty()) {
-                    fields.add(new String[] { "Group", opt.group });
-                }
-                if (opt.enumValues != null && !opt.enumValues.isEmpty()) {
-                    fields.add(new String[] { "Values", opt.enumValues });
-                }
-                flowFields(lines, fields, wrapWidth, opt);
-
-                lines.add(Line.from(Span.raw("")));
-                if (opt.description != null && !opt.description.isEmpty()) {
-                    lines.add(Line.from(Span.raw(" " + opt.description)));
-                }
-            } else {
-                title = " Description ";
-            }
-        } else {
-            // Show selected component detail
-            Integer sel = listTableState.selected();
-            if (sel != null && sel >= 0 && sel < filteredComponents.size()) {
-                ComponentInfo comp = filteredComponents.get(sel);
-                title = " " + comp.title + " ";
-
-                List<String[]> fields = new ArrayList<>();
-                fields.add(new String[] { "Scheme", comp.scheme });
-                fields.add(new String[] { "Syntax", comp.syntax });
-                fields.add(new String[] { "Label", comp.label });
-                fields.add(new String[] { "Maven", comp.groupId + ":" + comp.artifactId + ":" + comp.version });
-                fields.add(new String[] {
-                        "Options",
-                        comp.endpointOptions.size() + " endpoint, " + comp.componentOptions.size() + " component" });
-                if (comp.deprecated) {
-                    fields.add(new String[] { "Status", "DEPRECATED" });
-                }
-                flowFields(lines, fields, wrapWidth, null);
-
-                lines.add(Line.from(Span.raw("")));
-                if (comp.description != null && !comp.description.isEmpty()) {
-                    lines.add(Line.from(Span.raw(" " + comp.description)));
-                }
-            } else {
-                title = " Description ";
-            }
-        }
-
-        frame.renderWidget(
-                Paragraph.builder()
-                        .text(Text.from(lines))
-                        .overflow(Overflow.WRAP_WORD)
-                        .scroll(descriptionScroll)
-                        .block(Block.builder()
-                                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                                .title(title)
-                                .build())
-                        .build(),
-                area);
-    }
-
-    private Row optionToRow(OptionInfo opt) {
-        Style nameStyle = opt.required
-                ? Style.EMPTY.fg(Theme.accent()).bold()
-                : Style.EMPTY.fg(Theme.accent());
-
-        return Row.from(
-                Cell.from(Span.styled(opt.name, nameStyle)),
-                Cell.from(Span.styled(opt.type, Style.EMPTY.dim())),
-                Cell.from(opt.required
-                        ? Span.styled("*", Theme.error().bold())
-                        : Span.raw("")),
-                Cell.from(Span.styled(opt.defaultValue, Style.EMPTY.dim())),
-                Cell.from(Span.styled(opt.kind != null ? opt.kind : "", Style.EMPTY.dim())));
-    }
-
-    private void renderFooter(Frame frame, Rect area) {
-        Line footer = Line.from(
-                Span.styled(" Type", Theme.label().bold()),
-                Span.raw(" name filter  "),
-                Span.styled("/", Theme.label().bold()),
-                Span.raw(" full-text  "),
-                Span.styled("Esc", Theme.label().bold()),
-                Span.raw(" clear/back/quit  "),
-                Span.styled("\u2191\u2193", Theme.label().bold()),
-                Span.raw(" navigate  "),
-                Span.styled("\u2190\u2192", Theme.label().bold()),
-                Span.raw("/"),
-                Span.styled("Tab", Theme.label().bold()),
-                Span.raw(" panels  "),
-                Span.styled("PgUp/Dn", Theme.label().bold()),
-                Span.raw(" scroll"));
-
-        frame.renderWidget(Paragraph.from(footer), area);
-    }
-
-    // ---- Helpers ----
-
-    private static void flowFields(List<Line> lines, List<String[]> fields, int maxWidth, OptionInfo opt) {
-        List<Span> currentSpans = new ArrayList<>();
-        int currentLen = 1; // leading space
-        String gap = "    ";
-
-        for (String[] field : fields) {
-            String label = field[0] + ": ";
-            String value = field[1];
-            int fieldLen = CharWidth.of(label) + CharWidth.of(value);
-
-            // If adding this field would exceed width, flush current line
-            if (!currentSpans.isEmpty() && currentLen + CharWidth.of(gap) + fieldLen > maxWidth) {
-                lines.add(Line.from(currentSpans));
-                currentSpans = new ArrayList<>();
-                currentLen = 1;
-            }
-
-            if (!currentSpans.isEmpty()) {
-                currentSpans.add(Span.raw(gap));
-                currentLen += CharWidth.of(gap);
-            } else {
-                currentSpans.add(Span.raw(" "));
-            }
-
-            currentSpans.add(Span.styled(label, Theme.label().bold()));
-
-            // Apply special styling for certain values
-            Style valueStyle;
-            if ("DEPRECATED".equals(value)) {
-                valueStyle = Theme.error().bold();
-            } else if (opt != null && "Required".equals(field[0]) && opt.required) {
-                valueStyle = Theme.error().bold();
-            } else if (opt != null && "Name".equals(field[0])) {
-                valueStyle = Style.EMPTY.fg(Theme.accent()).bold();
-            } else if ("Label".equals(field[0]) || "Values".equals(field[0])) {
-                valueStyle = Style.EMPTY.fg(Theme.accent());
-            } else {
-                valueStyle = Style.EMPTY;
-            }
-            currentSpans.add(Span.styled(value, valueStyle));
-            currentLen += fieldLen;
-        }
-
-        if (!currentSpans.isEmpty()) {
-            lines.add(Line.from(currentSpans));
-        }
-    }
-
-    // ---- Data Classes ----
-
-    static class ComponentInfo {
-        String name;
-        String title;
-        String description;
-        String label;
-        String groupId;
-        String artifactId;
-        String version;
-        String scheme;
-        String syntax;
-        boolean deprecated;
-        final List<OptionInfo> componentOptions = new ArrayList<>();
-        final List<OptionInfo> endpointOptions = new ArrayList<>();
-    }
-
-    static class OptionInfo {
-        String name;
-        String kind;
-        String type;
-        boolean required;
-        String defaultValue;
-        String description;
-        String group;
-        String enumValues;
-    }
-}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
new file mode 100644
index 000000000000..ea88d61c8153
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -0,0 +1,535 @@
+/*
+ * 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.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+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.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.tooling.model.ArtifactModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+/**
+ * TUI tab showing Camel catalog artifacts (components, data formats, languages, others) that the running integration
+ * uses, based on its declared Maven dependencies.
+ */
+class CatalogTab extends AbstractTableTab {
+
+    private static final String[] SCOPES = { "all", "component", "dataformat", "language", "other" };
+
+    private final AtomicBoolean loading = new AtomicBoolean(false);
+
+    private boolean filterInputActive;
+    private TextInputState filterInputState = new TextInputState("");
+    private String filterTerm;
+    private int scopeIndex;
+    private List<CatalogEntry> allEntries = Collections.emptyList();
+    private List<CatalogEntry> filteredEntries = Collections.emptyList();
+    private String lastPid;
+    private String errorMessage;
+    private boolean dataLoaded;
+
+    CatalogTab(MonitorContext ctx) {
+        super(ctx, "name", "kind", "title");
+    }
+
+    @Override
+    public void onTabSelected() {
+        String pid = ctx.selectedPid;
+        if (pid != null && !pid.equals(lastPid)) {
+            lastPid = pid;
+            allEntries = Collections.emptyList();
+            dataLoaded = false;
+        }
+        if (!dataLoaded) {
+            loadCatalogData();
+        }
+    }
+
+    @Override
+    public void onIntegrationChanged() {
+        allEntries = Collections.emptyList();
+        filteredEntries = Collections.emptyList();
+        filterTerm = null;
+        filterInputActive = false;
+        scopeIndex = 0;
+        lastPid = null;
+        errorMessage = null;
+        dataLoaded = false;
+        loading.set(false);
+        if (ctx.selectedPid != null) {
+            lastPid = ctx.selectedPid;
+            loadCatalogData();
+        }
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (filterInputActive) {
+            return handleFilterInput(ke);
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    protected boolean handleTabKeyEvent(KeyEvent ke) {
+        if (ke.isChar('/')) {
+            filterInputActive = true;
+            filterInputState = new TextInputState(filterTerm != null ? filterTerm : "");
+            return true;
+        }
+        if (ke.isCharIgnoreCase('f')) {
+            scopeIndex = (scopeIndex + 1) % SCOPES.length;
+            refilter();
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handleFilterInput(KeyEvent ke) {
+        if (ke.isKey(KeyCode.ESCAPE)) {
+            filterInputActive = false;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            String text = filterInputState.text().trim();
+            filterTerm = text.isEmpty() ? null : text;
+            filterInputActive = false;
+            refilter();
+            return true;
+        }
+        FormHelper.handleTextInput(ke, filterInputState);
+        return true;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        if (filterTerm != null) {
+            filterTerm = null;
+            refilter();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        return filteredEntries.size();
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        if (loading.get() && allEntries.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled("  Loading catalog...", Style.EMPTY.dim()))))
+                            .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Catalog ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        if (errorMessage != null && allEntries.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled("  " + errorMessage, Theme.error()))))
+                            .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" Catalog ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        renderContent(frame, area, info);
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) {
+        List<CatalogEntry> sorted = new ArrayList<>(filteredEntries);
+        sorted.sort(this::sortEntry);
+
+        List<Row> rows = new ArrayList<>();
+        for (CatalogEntry entry : sorted) {
+            Style nameStyle = entry.deprecated
+                    ? Theme.error().dim()
+                    : Style.EMPTY.fg(Theme.accent());
+            String name = entry.deprecated ? entry.name + " (deprecated)" : entry.name;
+            Style kindStyle = kindStyle(entry.kind);
+            rows.add(Row.from(
+                    Cell.from(Span.styled(" " + name, nameStyle)),
+                    Cell.from(Span.styled(entry.kind, kindStyle)),
+                    Cell.from(entry.title),
+                    Cell.from(Span.styled(entry.label != null ? entry.label : "", Style.EMPTY.dim()))));
+        }
+
+        if (rows.isEmpty() && dataLoaded) {
+            rows.add(emptyRow("No catalog entries found", 4));
+        }
+
+        String scope = SCOPES[scopeIndex];
+        boolean filtered = filterTerm != null || !"all".equals(scope);
+        StringBuilder title = new StringBuilder(" Catalog ");
+        title.append('[');
+        if (filtered) {
+            title.append(filteredEntries.size()).append('/').append(allEntries.size());
+        } else {
+            title.append(filteredEntries.size());
+        }
+        title.append(']');
+        if (!"all".equals(scope)) {
+            title.append(" scope:").append(scope);
+        }
+        if (filterTerm != null) {
+            title.append(" filter:\"").append(filterTerm).append('"');
+        }
+        title.append(' ');
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(" " + sortLabel("NAME", "name"), sortStyle("name"))),
+                        Cell.from(Span.styled(sortLabel("KIND", "kind"), sortStyle("kind"))),
+                        Cell.from(Span.styled(sortLabel("TITLE", "title"), sortStyle("title"))),
+                        Cell.from(Span.styled("LABEL", Style.EMPTY.bold()))))
+                .widths(
+                        Constraint.length(30),
+                        Constraint.length(12),
+                        Constraint.length(30),
+                        Constraint.fill())
+                .highlightStyle(Theme.selectionBg())
+                .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title.toString()).build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        if (filterInputActive) {
+            spans.add(Span.styled(" /", Theme.label().bold()));
+            spans.add(Span.raw(filterInputState.text() + "█  "));
+            hint(spans, "Enter", "filter");
+            hintLast(spans, "Esc", "cancel");
+            return;
+        }
+        hint(spans, "Esc", filterTerm != null ? "clear" : "back");
+        hint(spans, "s", "sort");
+        hint(spans, "f", "scope [" + SCOPES[scopeIndex] + "]");
+        if (filterTerm != null) {
+            spans.add(Span.styled("  /", Theme.label().bold()));
+            spans.add(Span.raw("\"" + filterTerm + "\"  "));
+        } else {
+            hint(spans, "/", "filter");
+        }
+        hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
+    }
+
+    private int sortEntry(CatalogEntry a, CatalogEntry b) {
+        int result = switch (sort) {
+            case "kind" -> a.kind.compareToIgnoreCase(b.kind);
+            case "title" -> a.title.compareToIgnoreCase(b.title);
+            default -> a.name.compareToIgnoreCase(b.name); // "name"
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static Style kindStyle(String kind) {
+        return switch (kind) {
+            case "component" -> Style.EMPTY.fg(Theme.accent());
+            case "dataformat" -> Theme.success();
+            case "language" -> Theme.warning();
+            default -> Style.EMPTY.dim();
+        };
+    }
+
+    // ---- Data Loading ----
+
+    private void loadCatalogData() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || ctx.runner == null) {
+            return;
+        }
+        if (!loading.compareAndSet(false, true)) {
+            return;
+        }
+
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                DependencyLoader.LoadResult result = DependencyLoader.loadDependencies(info);
+                if (result.error() != null && result.entries().isEmpty()) {
+                    applyResult(Collections.emptyList(), result.error());
+                    return;
+                }
+
+                Set<String> appArtifacts = new HashSet<>();
+                for (DependencyLoader.DepEntry dep : result.entries()) {
+                    if (dep.isCamel()) {
+                        appArtifacts.add(dep.groupId() + ":" + dep.artifactId());
+                    }
+                }
+
+                CamelCatalog catalog = CatalogLoader.loadCatalog(null, info.camelVersion, true);
+                if (catalog == null) {
+                    applyResult(Collections.emptyList(), "Could not load catalog for version " + info.camelVersion);
+                    return;
+                }
+
+                List<CatalogEntry> entries = new ArrayList<>();
+                collectArtifacts(catalog, "component", catalog.findComponentNames(), appArtifacts, entries);
+                collectArtifacts(catalog, "dataformat", catalog.findDataFormatNames(), appArtifacts, entries);
+                collectArtifacts(catalog, "language", catalog.findLanguageNames(), appArtifacts, entries);
+                collectArtifacts(catalog, "other", catalog.findOtherNames(), appArtifacts, entries);
+
+                entries.sort((a, b) -> a.name.compareToIgnoreCase(b.name));
+                applyResult(entries, null);
+            } catch (Exception e) {
+                applyResult(Collections.emptyList(), "Error: " + e.getMessage());
+            } finally {
+                loading.set(false);
+            }
+        });
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void collectArtifacts(
+            CamelCatalog catalog, String kind, List<String> names,
+            Set<String> appArtifacts, List<CatalogEntry> entries) {
+        for (String name : names) {
+            try {
+                ArtifactModel<?> model = (ArtifactModel<?>) switch (kind) {
+                    case "component" -> catalog.componentModel(name);
+                    case "dataformat" -> catalog.dataFormatModel(name);
+                    case "language" -> catalog.languageModel(name);
+                    case "other" -> catalog.otherModel(name);
+                    default -> null;
+                };
+                if (model == null) {
+                    continue;
+                }
+                String ga = model.getGroupId() + ":" + model.getArtifactId();
+                if (appArtifacts.contains(ga)) {
+                    CatalogEntry entry = new CatalogEntry();
+                    entry.name = model.getName();
+                    entry.kind = kind;
+                    entry.title = model.getTitle() != null ? model.getTitle() : name;
+                    entry.description = model.getDescription() != null ? model.getDescription() : "";
+                    entry.label = model.getLabel();
+                    entry.artifactId = model.getArtifactId();
+                    entry.deprecated = model.isDeprecated();
+                    entries.add(entry);
+                }
+            } catch (Exception e) {
+                // skip unparseable entries
+            }
+        }
+    }
+
+    private void applyResult(List<CatalogEntry> entries, String error) {
+        if (ctx.runner == null) {
+            return;
+        }
+        ctx.runner.runOnRenderThread(() -> {
+            allEntries = entries;
+            errorMessage = error;
+            dataLoaded = true;
+            refilter();
+        });
+    }
+
+    private void refilter() {
+        List<CatalogEntry> result = new ArrayList<>();
+        String ft = filterTerm != null ? filterTerm.toLowerCase() : null;
+        String scope = SCOPES[scopeIndex];
+        for (CatalogEntry entry : allEntries) {
+            if (!"all".equals(scope) && !scope.equals(entry.kind)) {
+                continue;
+            }
+            if (ft != null
+                    && !entry.name.toLowerCase().contains(ft)
+                    && !entry.title.toLowerCase().contains(ft)
+                    && !(entry.label != null && entry.label.toLowerCase().contains(ft))) {
+                continue;
+            }
+            result.add(entry);
+        }
+        filteredEntries = result;
+        if (!filteredEntries.isEmpty()) {
+            tableState.select(0);
+        }
+    }
+
+    @Override
+    public boolean setFilter(String filter) {
+        filterTerm = filter != null && !filter.isEmpty() ? filter : null;
+        refilter();
+        return true;
+    }
+
+    @Override
+    public boolean setInputValue(String field, String value) {
+        if ("filter".equals(field)) {
+            return setFilter(value);
+        }
+        return false;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        if (filteredEntries.isEmpty()) {
+            return null;
+        }
+        List<CatalogEntry> sorted = new ArrayList<>(filteredEntries);
+        sorted.sort(this::sortEntry);
+        List<String> items = sorted.stream().map(e -> e.name).toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, items.size(), "Catalog");
+    }
+
+    @Override
+    public String description() {
+        return "Camel catalog artifacts used by the integration";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Catalog
+
+                The Catalog tab shows which Camel catalog artifacts (components, data
+                formats, languages, and others) the running integration uses, based on
+                its declared Maven dependencies.
+
+                For each Camel dependency in the integration, the tab cross-references
+                the Camel catalog to identify all artifacts provided by that dependency.
+                For example, `camel-core` provides the `direct`, `seda`, `timer`, `bean`,
+                `log`, and `mock` components, while `camel-kafka` provides the `kafka`
+                component.
+
+                ## Table Columns
+
+                - **NAME** — The catalog artifact name (e.g., `kafka`, `timer`, `json-jackson`)
+                - **KIND** — The artifact type: `component`, `dataformat`, `language`, or `other`
+                - **TITLE** — Human-readable title (e.g., "Apache Kafka", "Timer")
+                - **LABEL** — Category labels (e.g., "messaging", "scheduling", "transformation")
+
+                Deprecated artifacts are shown dimmed with a "(deprecated)" suffix.
+
+                ## Scope
+
+                Press `f` to cycle the scope filter:
+
+                - **all** — show all catalog artifacts (default)
+                - **component** — show only components
+                - **dataformat** — show only data formats
+                - **language** — show only expression languages
+                - **other** — show only miscellaneous artifacts
+
+                ## Filter
+
+                Press `/` to open the filter input. Type a search term and press
+                `Enter` to filter by substring match on name, title, or label.
+
+                ## Keys
+
+                - `s` — cycle sort column (name, kind, title)
+                - `S` — reverse sort order
+                - `f` — cycle scope
+                - `/` — open filter
+                - `Esc` — clear filter or back
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        if (filteredEntries.isEmpty()) {
+            return null;
+        }
+        List<CatalogEntry> sorted = new ArrayList<>(filteredEntries);
+        sorted.sort(this::sortEntry);
+        JsonObject result = new JsonObject();
+        result.put("tab", "Catalog");
+        JsonArray rows = new JsonArray();
+        for (CatalogEntry e : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("name", e.name);
+            row.put("kind", e.kind);
+            row.put("title", e.title);
+            row.put("description", e.description);
+            if (e.label != null) {
+                row.put("label", e.label);
+            }
+            row.put("artifactId", e.artifactId);
+            if (e.deprecated) {
+                row.put("deprecated", true);
+            }
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", sorted.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+
+    // ---- Data Class ----
+
+    static class CatalogEntry {
+        String name;
+        String kind;
+        String title;
+        String description;
+        String label;
+        String artifactId;
+        boolean deprecated;
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 1a62be602b91..51a991c8404e 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -82,6 +82,7 @@ class TabRegistry {
     private ConfigurationTab configurationTab;
     private BeansTab beansTab;
     private BrowseTab browseTab;
+    private CatalogTab catalogTab;
     private ClasspathTab classpathTab;
     private MavenDependenciesTab mavenDependenciesTab;
     private CveAuditTab cveAuditTab;
@@ -132,6 +133,7 @@ class TabRegistry {
         configurationTab = new ConfigurationTab(ctx);
         beansTab = new BeansTab(ctx);
         browseTab = new BrowseTab(ctx);
+        catalogTab = new CatalogTab(ctx);
         classpathTab = new ClasspathTab(ctx);
         mavenDependenciesTab = new MavenDependenciesTab(ctx);
         cveAuditTab = new CveAuditTab(ctx);
@@ -154,6 +156,7 @@ class TabRegistry {
         moreTabs = List.of(
                 new MoreTab(TuiIcons.TAB_BEANS, "Beans", "&Beans", beansTab),
                 new MoreTab(TuiIcons.TAB_BROWSE, "Browse", "Bro&wse", browseTab),
+                new MoreTab(TuiIcons.TAB_CATALOG, "Catalog", "Catal&og", catalogTab),
                 new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker", "&Circuit Breaker", circuitBreakerTab),
                 new MoreTab(TuiIcons.TAB_CLASSPATH, "Classpath", "Cl&asspath", classpathTab),
                 new MoreTab(TuiIcons.TAB_CONFIGURATION, "Configuration", "Confi&guration", configurationTab),
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 95237e6e990e..395d0a118149 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
@@ -126,6 +126,7 @@ final class TuiIcons {
     // ---- More submenu tabs ----
     static final String TAB_BEANS = JAVA;
     static final String TAB_BROWSE = MESSAGE;
+    static final String TAB_CATALOG = "📖";
     static final String TAB_CIRCUIT_BREAKER = "⚡";
     static final String TAB_CLASSPATH = BUNDLED;
     static final String TAB_CONFIGURATION = DOCUMENT;
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPlugin.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPlugin.java
index e8eb93466796..d51a01e23aa4 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPlugin.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiPlugin.java
@@ -34,8 +34,7 @@ public class TuiPlugin implements Plugin {
     @Override
     public void customize(CommandLine commandLine, CamelJBangMain main) {
         var cmd = new CommandLine(new TuiCommand(main, classLoader))
-                .addSubcommand("monitor", new CommandLine(new CamelMonitor(main, classLoader)))
-                .addSubcommand("catalog", new CommandLine(new CamelCatalogTui(main, classLoader)));
+                .addSubcommand("monitor", new CommandLine(new CamelMonitor(main, classLoader)));
 
         commandLine.addSubcommand("tui", cmd);
     }