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 2fac10ed617a8..eea628bd75b59 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 @@ -18,32 +18,19 @@ import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Optional; import java.util.Queue; -import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import dev.tamboui.buffer.Buffer; -import dev.tamboui.export.ExportRequest; import dev.tamboui.layout.Constraint; import dev.tamboui.layout.Layout; import dev.tamboui.layout.Rect; @@ -58,19 +45,9 @@ import dev.tamboui.tui.event.Event; import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; -import dev.tamboui.tui.event.KeyModifiers; import dev.tamboui.tui.event.MouseEvent; import dev.tamboui.tui.event.PasteEvent; import dev.tamboui.tui.event.TickEvent; -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.block.Title; -import dev.tamboui.widgets.list.ListItem; -import dev.tamboui.widgets.list.ListState; -import dev.tamboui.widgets.list.ListWidget; -import dev.tamboui.widgets.list.ScrollMode; import dev.tamboui.widgets.paragraph.Paragraph; import dev.tamboui.widgets.tabs.Tabs; import dev.tamboui.widgets.tabs.TabsState; @@ -78,46 +55,28 @@ import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; -import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; -import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; -import org.apache.camel.util.json.Jsoner; import picocli.CommandLine; import picocli.CommandLine.Command; import sun.misc.Signal; import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*; +import static org.apache.camel.dsl.jbang.core.commands.tui.TabRegistry.*; @Command(name = "monitor", description = "Live dashboard for monitoring Camel integrations", sortOptions = false) public class CamelMonitor extends CamelCommand { - private static final long VANISH_DURATION_MS = 6000; private static final long DEFAULT_REFRESH_MS = 100; - private static final int MAX_TRACES = 200; - private static final int NUM_TABS = 10; - // Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is the true minimum private static final int MIN_WIDTH = 88; private static final int MIN_HEIGHT = 24; // Full tab bar (10 labels + 9 " | " dividers) needs 126 chars; use compact below that private static final int TABS_FULL_MIN_WIDTH = 126; - // Tab indices - private static final int TAB_OVERVIEW = 0; - private static final int TAB_LOG = 1; - private static final int TAB_DIAGRAM = 2; - private static final int TAB_ROUTES = 3; - private static final int TAB_ENDPOINTS = 4; - private static final int TAB_HTTP = 5; - private static final int TAB_HEALTH = 6; - private static final int TAB_HISTORY = 7; - private static final int TAB_ERRORS = 8; - private static final int TAB_MORE = 9; - @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") String name = "*"; @@ -141,111 +100,32 @@ public class CamelMonitor extends CamelCommand { int mcpPort = 8123; // State - private final AtomicReference> data = new AtomicReference<>(Collections.emptyList()); - private final AtomicReference> infraData = new AtomicReference<>(Collections.emptyList()); - private final Map vanishing = new ConcurrentHashMap<>(); - private final Map vanishingInfra = new ConcurrentHashMap<>(); private final TabsState tabsState = new TabsState(TAB_OVERVIEW); - - // Sparkline/chart history for all metric families - private final MetricsCollector metrics = new MetricsCollector(); - - // Cached PID list — full process scan throttled to every 2 seconds (1 second in burst mode) - private volatile List cachedPids = Collections.emptyList(); - private volatile long lastFullScanTime; - private volatile long burstModeUntil; - final Set stoppingPids = ConcurrentHashMap.newKeySet(); - - // Trace/history data — shared between CamelMonitor and tabs - private final AtomicReference> traces = new AtomicReference<>(Collections.emptyList()); - private final Map traceFilePositions = new ConcurrentHashMap<>(); - // OTel span data — shared between CamelMonitor and SpansTab - private final AtomicReference> otelSpans = new AtomicReference<>(List.of()); + private TabRegistry tabRegistry; // selectedPid is stored on ctx (MonitorContext) so tabs can access it - private volatile long lastRefresh; - private boolean showKillConfirm; + private DataRefreshService dataService; private String monitorNotification; private boolean monitorNotificationError; private long monitorNotificationExpiry; - private volatile Buffer lastBuffer; - private volatile long renderGeneration; - private volatile String screenshotMessage; - private volatile long screenshotMessageTime; - private volatile boolean pendingScreenshot; - private boolean recording; - private TapeRecorder tapeRecorder; private boolean mcpInjectedKey; - private TuiEventLog eventLog; private TuiMcpServer mcpServer; - private final Queue pendingKeys = new ConcurrentLinkedQueue<>(); - private final List recentKeys = new ArrayList<>(); + private McpFacade mcpFacade; + private final Queue pendingKeys = new ConcurrentLinkedQueue<>(); private final CaptionOverlay captionOverlay = new CaptionOverlay(); + private final RecordingManager recordingManager = new RecordingManager(captionOverlay); private final DrawOverlay drawOverlay = new DrawOverlay(); private final HelpOverlay helpOverlay = new HelpOverlay(); private final ShellPanel shellPanel = new ShellPanel(); - private final ActionsPopup actionsPopup = new ActionsPopup( - () -> data.get().stream() - .filter(i -> !i.vanishing && i.name != null) - .map(i -> i.name) - .collect(Collectors.toSet()), - () -> data.get().stream() - .filter(i -> !i.vanishing) - .collect(Collectors.toList()), - () -> infraData.get().stream() - .filter(i -> !i.vanishing) - .collect(Collectors.toList()), - captionOverlay, - () -> pendingScreenshot = true, - () -> recording = !recording, - () -> recording, - this::toggleTapeRecording, - () -> tapeRecorder != null && tapeRecorder.isActive(), - this::enableBurstMode, stoppingPids); - - private final AtomicBoolean refreshInProgress = new AtomicBoolean(false); + private ActionsPopup actionsPopup; private TuiRunner runner; private MonitorContext ctx; - private LogTab logTab; - private DiagramTab diagramTab; - private RoutesTab routesTab; - private ConsumersTab consumersTab; - private EndpointsTab endpointsTab; - private HttpTab httpTab; - private HealthTab healthTab; - private HistoryTab historyTab; - private CircuitBreakerTab circuitBreakerTab; - private ErrorsTab errorsTab; - private MetricsTab metricsTab; - private StartupTab startupTab; - private ConfigurationTab configurationTab; - private BeansTab beansTab; - private BrowseTab browseTab; - private ClasspathTab classpathTab; - private InflightTab inflightTab; - private MemoryTab memoryTab; - private ThreadsTab threadsTab; - private SpansTab spansTab; - private ProcessTab processTab; - private OverviewTab overviewTab; - private DataSourceTab dataSourceTab; - private SqlQueryTab sqlQueryTab; - - // "Switch integration" popup state - private boolean showSwitchPopup; - private final ListState switchPopupState = new ListState(); private final FilesBrowser filesBrowser = new FilesBrowser(); - - // "More" dropdown state - private boolean showMorePopup; - private final ListState morePopupState = new ListState(); - private MonitorTab activeMoreTab; - private int lastMoreSelection; - private Line[] currentTabLabels; + private PopupManager popupManager; private ClassLoader classLoader; @@ -270,55 +150,243 @@ public Integer doCall() throws Exception { System.setProperty("tamboui.record.fps", "10"); } - recording = record != null; + recordingManager.init(record != null); // to make ServiceLoader work with tamboui for downloaded JARs Thread.currentThread().setContextClassLoader(classLoader); + // Create data refresh service first — tabs and popups reference its state + dataService = new DataRefreshService( + name, + new DataRefreshService.RefreshContext() { + @Override + public int selectedTab() { + return tabRegistry != null ? tabRegistry.selectedTabIndex() : tabsState.selected(); + } + + @Override + public boolean isSwitchPopupVisible() { + return popupManager != null && popupManager.isSwitchPopupVisible(); + } + + @Override + public String getPendingAutoSelect() { + return actionsPopup != null ? actionsPopup.getPendingAutoSelect() : null; + } + + @Override + public void clearPendingAutoSelect() { + if (actionsPopup != null) { + actionsPopup.clearPendingAutoSelect(); + } + } + + @Override + public void onInfraAutoSelected(int tableIndex, String pid) { + if (tabRegistry != null) { + tabRegistry.overviewTab().tableState.select(tableIndex); + } + ctx.selectedPid = pid; + } + + @Override + public boolean isInfraSelected() { + return CamelMonitor.this.isInfraSelected(); + } + }, + this::getStatusFile, + this::getErrorFile); + // Create shared context and tab instances - ctx = new MonitorContext(data, infraData); + ctx = new MonitorContext(dataService.data(), dataService.infraData()); + dataService.setContext(ctx); + + actionsPopup = new ActionsPopup( + () -> dataService.data().get().stream() + .filter(i -> !i.vanishing && i.name != null) + .map(i -> i.name) + .collect(Collectors.toSet()), + () -> dataService.data().get().stream() + .filter(i -> !i.vanishing) + .collect(Collectors.toList()), + () -> dataService.infraData().get().stream() + .filter(i -> !i.vanishing) + .collect(Collectors.toList()), + captionOverlay, + () -> recordingManager.requestScreenshot(), + () -> recordingManager.toggleRecording(), + () -> recordingManager.isRecording(), + () -> recordingManager.toggleTapeRecording(), + () -> recordingManager.isTapeRecording(), + dataService::enableBurstMode, dataService.stoppingPids()); + actionsPopup.setContext(ctx); actionsPopup.setResetStatsAction(this::resetStats); shellPanel.setContext(ctx); actionsPopup.setOpenShellAction(shellPanel::open); actionsPopup.setBrowseFilesAction(this::openFilesPopup); - logTab = new LogTab(ctx); - diagramTab = new DiagramTab(ctx); - routesTab = new RoutesTab(ctx); - consumersTab = new ConsumersTab(ctx); - dataSourceTab = new DataSourceTab(ctx); - sqlQueryTab = new SqlQueryTab(ctx); - endpointsTab = new EndpointsTab(ctx, metrics); - httpTab = new HttpTab(ctx); - healthTab = new HealthTab(ctx); - historyTab = new HistoryTab(ctx, traces, traceFilePositions); - circuitBreakerTab = new CircuitBreakerTab(ctx, metrics); - errorsTab = new ErrorsTab(ctx); - metricsTab = new MetricsTab(ctx); - startupTab = new StartupTab(ctx); - configurationTab = new ConfigurationTab(ctx); - beansTab = new BeansTab(ctx); - browseTab = new BrowseTab(ctx); - classpathTab = new ClasspathTab(ctx); - inflightTab = new InflightTab(ctx); - memoryTab = new MemoryTab(ctx, metrics); - threadsTab = new ThreadsTab(ctx); - spansTab = new SpansTab(ctx, otelSpans); - processTab = new ProcessTab(ctx); - overviewTab = new OverviewTab( - ctx, metrics, stoppingPids, - this::resetIntegrationTabState); + + tabRegistry = new TabRegistry(tabsState); + tabRegistry.initTabs(ctx, dataService, this::resetIntegrationTabState); + tabRegistry.setCallbacks(new TabRegistry.TabCallbacks() { + @Override + public void refreshLogData() { + CamelMonitor.this.refreshLogData(); + } + + @Override + public void refreshHistoryData(List pids) { + dataService.loadHistoryData(pids); + } + + @Override + public void refreshTraceData(List pids) { + dataService.refreshTraceData(pids); + } + + @Override + public void refreshErrorData(List pids) { + dataService.refreshErrorData(pids); + } + + @Override + public void openMorePopup() { + popupManager.openMorePopup(); + } + + @Override + public void closeMorePopup() { + popupManager.closeMorePopup(); + } + + @Override + public void selectMorePopupEntry(int index) { + popupManager.selectMorePopupEntry(index); + } + }); + + popupManager = new PopupManager( + ctx, this::getNonVanishingIntegrations, filesBrowser, + new PopupManager.PopupCallbacks() { + @Override + public void selectMoreTab(int index) { + tabRegistry.selectMoreTab(index); + } + + @Override + public void resetIntegrationTabState() { + CamelMonitor.this.resetIntegrationTabState(); + } + + @Override + public void refreshLogData() { + CamelMonitor.this.refreshLogData(); + } + + @Override + public void stopSelectedProcess(boolean forceKill) { + CamelMonitor.this.stopSelectedProcess(forceKill); + } + }); + + tabRegistry.overviewTab().setActions(new OverviewTab.OverviewActions() { + @Override + public void sendRouteCommand(String pid, String routeId, String command) { + CamelMonitor.this.sendRouteCommand(pid, routeId, command); + } + + @Override + public void stopSelectedProcess(boolean forceKill) { + CamelMonitor.this.stopSelectedProcess(forceKill); + } + + @Override + public void restartSelectedProcess() { + CamelMonitor.this.restartSelectedProcess(); + } + + @Override + public void showKillConfirm() { + popupManager.showKillConfirm(); + } + + @Override + public void openDoc(IntegrationInfo info) { + actionsPopup.openDoc(info); + } + + @Override + public void openFilesPopup() { + CamelMonitor.this.openFilesPopup(); + } + }); // Initial data load (synchronous before TUI starts) - refreshDataSync(); + dataService.refreshSync(this::refreshLogData, this::refreshConditionalData); // Auto-select if there's exactly one integration running - overviewTab.selectCurrentIntegration(); + tabRegistry.overviewTab().selectCurrentIntegration(); + + mcpFacade = new McpFacade( + ctx, dataService.data(), tabsState, recordingManager, + captionOverlay, drawOverlay, helpOverlay, + actionsPopup, filesBrowser, + tabRegistry, + pendingKeys, + new McpFacade.MonitorBridge() { + @Override + public MonitorTab activeTab() { + return tabRegistry.activeTab(); + } + + @Override + public void handleTabKey(int tabIndex) { + tabRegistry.handleTabKey(tabIndex, ctx, dataService); + } + + @Override + public void selectMoreTab(int moreIndex) { + tabRegistry.selectMoreTab(moreIndex); + } + + @Override + public boolean isSwitchPopupVisible() { + return popupManager.isSwitchPopupVisible(); + } - eventLog = new TuiEventLog(500); + @Override + public boolean isMorePopupVisible() { + return popupManager.isMorePopupVisible(); + } + + @Override + public void renderOverviewFooter(List spans) { + CamelMonitor.this.renderOverviewFooter(spans); + } + + @Override + public void insertFKeyHints(List spans) { + CamelMonitor.this.insertFKeyHints(spans); + } + + @Override + public void sendRouteCommand(String pid, String routeId, String command) { + CamelMonitor.this.sendRouteCommand(pid, routeId, command); + } + + @Override + public void restartProcess() { + restartSelectedProcess(); + } + + @Override + public void stopProcess(boolean forceKill) { + stopSelectedProcess(forceKill); + } + }); Path mcpJsonFile = null; if (mcp) { - mcpServer = new TuiMcpServer(mcpPort, this); + mcpServer = new TuiMcpServer(mcpPort, mcpFacade); try { mcpServer.start(); actionsPopup.setMcpEnabled(true, mcpPort, mcpServer::getConnectedClient, mcpServer::getActivityLog); @@ -337,8 +405,8 @@ public Integer doCall() throws Exception { actionsPopup.setScheduler(tui.scheduler()); actionsPopup.setResetScreenAction(() -> tui.terminal().clear()); // Preload diagram data if an integration was auto-selected - routesTab.preloadDiagram(); - diagramTab.preloadDiagram(); + tabRegistry.routesTab().preloadDiagram(); + tabRegistry.diagramTab().preloadDiagram(); // 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()); @@ -360,35 +428,18 @@ public Integer doCall() throws Exception { private boolean handleEvent(Event event, TuiRunner runner) { if (event instanceof KeyEvent ke) { - if (eventLog != null) { - String elabel = keyLabel(ke); - if (elabel != null) { - eventLog.record(elabel, elabel); - } - } - if (recording) { - String label = keyLabel(ke); - if (label != null) { - recentKeys.add(new KeyRecord(label, System.currentTimeMillis())); - } - } + recordingManager.recordKey(ke, mcpInjectedKey); if (ke.hasCtrl() && ke.isChar('r')) { - toggleTapeRecording(); + recordingManager.toggleTapeRecording(); return true; } - if (tapeRecorder != null && tapeRecorder.isActive() && !mcpInjectedKey) { - String label = keyLabel(ke); - if (label != null) { - tapeRecorder.recordKey(label); - } - } if (captionOverlay.isVisible()) { if (captionOverlay.handleKeyEvent(ke)) { return true; } } if (ke.hasCtrl() && ke.isChar('k')) { - recording = !recording; + recordingManager.toggleRecording(); return true; } if (ke.hasCtrl() && ke.isChar('t')) { @@ -409,7 +460,7 @@ private boolean handleEvent(Event event, TuiRunner runner) { if (actionsPopup.isVisible()) { return actionsPopup.handleKeyEvent(ke); } - if (handlePopupKeys(ke)) { + if (popupManager.handleKeyEvent(ke, tabRegistry.selectedTabIndex(), TAB_LOG)) { return true; } if (handleGlobalKeys(ke, runner)) { @@ -433,109 +484,13 @@ private boolean handleEvent(Event event, TuiRunner runner) { return false; } - private boolean handlePopupKeys(KeyEvent ke) { - if (filesBrowser.isVisible()) { - return filesBrowser.handleKeyEvent(ke); - } - if (showMorePopup) { - if (ke.isCancel()) { - showMorePopup = false; - return true; - } - if (ke.isUp()) { - morePopupState.selectPrevious(); - return true; - } - if (ke.isDown()) { - morePopupState.selectNext(15); - return true; - } - int shortcutSel = morePopupShortcut(ke); - if (shortcutSel >= 0) { - morePopupState.select(shortcutSel); - } - if (ke.isConfirm() || shortcutSel >= 0) { - showMorePopup = false; - Integer sel = shortcutSel >= 0 ? shortcutSel : morePopupState.selected(); - if (sel != null) { - lastMoreSelection = sel; - activeMoreTab = switch (sel) { - case 0 -> beansTab; - case 1 -> browseTab; - case 2 -> circuitBreakerTab; - case 3 -> classpathTab; - case 4 -> configurationTab; - case 5 -> consumersTab; - case 6 -> dataSourceTab; - case 7 -> inflightTab; - case 8 -> memoryTab; - case 9 -> metricsTab; - case 10 -> sqlQueryTab; - case 11 -> spansTab; - case 12 -> processTab; - case 13 -> startupTab; - case 14 -> threadsTab; - default -> null; - }; - if (activeMoreTab != null) { - overviewTab.selectCurrentIntegration(); - tabsState.select(TAB_MORE); - activeMoreTab.onTabSelected(); - } - } - return true; - } - return true; - } - if (showSwitchPopup) { - if (ke.isCancel()) { - showSwitchPopup = false; - return true; - } - List switchList = getNonVanishingIntegrations(); - if (ke.isUp()) { - switchPopupState.selectPrevious(); - return true; - } - if (ke.isDown()) { - switchPopupState.selectNext(switchList.size()); - return true; - } - if (ke.isConfirm()) { - showSwitchPopup = false; - Integer sel = switchPopupState.selected(); - if (sel != null && sel >= 0 && sel < switchList.size()) { - IntegrationInfo chosen = switchList.get(sel); - ctx.selectedPid = chosen.pid; - ctx.lastSelectedName = chosen.name; - resetIntegrationTabState(); - if (tabsState.selected() == TAB_LOG) { - refreshLogData(); - } - } - return true; - } - return true; - } - if (showKillConfirm) { - if (ke.isConfirm()) { - showKillConfirm = false; - stopSelectedProcess(true); - } else { - showKillConfirm = false; - } - return true; - } - return false; - } - private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) { if (ke.isCancel()) { - MonitorTab tab = activeTab(); + MonitorTab tab = tabRegistry.activeTab(); if (tab != null && tab.handleEscape()) { return true; } - if (tabsState.selected() != TAB_OVERVIEW) { + if (tabRegistry.selectedTabIndex() != TAB_OVERVIEW) { tabsState.select(TAB_OVERVIEW); return true; } @@ -546,12 +501,16 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) { } return true; } - boolean probeEditing = tabsState.selected() == TAB_HTTP && httpTab.isProbeMode(); - boolean logSearchActive = tabsState.selected() == TAB_LOG && logTab.isSearchInputActive(); - boolean spanFilterActive = tabsState.selected() == TAB_MORE && activeMoreTab == spansTab - && spansTab.isFilterInputActive(); - boolean sqlInputActive = tabsState.selected() == TAB_MORE && activeMoreTab == sqlQueryTab - && sqlQueryTab.isInputActive(); + boolean probeEditing = tabRegistry.selectedTabIndex() == TAB_HTTP + && tabRegistry.httpTab().isProbeMode(); + boolean logSearchActive = tabRegistry.selectedTabIndex() == TAB_LOG + && tabRegistry.logTab().isSearchInputActive(); + boolean spanFilterActive = tabRegistry.selectedTabIndex() == TAB_MORE + && tabRegistry.getActiveMoreTab() == tabRegistry.spansTab() + && tabRegistry.spansTab().isFilterInputActive(); + boolean sqlInputActive = tabRegistry.selectedTabIndex() == TAB_MORE + && tabRegistry.getActiveMoreTab() == tabRegistry.sqlQueryTab() + && tabRegistry.sqlQueryTab().isInputActive(); boolean textEditing = probeEditing || logSearchActive || spanFilterActive || sqlInputActive; if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) { runner.quit(); @@ -563,46 +522,46 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) { } if (!textEditing) { if (ke.isChar('1')) { - return handleTabKey(TAB_OVERVIEW); + return tabRegistry.handleTabKey(TAB_OVERVIEW, ctx, dataService); } if (ke.isChar('2')) { - return handleTabKey(TAB_LOG); + return tabRegistry.handleTabKey(TAB_LOG, ctx, dataService); } if (!isInfraSelected()) { if (ke.isChar('3')) { - return handleTabKey(TAB_DIAGRAM); + return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx, dataService); } if (ke.isChar('4')) { - return handleTabKey(TAB_ROUTES); + return tabRegistry.handleTabKey(TAB_ROUTES, ctx, dataService); } if (ke.isChar('5')) { - return handleTabKey(TAB_ENDPOINTS); + return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx, dataService); } if (ke.isChar('6')) { - return handleTabKey(TAB_HTTP); + return tabRegistry.handleTabKey(TAB_HTTP, ctx, dataService); } if (ke.isChar('7')) { - return handleTabKey(TAB_HEALTH); + return tabRegistry.handleTabKey(TAB_HEALTH, ctx, dataService); } if (ke.isChar('8')) { - return handleTabKey(TAB_HISTORY); + return tabRegistry.handleTabKey(TAB_HISTORY, ctx, dataService); } if (ke.isChar('9')) { - return handleTabKey(TAB_ERRORS); + return tabRegistry.handleTabKey(TAB_ERRORS, ctx, dataService); } if (ke.isChar('0')) { - return handleTabKey(TAB_MORE); + return tabRegistry.handleTabKey(TAB_MORE, ctx, dataService); } } } if (ke.isFocusPrevious() && !textEditing) { if (isInfraSelected()) { - int prev = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; + int prev = tabRegistry.selectedTabIndex() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; tabsState.select(prev); } else { - int prev = (tabsState.selected() - 1 + NUM_TABS) % NUM_TABS; + int prev = (tabRegistry.selectedTabIndex() - 1 + NUM_TABS) % NUM_TABS; if (prev != TAB_OVERVIEW) { - overviewTab.selectCurrentIntegration(); + tabRegistry.overviewTab().selectCurrentIntegration(); } tabsState.select(prev); } @@ -610,25 +569,25 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) { } if (ke.isFocusNext() && !textEditing) { if (isInfraSelected()) { - int next = tabsState.selected() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; + int next = tabRegistry.selectedTabIndex() == TAB_OVERVIEW ? TAB_LOG : TAB_OVERVIEW; tabsState.select(next); } else { - int next = (tabsState.selected() + 1) % NUM_TABS; + int next = (tabRegistry.selectedTabIndex() + 1) % NUM_TABS; if (next != TAB_OVERVIEW) { - overviewTab.selectCurrentIntegration(); + tabRegistry.overviewTab().selectCurrentIntegration(); } tabsState.select(next); } return true; } if (ke.isKey(KeyCode.F5) && ke.hasShift()) { - takeScreenshot(); + recordingManager.takeScreenshot(); return true; } if (opensHelp(ke, textEditing)) { // Only opens the overlay: while it is visible, dispatch delegates to // helpOverlay.handleKeyEvent (which handles F1/?/q/Esc to close) before reaching here. - MonitorTab tab = activeTab(); + MonitorTab tab = tabRegistry.activeTab(); if (tab != null) { String help = tab.getHelpText(); if (help != null) { @@ -646,23 +605,14 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner runner) { return true; } if (ke.isKey(KeyCode.F2)) { - if (tabsState.selected() == TAB_ROUTES && routesTab != null) { - actionsPopup.setPreSelectedRouteId(routesTab.selectedRouteId()); + if (tabRegistry.selectedTabIndex() == TAB_ROUTES && tabRegistry.routesTab() != null) { + actionsPopup.setPreSelectedRouteId(tabRegistry.routesTab().selectedRouteId()); } actionsPopup.open(); return true; } if (ke.isKey(KeyCode.F3)) { - List switchList = getNonVanishingIntegrations(); - if (switchList.size() > 1) { - showSwitchPopup = true; - for (int i = 0; i < switchList.size(); i++) { - if (switchList.get(i).pid.equals(ctx.selectedPid)) { - switchPopupState.select(i); - break; - } - } - } + popupManager.openSwitchPopup(ctx.selectedPid, getNonVanishingIntegrations()); return true; } return false; @@ -677,20 +627,20 @@ static boolean opensHelp(KeyEvent ke, boolean textEditing) { } private boolean handleTabKeys(KeyEvent ke) { - MonitorTab activeTab = activeTab(); + MonitorTab activeTab = tabRegistry.activeTab(); if (ke.isUp()) { if (activeTab != null && activeTab.handleKeyEvent(ke)) { return true; } - navigateUp(); + tabRegistry.navigateUp(); return true; } if (ke.isDown()) { if (activeTab != null && activeTab.handleKeyEvent(ke)) { return true; } - navigateDown(); + tabRegistry.navigateDown(); return true; } if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) { @@ -726,46 +676,15 @@ private boolean handleTabKeys(KeyEvent ke) { } } - int tab = tabsState.selected(); + int tab = tabRegistry.selectedTabIndex(); if (ke.isConfirm() && tab == TAB_OVERVIEW) { - overviewTab.selectCurrentIntegration(); + tabRegistry.overviewTab().selectCurrentIntegration(); if (ctx.selectedPid != null) { tabsState.select(TAB_LOG); refreshLogData(); } return true; } - if (tab == TAB_OVERVIEW && ke.isChar('p') && ctx.selectedPid != null && !isInfraSelected()) { - IntegrationInfo selInfo = findSelectedIntegration(); - if (selInfo != null) { - String cmd = selInfo.routeStarted > 0 ? "stop" : "start"; - sendRouteCommand(ctx.selectedPid, "*", cmd); - } - return true; - } - if (tab == TAB_OVERVIEW && ke.isChar('x') && ctx.selectedPid != null) { - stopSelectedProcess(false); - return true; - } - if (tab == TAB_OVERVIEW && ke.isChar('X') && ctx.selectedPid != null) { - showKillConfirm = true; - return true; - } - if (tab == TAB_OVERVIEW && ke.isChar('r') && ctx.selectedPid != null && !isInfraSelected()) { - restartSelectedProcess(); - return true; - } - if (tab == TAB_OVERVIEW && ke.isChar('d') && ctx.selectedPid != null && !isInfraSelected()) { - IntegrationInfo selInfo = findSelectedIntegration(); - if (selInfo != null && selInfo.readmeFiles != null && !selInfo.readmeFiles.isEmpty()) { - actionsPopup.openDoc(selInfo); - return true; - } - } - if (tab == TAB_OVERVIEW && ke.isChar('f') && ctx.selectedPid != null && !isInfraSelected()) { - openFilesPopup(); - return true; - } if (activeTab != null && activeTab.handleKeyEvent(ke)) { return true; } @@ -777,20 +696,21 @@ private boolean handlePasteEvent(PasteEvent pe) { actionsPopup.handlePaste(pe.text()); return true; } - if (httpTab.isProbeMode()) { - httpTab.handlePaste(pe.text()); + if (tabRegistry.httpTab().isProbeMode()) { + tabRegistry.httpTab().handlePaste(pe.text()); return true; } - if (logTab.isSearchInputActive()) { - logTab.handlePaste(pe.text()); + if (tabRegistry.logTab().isSearchInputActive()) { + tabRegistry.logTab().handlePaste(pe.text()); return true; } if (filesBrowser.isSourceViewerPasteActive()) { filesBrowser.handlePaste(pe.text()); return true; } - if (activeMoreTab == sqlQueryTab && sqlQueryTab.isInputActive()) { - sqlQueryTab.handlePaste(pe.text()); + if (tabRegistry.getActiveMoreTab() == tabRegistry.sqlQueryTab() + && tabRegistry.sqlQueryTab().isInputActive()) { + tabRegistry.sqlQueryTab().handlePaste(pe.text()); return true; } return false; @@ -799,7 +719,7 @@ private boolean handlePasteEvent(PasteEvent pe) { private boolean handleTickEvent(TuiRunner runner) { long now = System.currentTimeMillis(); boolean keyProcessed = false; - PendingKey pk; + McpFacade.PendingKey pk; while ((pk = pendingKeys.peek()) != null && now >= pk.fireAt()) { pendingKeys.poll(); mcpInjectedKey = true; @@ -813,171 +733,21 @@ private boolean handleTickEvent(TuiRunner runner) { actionsPopup.tick(now); drawOverlay.tick(now); captionOverlay.tick(now); - if (recording && !recentKeys.isEmpty()) { - long cutoff = now - 2000; - recentKeys.removeIf(k -> k.timestamp() < cutoff); - } - boolean anyDiagramShowing = routesTab.isShowDiagram() || diagramTab.isShowDiagram(); + recordingManager.tickRecentKeys(now); + boolean anyDiagramShowing = tabRegistry.routesTab().isShowDiagram() + || tabRegistry.diagramTab().isShowDiagram(); long interval = anyDiagramShowing ? Math.max(refreshInterval, 1000) : refreshInterval; - if (now - lastRefresh >= interval) { - refreshData(); - routesTab.refreshDiagramIfNeeded(); - diagramTab.refreshDiagramIfNeeded(); - return true; - } - return true; - } - - private String keyLabel(KeyEvent ke) { - if (ke.isKey(KeyCode.ENTER)) { - return "Enter"; - } - if (ke.isKey(KeyCode.ESCAPE)) { - return "Esc"; - } - if (ke.isKey(KeyCode.TAB)) { - return ke.hasShift() ? "⇧Tab" : "Tab"; - } - if (ke.isKey(KeyCode.UP)) { - return "↑"; - } - if (ke.isKey(KeyCode.DOWN)) { - return "↓"; - } - if (ke.isKey(KeyCode.LEFT)) { - return "←"; - } - if (ke.isKey(KeyCode.RIGHT)) { - return "→"; - } - if (ke.isKey(KeyCode.PAGE_UP)) { - return "PgUp"; - } - if (ke.isKey(KeyCode.PAGE_DOWN)) { - return "PgDn"; - } - if (ke.isKey(KeyCode.HOME)) { - return "Home"; - } - if (ke.isKey(KeyCode.END)) { - return "End"; - } - if (ke.isKey(KeyCode.BACKSPACE)) { - return "⌫"; - } - for (int i = 1; i <= 12; i++) { - try { - KeyCode fKey = KeyCode.valueOf("F" + i); - if (ke.isKey(fKey)) { - return "F" + i; - } - } catch (IllegalArgumentException e) { - break; - } - } - if (ke.code() == KeyCode.CHAR) { - String s = ke.string(); - if (" ".equals(s)) { - return "Space"; - } - if (!s.isEmpty()) { - return s; - } - } - return null; - } - - private boolean handleTabKey(int tab) { - if (tab != TAB_OVERVIEW) { - overviewTab.selectCurrentIntegration(); - routesTab.preloadDiagram(); - diagramTab.preloadDiagram(); - } - if (tab == TAB_LOG) { - refreshLogData(); - logTab.onTabSelected(); - } - if (tab == TAB_ROUTES && routesTab != null && routesTab.isShowDiagram()) { - routesTab.closeDiagram(); - } - if (tab == TAB_DIAGRAM) { - diagramTab.onTabSelected(); - } - if (tab == TAB_HISTORY && ctx.selectedPid != null) { - try { - long pid = Long.parseLong(ctx.selectedPid); - refreshHistoryData(List.of(pid)); - refreshTraceData(List.of(pid)); - } catch (NumberFormatException e) { - // ignore - } - historyTab.onTabSelected(); - } - if (tab == TAB_ERRORS && ctx.selectedPid != null) { - try { - long pid = Long.parseLong(ctx.selectedPid); - refreshErrorData(List.of(pid)); - } catch (NumberFormatException e) { - // ignore - } - errorsTab.onTabSelected(); - } - if (tab == TAB_MORE) { - showMorePopup = !showMorePopup; - if (showMorePopup) { - morePopupState.select(lastMoreSelection); - } + if (now - dataService.lastRefresh() >= interval) { + dataService.refresh(runner, this::refreshLogData, this::refreshConditionalData); + tabRegistry.routesTab().refreshDiagramIfNeeded(); + tabRegistry.diagramTab().refreshDiagramIfNeeded(); return true; } - showMorePopup = false; - tabsState.select(tab); return true; } - private List selectedPidAsList() { - if (ctx.selectedPid == null) { - return Collections.emptyList(); - } - try { - return List.of(Long.parseLong(ctx.selectedPid)); - } catch (NumberFormatException e) { - return Collections.emptyList(); - } - } - private void resetIntegrationTabState() { - diagramTab.onIntegrationChanged(); - routesTab.onIntegrationChanged(); - httpTab.onIntegrationChanged(); - logTab.onIntegrationChanged(); - historyTab.onIntegrationChanged(); - beansTab.onIntegrationChanged(); - browseTab.onIntegrationChanged(); - threadsTab.onIntegrationChanged(); - startupTab.onIntegrationChanged(); - configurationTab.onIntegrationChanged(); - consumersTab.onIntegrationChanged(); - dataSourceTab.onIntegrationChanged(); - sqlQueryTab.onIntegrationChanged(); - circuitBreakerTab.onIntegrationChanged(); - inflightTab.onIntegrationChanged(); - spansTab.onIntegrationChanged(); - processTab.onIntegrationChanged(); - otelSpans.set(List.of()); - - filesBrowser.reset(); - - // Preload diagram data in background so it's ready when the user switches tabs - routesTab.preloadDiagram(); - diagramTab.preloadDiagram(); - } - - private void navigateUp() { - activeTab().navigateUp(); - } - - private void navigateDown() { - activeTab().navigateDown(); + tabRegistry.resetIntegrationTabState(dataService, filesBrowser); } // ---- Rendering ---- @@ -1017,8 +787,8 @@ private void render(Frame frame) { if (drawOverlay.isVisible()) { drawOverlay.render(frame, contentArea); } - if (showKillConfirm) { - renderKillConfirm(frame, contentArea); + if (popupManager.isKillConfirmVisible()) { + popupManager.renderKillConfirm(frame, contentArea); } actionsPopup.render(frame, contentArea); if (captionOverlay.isCaptionVisible()) { @@ -1029,17 +799,12 @@ private void render(Frame frame) { } renderFooter(frame, mainChunks.get(3)); - lastBuffer = frame.buffer(); - renderGeneration++; - - if (pendingScreenshot) { - pendingScreenshot = false; - takeScreenshot(); - } + recordingManager.updateBuffer(frame.buffer()); + recordingManager.processPendingScreenshot(); } private void renderHeader(Frame frame, Rect area) { - List infos = data.get(); + List infos = dataService.data().get(); String camelVersion = VersionHelper.extractCamelVersion(); long activeCount = infos.stream().filter(i -> !i.vanishing).count(); @@ -1049,7 +814,7 @@ private void renderHeader(Frame frame, Rect area) { titleSpans.add(Span.styled(camelVersion != null ? "v" + camelVersion : "", Style.EMPTY.fg(Color.GREEN))); titleSpans.add(Span.raw(" ")); titleSpans.add(Span.styled(activeCount + " integration(s)", Style.EMPTY.fg(Color.CYAN))); - long activeInfra = infraData.get().stream().filter(i -> !i.vanishing).count(); + long activeInfra = dataService.infraData().get().stream().filter(i -> !i.vanishing).count(); if (activeInfra > 0) { titleSpans.add(Span.raw(" ")); titleSpans.add(Span.styled(activeInfra + " infra(s)", Style.EMPTY.fg(Color.MAGENTA))); @@ -1163,7 +928,7 @@ private void renderTabs(Frame frame, Rect area) { Line.from("1 Overview"), Line.from("2 Log"), Line.from("3 Diagram"), - Line.from(routesTab.isTopMode() ? "4 Top " : "4 Route"), + Line.from(tabRegistry.routesTab().isTopMode() ? "4 Top " : "4 Route"), Line.from("5 Endpoint"), Line.from("6 HTTP"), Line.from("7 Health"), @@ -1175,7 +940,7 @@ private void renderTabs(Frame frame, Rect area) { Line.from(" 1 Overview "), Line.from(" 2 Log "), Line.from(" 3 Diagram "), - Line.from(routesTab.isTopMode() ? " 4 Top " : " 4 Route "), + Line.from(tabRegistry.routesTab().isTopMode() ? " 4 Top " : " 4 Route "), Line.from(" 5 Endpoint "), Line.from(" 6 HTTP "), Line.from(" 7 Health "), @@ -1183,7 +948,7 @@ private void renderTabs(Frame frame, Rect area) { Line.from(" 9 Errors "), Line.from(" 0 More▾ "), }; - currentTabLabels = labels; + popupManager.setCurrentTabLabels(labels); Tabs tabs = Tabs.builder() .titles(labels) @@ -1224,15 +989,15 @@ private void renderContent(Frame frame, Rect area) { // from the previous tab (e.g. RED error text in the log detail) can bleed through when // switching tabs if TamboUI's buffer diff does not reset every cell in the region. frame.buffer().clear(area); - MonitorTab tab = activeTab(); + MonitorTab tab = tabRegistry.activeTab(); tab.render(frame, area); // Render "More" popup overlay when visible - if (showMorePopup) { - renderMorePopup(frame, area); + if (popupManager.isMorePopupVisible()) { + popupManager.renderMorePopup(frame, area); } // Render "Switch integration" popup overlay when visible - if (showSwitchPopup) { - renderSwitchPopup(frame, area); + if (popupManager.isSwitchPopupVisible()) { + popupManager.renderSwitchPopup(frame, area); } // Render "Files" popup overlay when visible if (filesBrowser.isVisible()) { @@ -1249,7 +1014,7 @@ private void computeTabBadges(String[] badgeTexts, Style[] badgeStyles) { badgeStyles[j] = yellow; } - List infos = data.get(); + List infos = dataService.data().get(); long activeCount = infos.stream().filter(i -> !i.vanishing).count(); IntegrationInfo sel = findSelectedIntegration(); boolean hasSelection = ctx.selectedPid != null && sel != null; @@ -1281,13 +1046,13 @@ private void computeTabBadges(String[] badgeTexts, Style[] badgeStyles) { badgeTexts[TAB_HEALTH] = "(" + healthCount + ")"; } } - boolean hasTraces = hasSelection && !traces.get().isEmpty(); + boolean hasTraces = hasSelection && !dataService.traces().get().isEmpty(); if (hasTraces) { badgeTexts[TAB_HISTORY] = "(*)"; badgeStyles[TAB_HISTORY] = cyan; } else { long historyCount = hasSelection - ? historyTab.historyEntries.stream() + ? tabRegistry.historyTab().historyEntries.stream() .map(e -> { if (e.headers != null) { Object bid = e.headers.get("breadcrumbId"); @@ -1320,107 +1085,6 @@ private void computeTabBadges(String[] badgeTexts, Style[] badgeStyles) { } } - private void renderMorePopup(Frame frame, Rect area) { - int popupW = 22; - int popupH = 17; - // Position just below the "0 More▾" tab label - int dividerW = CharWidth.of(" | "); - int tabBarX = 0; - Line[] tabLabels = currentTabLabels; - if (tabLabels != null) { - for (int i = 0; i < tabLabels.length - 1; i++) { - tabBarX += tabLabels[i].width(); - tabBarX += dividerW; - } - } - int x = area.left() + tabBarX; - int y = area.top(); - if (x + popupW > area.right()) { - x = Math.max(area.left(), area.right() - popupW); - } - Rect popup = new Rect(x, y, Math.min(popupW, area.width() - (x - area.left())), Math.min(popupH, area.height())); - - frame.renderWidget(Clear.INSTANCE, popup); - - Style keyStyle = Style.EMPTY.fg(Color.YELLOW).bold(); - ListItem[] items = { - ListItem.from(Line.from(Span.raw(" "), Span.styled("B", keyStyle), Span.raw("eans"))), - ListItem.from(Line.from(Span.raw(" Bro"), Span.styled("w", keyStyle), Span.raw("se"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("C", keyStyle), Span.raw("ircuit Breaker"))), - ListItem.from(Line.from(Span.raw(" Cl"), Span.styled("a", keyStyle), Span.raw("sspath"))), - ListItem.from(Line.from(Span.raw(" Confi"), Span.styled("g", keyStyle), Span.raw("uration"))), - ListItem.from(Line.from(Span.raw(" Co"), Span.styled("n", keyStyle), Span.raw("sumers"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("D", keyStyle), Span.raw("ataSource"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("I", keyStyle), Span.raw("nflight"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("M", keyStyle), Span.raw("emory"))), - ListItem.from(Line.from(Span.raw(" M"), Span.styled("e", keyStyle), Span.raw("trics"))), - ListItem.from(Line.from(Span.raw(" S"), Span.styled("Q", keyStyle), Span.raw("L Query"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("O", keyStyle), Span.raw("Tel Spans"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("P", keyStyle), Span.raw("rocess"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("S", keyStyle), Span.raw("tartup"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("T", keyStyle), Span.raw("hreads"))), - }; - ListWidget list = ListWidget.builder() - .items(items) - .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) - .highlightSymbol("") - .scrollMode(ScrollMode.NONE) - .block(Block.builder() - .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(Title.from(Line.from(Span.styled(" More Tabs ", Style.EMPTY.fg(Color.YELLOW).bold())))) - .build()) - .build(); - frame.renderStatefulWidget(list, popup, morePopupState); - } - - private void renderSwitchPopup(Frame frame, Rect area) { - List integrations = getNonVanishingIntegrations(); - if (integrations.isEmpty()) { - showSwitchPopup = false; - return; - } - - int maxLabelLen = integrations.stream() - .mapToInt(i -> { - String n = i.name != null ? i.name : "?"; - return n.length() + i.pid.length() + 14; - }) - .max().orElse(30); - int popupW = Math.min(area.width() - 4, Math.max(40, maxLabelLen + 4)); - int popupH = Math.min(area.height() - 4, integrations.size() + 2); - - 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)); - - frame.renderWidget(Clear.INSTANCE, popup); - - ListItem[] items = new ListItem[integrations.size()]; - for (int i = 0; i < integrations.size(); i++) { - IntegrationInfo info = integrations.get(i); - String name = info.name != null ? info.name : "?"; - boolean current = info.pid.equals(ctx.selectedPid); - String label = String.format(" 🐪 %s (pid:%s)%s", name, info.pid, current ? " ●" : ""); - if (current) { - items[i] = ListItem.from(Line.from(Span.styled(label, Style.EMPTY.fg(Color.CYAN)))); - } else { - items[i] = ListItem.from(label); - } - } - - ListWidget list = ListWidget.builder() - .items(items) - .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) - .highlightSymbol("") - .scrollMode(ScrollMode.NONE) - .block(Block.builder() - .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(Title.from(Line.from(Span.styled(" Switch Integration ", Style.EMPTY.fg(Color.YELLOW).bold())))) - .build()) - .build(); - frame.renderStatefulWidget(list, popup, switchPopupState); - } - private void openFilesPopup() { IntegrationInfo info = findSelectedIntegration(); if (info != null) { @@ -1429,79 +1093,14 @@ private void openFilesPopup() { } private List getNonVanishingIntegrations() { - return data.get().stream() + return dataService.data().get().stream() .filter(i -> !i.vanishing && i.name != null) .sorted(Comparator.comparing(i -> i.name, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()); } - private static int morePopupShortcut(KeyEvent ke) { - if (ke.isChar('b')) { - return 0; - } - if (ke.isChar('w')) { - return 1; - } - if (ke.isChar('c')) { - return 2; - } - if (ke.isChar('a')) { - return 3; - } - if (ke.isChar('g')) { - return 4; - } - if (ke.isChar('n')) { - return 5; - } - if (ke.isChar('d')) { - return 6; - } - if (ke.isChar('i')) { - return 7; - } - if (ke.isChar('m')) { - return 8; - } - if (ke.isChar('e')) { - return 9; - } - if (ke.isChar('q')) { - return 10; - } - if (ke.isChar('o')) { - return 11; - } - if (ke.isChar('p')) { - return 12; - } - if (ke.isChar('s')) { - return 13; - } - if (ke.isChar('t')) { - return 14; - } - return -1; - } - - private MonitorTab activeTab() { - return switch (tabsState.selected()) { - case TAB_OVERVIEW -> overviewTab; - case TAB_LOG -> logTab; - case TAB_DIAGRAM -> diagramTab; - case TAB_ROUTES -> routesTab; - case TAB_ENDPOINTS -> endpointsTab; - case TAB_HEALTH -> healthTab; - case TAB_HISTORY -> historyTab; - case TAB_HTTP -> httpTab; - case TAB_ERRORS -> errorsTab; - case TAB_MORE -> activeMoreTab; - default -> null; - }; - } - private void stopSelectedProcess(boolean forceKill) { - enableBurstMode(); + dataService.enableBurstMode(); if (ctx.selectedPid == null) { return; } @@ -1536,7 +1135,7 @@ private void stopSelectedProcess(boolean forceKill) { PathUtils.deleteFile(camelDir.resolve(pidStr + "-debug.json")); PathUtils.deleteFile(camelDir.resolve(pidStr + "-receive.json")); } else { - stoppingPids.add(pidStr); + dataService.stoppingPids().add(pidStr); ph.destroy(); } }); @@ -1544,7 +1143,7 @@ private void stopSelectedProcess(boolean forceKill) { } private void restartSelectedProcess() { - enableBurstMode(); + dataService.enableBurstMode(); if (ctx.selectedPid == null || isInfraSelected()) { return; } @@ -1625,14 +1224,6 @@ private void restartSelectedProcess() { } } - private void enableBurstMode() { - burstModeUntil = System.currentTimeMillis() + 20_000; - } - - private boolean isBurstMode() { - return System.currentTimeMillis() < burstModeUntil; - } - private void setNotification(String message, boolean error) { monitorNotification = message; monitorNotificationError = error; @@ -1679,11 +1270,11 @@ private void resetStats() { root.put("action", "reset-stats"); Path actionFile = ctx.getActionFile(pid); PathUtils.writeTextSafely(root.toJson(), actionFile); - metrics.resetStats(pid); + dataService.metrics().resetStats(pid); } private void sendRouteCommand(String pid, String routeId, String command) { - enableBurstMode(); + dataService.enableBurstMode(); JsonObject root = new JsonObject(); root.put("action", "route"); root.put("id", routeId); @@ -1692,49 +1283,15 @@ private void sendRouteCommand(String pid, String routeId, String command) { PathUtils.writeTextSafely(root.toJson(), actionFile); } - private void renderKillConfirm(Frame frame, Rect area) { - String name = selectedName(); - String msg = " Kill " + name + " (PID: " + ctx.selectedPid + ")? "; - int popupW = Math.max(34, msg.length() + 4); - int popupH = 6; - int x = area.left() + Math.max(0, (area.width() - popupW) / 2); - int y = area.top() + Math.max(0, (area.height() - popupH) / 2); - 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) - .borderStyle(Style.EMPTY.fg(Color.LIGHT_RED)) - .title(" Confirm Kill ") - .build(); - frame.renderWidget(block, popup); - Rect inner = block.inner(popup); - frame.renderWidget( - Paragraph.builder() - .text(Text.from( - Line.from(Span.raw("")), - Line.from(Span.styled(msg, Style.EMPTY.fg(Color.LIGHT_RED).bold())), - Line.from(Span.raw("")), - Line.from( - Span.raw(" "), - Span.styled("Enter", Style.EMPTY.bold()), - Span.raw(" confirm "), - Span.styled("Esc", Style.EMPTY.bold()), - Span.raw(" cancel")))) - .build(), - inner); - } - private void renderFooter(Frame frame, Rect area) { // Show screenshot flash message briefly - String msg = screenshotMessage; - if (msg != null && System.currentTimeMillis() - screenshotMessageTime < 5000) { + String msg = recordingManager.screenshotFlashMessage(); + if (msg != null) { frame.renderWidget( Paragraph.from(Line.from(Span.styled(" " + msg, Style.EMPTY.fg(Color.GREEN)))), area); return; } - screenshotMessage = null; List spans = new ArrayList<>(); int fKeyTotal = 0; @@ -1753,20 +1310,20 @@ private void renderFooter(Frame frame, Rect area) { if (filesBrowser.isVisible()) { filesBrowser.renderFooter(spans); - } else if (showSwitchPopup) { + } else if (popupManager.isSwitchPopupVisible()) { hint(spans, "Up/Down", "select"); hint(spans, "Enter", "switch"); hint(spans, "Esc", "close"); - } else if (showMorePopup) { + } else if (popupManager.isMorePopupVisible()) { hint(spans, "Up/Down", "select"); hint(spans, "Enter", "open"); hint(spans, "Esc", "close"); } else if (shellPanel.isOpen()) { shellPanel.renderFooter(spans); } else { - MonitorTab tab = activeTab(); + MonitorTab tab = tabRegistry.activeTab(); - if (tabsState.selected() == TAB_OVERVIEW) { + if (tabRegistry.selectedTabIndex() == TAB_OVERVIEW) { fKeyTotal = renderOverviewFooter(spans); } else { tab.renderFooter(spans); @@ -1776,11 +1333,12 @@ private void renderFooter(Frame frame, Rect area) { List rightSpans = new ArrayList<>(); - if (recording && !recentKeys.isEmpty()) { + if (recordingManager.isRecording() && !recordingManager.getRecentKeys().isEmpty()) { long now = System.currentTimeMillis(); + List recentKeys = recordingManager.getRecentKeys(); int maxKeys = Math.min(recentKeys.size(), 8); - List visible = recentKeys.subList(recentKeys.size() - maxKeys, recentKeys.size()); - for (KeyRecord kr : visible) { + List visible = recentKeys.subList(recentKeys.size() - maxKeys, recentKeys.size()); + for (RecordingManager.KeyRecord kr : visible) { long age = now - kr.timestamp(); Style style = age < 1000 ? Style.EMPTY.fg(Color.WHITE).bold().onBlue() @@ -1847,13 +1405,13 @@ private void renderFooter(Frame frame, Rect area) { private int insertFKeyHints(List spans) { int insertPos = Math.min(2, spans.size()); List fKeySpans = new ArrayList<>(); - MonitorTab tab = activeTab(); + MonitorTab tab = tabRegistry.activeTab(); boolean hasHelp = tab != null && tab.getHelpText() != null; if (hasHelp) { hint(fKeySpans, "F1", "help"); } hint(fKeySpans, "F2", "actions"); - if (getNonVanishingIntegrations().size() > 1) { + if (popupManager.getNonVanishingIntegrations().size() > 1) { hint(fKeySpans, "F3", "switch"); } hint(fKeySpans, "F6", "shell"); @@ -1890,7 +1448,7 @@ private int renderOverviewFooter(List spans) { actionsPopup.renderFooter(spans); return 0; } - overviewTab.renderFooter(spans); + tabRegistry.overviewTab().renderFooter(spans); int fKeyTotal = insertFKeyHints(spans); // Process action hints if (ctx.selectedPid != null && !isInfraSelected()) { @@ -1918,7 +1476,7 @@ private int renderOverviewFooter(List spans) { // ---- Data Loading ---- private void refreshLogData() { - if (tabsState.selected() != TAB_LOG) { + if (tabRegistry.selectedTabIndex() != TAB_LOG) { return; } String logPid = null; @@ -1935,481 +1493,38 @@ private void refreshLogData() { } } if (logPid != null) { - logTab.refreshFromFile(logPid, logFileName); + tabRegistry.logTab().refreshFromFile(logPid, logFileName); } } - private void refreshData() { - if (runner == null) { - refreshDataSync(); - return; - } - if (!refreshInProgress.compareAndSet(false, true)) { - return; + private void refreshConditionalData() { + List selectedPids = dataService.selectedPidAsList(); + if (tabRegistry.selectedTabIndex() == TAB_ERRORS && !selectedPids.isEmpty()) { + dataService.refreshErrorData(selectedPids); } - lastRefresh = System.currentTimeMillis(); - String currentSelectedPid = ctx.selectedPid; - runner.scheduler().execute(() -> { - try { - refreshDataSync(); - } finally { - refreshInProgress.set(false); + if (tabRegistry.selectedTabIndex() == TAB_HISTORY && !selectedPids.isEmpty()) { + if (tabRegistry.historyTab().historyRefreshRequested) { + tabRegistry.historyTab().historyRefreshRequested = false; + tabRegistry.historyTab().historyEntries = dataService.loadHistoryData(selectedPids); } - }); - } - - private void refreshDataSync() { - lastRefresh = System.currentTimeMillis(); - try { - refreshLogData(); - boolean fullScan = scanIntegrations(); - List infos = data.get(); - handleAutoSelect(infos, fullScan); - refreshConditionalData(); - } catch (Exception e) { - // ignore refresh errors + dataService.refreshTraceData(selectedPids); + } + if (tabRegistry.selectedTabIndex() == TAB_MORE + && tabRegistry.getActiveMoreTab() == tabRegistry.spansTab() + && ctx.selectedPid != null && tabRegistry.spansTab().spanRefreshRequested) { + tabRegistry.spansTab().spanRefreshRequested = false; + dataService.refreshSpanData(); } } - private boolean scanIntegrations() { - List infos = new ArrayList<>(); - long now = System.currentTimeMillis(); - boolean wantFullScan = tabsState.selected() == TAB_OVERVIEW || showSwitchPopup || cachedPids.isEmpty(); - long scanInterval = isBurstMode() ? 1000 : 2000; - boolean fullScan = wantFullScan && (now - lastFullScanTime >= scanInterval); - List pids; - if (fullScan) { - pids = findPids(name); - cachedPids = pids; - lastFullScanTime = now; - } else { - pids = cachedPids; - } + // ---- Helpers ---- - List refreshPids; - if (!fullScan && ctx.selectedPid != null) { - try { - refreshPids = List.of(Long.parseLong(ctx.selectedPid)); - } catch (NumberFormatException e) { - refreshPids = pids; - } - } else { - refreshPids = pids; - } - for (Long pid : refreshPids) { - JsonObject root = loadStatus(pid); - if (root != null) { - ProcessHandle ph = ProcessHandle.of(pid).orElse(null); - if (ph == null) { - continue; - } - IntegrationInfo info = StatusParser.parseIntegration(ph, root); - if (info != null) { - infos.add(info); - metrics.updateThroughputHistory(info); - metrics.updateEndpointHistory(info); - metrics.updateCbHistory(info); - metrics.updateHeapHistory(info); - metrics.updateLoadMetrics(ph, info); - } - } - } - if (!fullScan && ctx.selectedPid != null) { - List previous = data.get(); - for (IntegrationInfo prev : previous) { - if (!prev.vanishing && !ctx.selectedPid.equals(prev.pid)) { - infos.add(prev); - } - } - } + private IntegrationInfo findSelectedIntegration() { + return ctx.findSelectedIntegration(); + } - handleVanishing(infos, now); - data.set(infos); - return fullScan; - } - - private void handleVanishing(List infos, long now) { - Set livePids = infos.stream().map(i -> i.pid).collect(Collectors.toSet()); - List previous = data.get(); - for (IntegrationInfo prev : previous) { - if (!prev.vanishing && !livePids.contains(prev.pid) && !vanishing.containsKey(prev.pid)) { - boolean wasExplicitStop = stoppingPids.remove(prev.pid); - if (wasExplicitStop) { - metrics.removeVanished(prev.pid); - } else { - vanishing.put(prev.pid, new VanishingInfo(prev, System.currentTimeMillis())); - } - } - } - - Iterator> it = vanishing.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (now - entry.getValue().startTime > VANISH_DURATION_MS) { - it.remove(); - metrics.removeVanished(entry.getKey()); - } else if (!livePids.contains(entry.getKey())) { - IntegrationInfo ghost = entry.getValue().info; - ghost.vanishing = true; - ghost.vanishStart = entry.getValue().startTime; - infos.add(ghost); - } else { - it.remove(); - } - } - } - - private void handleAutoSelect(List infos, boolean fullScan) { - if (ctx.selectedPid != null && !isInfraSelected()) { - boolean stillAlive = infos.stream() - .anyMatch(i -> ctx.selectedPid.equals(i.pid) && !i.vanishing); - if (!stillAlive) { - IntegrationInfo gone = infos.stream() - .filter(i -> ctx.selectedPid.equals(i.pid)) - .findFirst().orElse(null); - if (gone != null) { - ctx.lastSelectedName = gone.name; - } - ctx.selectedPid = null; - } - } - - String autoSelect = actionsPopup.getPendingAutoSelect(); - if (autoSelect != null) { - for (IntegrationInfo info : infos) { - if (!info.vanishing && autoSelect.equalsIgnoreCase(info.name)) { - ctx.selectedPid = info.pid; - ctx.lastSelectedName = null; - actionsPopup.clearPendingAutoSelect(); - break; - } - } - } - - if (ctx.selectedPid == null && ctx.lastSelectedName != null && !isInfraSelected()) { - for (IntegrationInfo info : infos) { - if (!info.vanishing && ctx.lastSelectedName.equalsIgnoreCase(info.name)) { - ctx.selectedPid = info.pid; - ctx.lastSelectedName = null; - break; - } - } - } - - if (fullScan) { - refreshInfraData(); - } - - if (ctx.selectedPid == null && !infraData.get().isEmpty() - && infos.stream().noneMatch(i -> !i.vanishing)) { - List infras = infraData.get(); - if (!infras.isEmpty()) { - int firstInfraIndex = infos.size() + (infras.size() > 0 ? 1 : 0); - overviewTab.tableState.select(firstInfraIndex); - ctx.selectedPid = infras.get(0).pid; - } - } - } - - private void refreshConditionalData() { - List selectedPids = selectedPidAsList(); - if (tabsState.selected() == TAB_ERRORS && !selectedPids.isEmpty()) { - refreshErrorData(selectedPids); - } - if (tabsState.selected() == TAB_HISTORY && !selectedPids.isEmpty()) { - if (historyTab.historyRefreshRequested) { - historyTab.historyRefreshRequested = false; - refreshHistoryData(selectedPids); - } - refreshTraceData(selectedPids); - } - if (tabsState.selected() == TAB_MORE && activeMoreTab == spansTab - && ctx.selectedPid != null && spansTab.spanRefreshRequested) { - spansTab.spanRefreshRequested = false; - refreshSpanData(); - } - } - - @SuppressWarnings("unchecked") - private void refreshSpanData() { - String pid = ctx.selectedPid; - if (pid == null) { - return; - } - try { - // Send action to request span data - Path outputFile = ctx.getOutputFile(pid); - PathUtils.deleteFile(outputFile); - - JsonObject action = new JsonObject(); - action.put("action", "span"); - action.put("dump", "true"); - action.put("limit", "500"); - Path actionFile = ctx.getActionFile(pid); - PathUtils.writeTextSafely(action.toJson(), actionFile); - - // Poll for response - JsonObject response = MonitorContext.pollJsonResponse(outputFile, 3000); - if (response != null) { - Boolean enabled = response.getBoolean("enabled"); - if (enabled != null && enabled) { - JsonArray arr = response.getCollection("spans"); - if (arr != null) { - List entries = new ArrayList<>(); - for (int i = 0; i < arr.size(); i++) { - JsonObject spanObj = (JsonObject) arr.get(i); - entries.add(SpanEntry.fromJson(spanObj)); - } - otelSpans.set(entries); - } - } - PathUtils.deleteFile(outputFile); - } - } catch (Exception e) { - // ignore - } - } - - @SuppressWarnings("unchecked") - private void refreshInfraData() { - List infraInfos = new ArrayList<>(); - try { - Path camelDir = CommandLineHelper.getCamelDir(); - if (Files.isDirectory(camelDir)) { - try (var files = Files.list(camelDir)) { - List jsonFiles = files - .filter(p -> { - String n = p.getFileName().toString(); - return n.startsWith("infra-") && n.endsWith(".json"); - }) - .toList(); - for (Path jsonFile : jsonFiles) { - String fn = jsonFile.getFileName().toString(); - // Format: infra-{alias}-{pid}.json - String withoutExt = fn.substring(0, fn.lastIndexOf('.')); - int lastDash = withoutExt.lastIndexOf('-'); - if (lastDash <= 6) { - continue; - } - String alias = withoutExt.substring(6, lastDash); - String pidStr = withoutExt.substring(lastDash + 1); - long pid; - try { - pid = Long.parseLong(pidStr); - } catch (NumberFormatException e) { - continue; - } - boolean alive = ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false); - - InfraInfo info = new InfraInfo(); - info.pid = pidStr; - info.alias = alias; - info.alive = alive; - try { - String json = Files.readString(jsonFile); - Object parsed = Jsoner.deserialize(json); - if (parsed instanceof Map map) { - for (Map.Entry e : map.entrySet()) { - info.properties.put(String.valueOf(e.getKey()), e.getValue()); - } - } - } catch (Exception e) { - // ignore parse errors - } - info.serviceVersion = StatusParser.objToString(info.properties.get("serviceVersion")); - infraInfos.add(info); - } - } - } - } catch (IOException e) { - // ignore - } - - // Handle vanishing infra services - Set liveInfraPids = infraInfos.stream().map(i -> i.pid).collect(Collectors.toSet()); - List previousInfra = infraData.get(); - for (InfraInfo prev : previousInfra) { - if (!prev.vanishing && !liveInfraPids.contains(prev.pid) && !vanishingInfra.containsKey(prev.pid)) { - vanishingInfra.put(prev.pid, new VanishingInfraInfo(prev, System.currentTimeMillis())); - } - } - long now = System.currentTimeMillis(); - Iterator> infraIt = vanishingInfra.entrySet().iterator(); - while (infraIt.hasNext()) { - Map.Entry entry = infraIt.next(); - if (now - entry.getValue().startTime > VANISH_DURATION_MS) { - infraIt.remove(); - } else if (!liveInfraPids.contains(entry.getKey())) { - InfraInfo ghost = entry.getValue().info; - ghost.vanishing = true; - ghost.vanishStart = entry.getValue().startTime; - infraInfos.add(ghost); - } else { - infraIt.remove(); - } - } - - infraInfos.sort((a, b) -> a.alias.compareToIgnoreCase(b.alias)); - infraData.set(infraInfos); - } - - // ---- Trace Data Loading ---- - - private void refreshTraceData(List pids) { - List allTraces = new ArrayList<>(traces.get()); - - for (Long pid : pids) { - readTraceFile(Long.toString(pid), allTraces); - } - - // Sort by timestamp - allTraces.sort((a, b) -> { - if (a.timestamp == null && b.timestamp == null) { - return 0; - } - if (a.timestamp == null) { - return -1; - } - if (b.timestamp == null) { - return 1; - } - return a.timestamp.compareTo(b.timestamp); - }); - - // Keep only last MAX_TRACES - if (allTraces.size() > MAX_TRACES) { - allTraces = new ArrayList<>(allTraces.subList(allTraces.size() - MAX_TRACES, allTraces.size())); - } - - traces.set(allTraces); - } - - @SuppressWarnings("unchecked") - private void readTraceFile(String pid, List allTraces) { - Path traceFile = CommandLineHelper.getCamelDir().resolve(pid + "-trace.json"); - if (!Files.exists(traceFile)) { - return; - } - - long lastPos = traceFilePositions.getOrDefault(pid, 0L); - - try (RandomAccessFile raf = new RandomAccessFile(traceFile.toFile(), "r")) { - long length = raf.length(); - if (length <= lastPos) { - return; // no new data - } - - raf.seek(lastPos); - // If we're resuming mid-file, skip any partial line - if (lastPos > 0) { - raf.readLine(); - } - - // Read remaining bytes - long startPos = raf.getFilePointer(); - byte[] remaining = new byte[(int) (length - startPos)]; - raf.readFully(remaining); - String content = new String(remaining, StandardCharsets.UTF_8); - - traceFilePositions.put(pid, length); - - // Each line is a JSON object: {"enabled":true,"traces":[...]} - String[] lines = content.split("\n"); - for (String line : lines) { - line = line.trim(); - if (line.isEmpty()) { - continue; - } - try { - JsonObject json = (JsonObject) Jsoner.deserialize(line); - Object tracesArray = json.get("traces"); - if (tracesArray instanceof List traceList) { - for (Object traceObj : traceList) { - if (traceObj instanceof JsonObject traceJson) { - TraceEntry entry = StatusParser.parseTraceEntry(traceJson, pid); - if (entry != null) { - allTraces.add(entry); - } - } - } - } else { - // Fallback: try parsing the line itself as a trace entry - TraceEntry entry = StatusParser.parseTraceEntry(json, pid); - if (entry != null) { - allTraces.add(entry); - } - } - } catch (Exception e) { - // skip malformed lines - } - } - } catch (IOException e) { - // ignore - } - } - - private JsonObject loadErrorFile(long pid) { - return TuiHelper.loadStatus(pid, this::getErrorFile); - } - - @SuppressWarnings("unchecked") - private void refreshHistoryData(List pids) { - List allEntries = new ArrayList<>(); - for (Long pid : pids) { - Path historyFile = CommandLineHelper.getCamelDir().resolve(pid + "-history.json"); - if (!Files.exists(historyFile)) { - continue; - } - try { - String content = Files.readString(historyFile); - if (content == null || content.isBlank()) { - continue; - } - JsonObject json = (JsonObject) Jsoner.deserialize(content); - Object tracesArray = json.get("traces"); - if (tracesArray instanceof List traceList) { - for (Object traceObj : traceList) { - if (traceObj instanceof JsonObject traceJson) { - HistoryEntry entry = StatusParser.parseHistoryEntry(traceJson, Long.toString(pid)); - if (entry != null) { - allEntries.add(entry); - } - } - } - } - } catch (Exception e) { - // ignore - } - } - historyTab.historyEntries = allEntries; - } - - private void refreshErrorData(List pids) { - IntegrationInfo sel = findSelectedIntegration(); - if (sel == null) { - return; - } - try { - long pid = Long.parseLong(sel.pid); - JsonObject root = loadErrorFile(pid); - if (root != null) { - List parsed = StatusParser.parseErrors(root); - sel.errors.clear(); - sel.errors.addAll(parsed); - } - } catch (Exception e) { - // ignore - } - } - - // ---- Helpers ---- - - private IntegrationInfo findSelectedIntegration() { - return ctx.findSelectedIntegration(); - } - - private InfraInfo findSelectedInfra() { - return ctx.findSelectedInfra(); + private InfraInfo findSelectedInfra() { + return ctx.findSelectedInfra(); } private boolean isInfraSelected() { @@ -2420,45 +1535,6 @@ private String selectedName() { return ctx.selectedName(); } - private List findPids(String name) { - return TuiHelper.findPids(name, this::getStatusFile); - } - - private JsonObject loadStatus(long pid) { - return TuiHelper.loadStatus(pid, this::getStatusFile); - } - - private void takeScreenshot() { - Buffer buf = lastBuffer; - if (buf == null) { - return; - } - try { - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); - String baseName = "camel-tui-screenshot-" + timestamp; - Path svgPath = Path.of(baseName + ".svg"); - Path txtPath = Path.of(baseName + ".txt"); - Path ansPath = Path.of(baseName + ".ans"); - ExportRequest.export(buf).svg().toFile(svgPath); - ExportRequest.export(buf).text().toFile(txtPath); - ExportRequest.export(buf).text().options(o -> o.styles(true)).toFile(ansPath); - screenshotMessage = "Screenshot saved to " + svgPath.toAbsolutePath() + " (and .txt, .ans)"; - screenshotMessageTime = System.currentTimeMillis(); - } catch (IOException e) { - screenshotMessage = "Screenshot failed: " + e.getMessage(); - screenshotMessageTime = System.currentTimeMillis(); - } - } - - record KeyRecord(String label, long timestamp) { - } - - record VanishingInfo(IntegrationInfo info, long startTime) { - } - - record VanishingInfraInfo(InfraInfo info, long startTime) { - } - // ---- MCP .mcp.json lifecycle ---- private static Path writeMcpJson(int port) { @@ -2489,712 +1565,4 @@ private static void deleteMcpJson(Path path) { } } - // ---- MCP accessor methods ---- - - private static final String[] TAB_NAMES = { - "Overview", "Log", "Diagram", "Routes", "Endpoints", - "HTTP", "Health", "Inspect", "Errors", "More" - }; - - Buffer getLastBuffer() { - return lastBuffer; - } - - long getRenderGeneration() { - return renderGeneration; - } - - boolean isKeystrokesVisible() { - return recording; - } - - TapeRecorder getTapeRecorder() { - return tapeRecorder; - } - - boolean isTapeRecording() { - return tapeRecorder != null && tapeRecorder.isActive(); - } - - void startTapeRecording(String title) { - tapeRecorder = new TapeRecorder(); - tapeRecorder.start(title); - } - - void clearTapeRecorder() { - tapeRecorder = null; - } - - private void toggleTapeRecording() { - if (tapeRecorder != null && tapeRecorder.isActive()) { - String tape = tapeRecorder.stop(); - tapeRecorder = null; - String timestamp = java.time.LocalDateTime.now() - .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); - String filename = "camel-tui-tape-" + timestamp + ".tape"; - try { - java.nio.file.Files.writeString(java.nio.file.Path.of(filename), tape); - captionOverlay.showCaption("Tape saved: " + filename, 5); - } catch (java.io.IOException e) { - captionOverlay.showCaption("Failed to save tape: " + e.getMessage(), 5); - } - } else { - tapeRecorder = new TapeRecorder(); - tapeRecorder.start(null); - captionOverlay.showCaption("Tape recording started", 3); - } - } - - TuiEventLog getEventLog() { - return eventLog; - } - - int getActiveTabIndex() { - return tabsState.selected(); - } - - String getActiveTabName() { - int idx = tabsState.selected(); - return idx >= 0 && idx < TAB_NAMES.length ? TAB_NAMES[idx] : "Unknown"; - } - - String getSelectedPid() { - return ctx != null ? ctx.selectedPid : null; - } - - String getSelectedIntegrationName() { - if (ctx == null) { - return null; - } - IntegrationInfo info = ctx.findSelectedIntegration(); - return info != null ? info.name : null; - } - - int getIntegrationCount() { - List list = data.get(); - return (int) list.stream().filter(i -> !i.vanishing).count(); - } - - boolean isCaptionVisible() { - return captionOverlay.isCaptionVisible(); - } - - void showCaption(String text) { - captionOverlay.showCaption(text); - } - - void showCaption(String text, int durationSeconds) { - captionOverlay.showCaption(text, durationSeconds); - } - - boolean isDrawVisible() { - return drawOverlay.isVisible(); - } - - void setDrawing(List cells, int durationSeconds) { - drawOverlay.setDrawing(cells, durationSeconds); - } - - void appendDrawing(List cells) { - drawOverlay.appendDrawing(cells); - } - - void clearDrawing() { - drawOverlay.clear(); - } - - private static final String[] MORE_TAB_NAMES = { - "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration", - "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "SQL Query", "Spans", "Process", "Startup", - "Threads" - }; - - String navigateToTab(String tabName) { - for (int i = 0; i < TAB_NAMES.length; i++) { - if (TAB_NAMES[i].equalsIgnoreCase(tabName)) { - handleTabKey(i); - return TAB_NAMES[i]; - } - } - // Check More submenu tabs - for (int i = 0; i < MORE_TAB_NAMES.length; i++) { - if (MORE_TAB_NAMES[i].equalsIgnoreCase(tabName)) { - morePopupState.select(i); - lastMoreSelection = i; - activeMoreTab = switch (i) { - case 0 -> beansTab; - case 1 -> browseTab; - case 2 -> circuitBreakerTab; - case 3 -> classpathTab; - case 4 -> configurationTab; - case 5 -> consumersTab; - case 6 -> dataSourceTab; - case 7 -> inflightTab; - case 8 -> memoryTab; - case 9 -> metricsTab; - case 10 -> sqlQueryTab; - case 11 -> spansTab; - case 12 -> processTab; - case 13 -> startupTab; - case 14 -> threadsTab; - default -> null; - }; - if (activeMoreTab != null) { - overviewTab.selectCurrentIntegration(); - tabsState.select(TAB_MORE); - activeMoreTab.onTabSelected(); - } - return MORE_TAB_NAMES[i]; - } - } - return null; - } - - String selectIntegration(String nameOrPid) { - List infos = data.get(); - for (IntegrationInfo info : infos) { - if (info.vanishing) { - continue; - } - if (nameOrPid.equals(info.pid) - || (info.name != null && info.name.equalsIgnoreCase(nameOrPid))) { - ctx.selectedPid = info.pid; - return info.name != null ? info.name : info.pid; - } - } - return null; - } - - List getTabNames() { - List names = new ArrayList<>(); - names.addAll(List.of(TAB_NAMES)); - names.addAll(List.of(MORE_TAB_NAMES)); - return names; - } - - List getActionLabels() { - return actionsPopup.getActionLabels(); - } - - SelectionContext getSelectionContext() { - SelectionContext popup = actionsPopup.getSelectionContext(); - if (popup != null) { - return popup; - } - MonitorTab tab = activeTab(); - return tab != null ? tab.getSelectionContext() : null; - } - - List getIntegrationNames() { - return data.get().stream() - .filter(i -> !i.vanishing) - .map(i -> i.name != null ? i.name : i.pid) - .toList(); - } - - JsonObject getReadme(String name) { - List integrations = data.get(); - IntegrationInfo target = null; - if (name != null && !name.isEmpty()) { - for (IntegrationInfo info : integrations) { - if (!info.vanishing && name.equals(info.name)) { - target = info; - break; - } - } - } else { - target = ctx != null ? ctx.findSelectedIntegration() : null; - } - if (target == null) { - return null; - } - if (target.readmeFiles == null || target.readmeFiles.isEmpty()) { - return null; - } - try { - Path outputFile = ctx.getOutputFile(target.pid); - Files.deleteIfExists(outputFile); - JsonObject action = new JsonObject(); - action.put("action", "readme"); - PathUtils.writeTextSafely(action.toJson(), ctx.getActionFile(target.pid)); - return MonitorContext.pollJsonResponse(outputFile, 5000); - } catch (Exception e) { - return null; - } - } - - JsonObject getFiles(String name, String file) { - List integrations = data.get(); - IntegrationInfo target = null; - if (name != null && !name.isEmpty()) { - for (IntegrationInfo info : integrations) { - if (!info.vanishing && name.equals(info.name)) { - target = info; - break; - } - } - } else { - target = ctx != null ? ctx.findSelectedIntegration() : null; - } - if (target == null) { - return null; - } - Path dir = FilesBrowser.resolveSourceDirectory(target); - if (dir == null || !Files.isDirectory(dir)) { - return null; - } - if (file != null && !file.isEmpty()) { - Path filePath = dir.resolve(file); - if (!Files.isRegularFile(filePath)) { - return null; - } - try { - String content = Files.readString(filePath, StandardCharsets.UTF_8); - JsonObject result = new JsonObject(); - result.put("file", file); - result.put("directory", dir.toString()); - result.put("size", FilesBrowser.formatFileSize(Files.size(filePath))); - result.put("type", FilesBrowser.fileType(filePath)); - result.put("content", content); - return result; - } catch (IOException e) { - return null; - } - } - JsonArray files = new JsonArray(); - try (var stream = Files.list(dir)) { - stream.filter(Files::isRegularFile) - .sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString())) - .limit(99) - .forEach(p -> { - JsonObject entry = new JsonObject(); - entry.put("name", p.getFileName().toString()); - try { - entry.put("size", FilesBrowser.formatFileSize(Files.size(p))); - } catch (IOException e) { - entry.put("size", "0 B"); - } - entry.put("type", FilesBrowser.fileType(p)); - files.add(entry); - }); - } catch (IOException e) { - return null; - } - if (files.isEmpty()) { - return null; - } - JsonObject result = new JsonObject(); - result.put("directory", dir.toString()); - result.put("files", files); - result.put("totalFiles", files.size()); - return result; - } - - int injectKeys(List keys, int delayMs) { - long fireAt = System.currentTimeMillis(); - int count = 0; - for (String key : keys) { - KeyEvent ke = parseKey(key); - if (ke != null) { - pendingKeys.add(new PendingKey(ke, fireAt)); - fireAt += delayMs; - count++; - } - } - return count; - } - - static KeyEvent parseKey(String key) { - if (key == null || key.isEmpty()) { - return null; - } - - boolean ctrl = false; - boolean shift = false; - String remainder = key; - while (remainder.contains("+")) { - int plus = remainder.indexOf('+'); - String mod = remainder.substring(0, plus).trim(); - remainder = remainder.substring(plus + 1).trim(); - if (mod.equalsIgnoreCase("Ctrl")) { - ctrl = true; - } else if (mod.equalsIgnoreCase("Shift")) { - shift = true; - } - } - - KeyModifiers mods = KeyModifiers.of(ctrl, false, shift); - - KeyCode code = switch (remainder.toLowerCase(Locale.ROOT)) { - case "enter", "return" -> KeyCode.ENTER; - case "esc", "escape" -> KeyCode.ESCAPE; - case "tab" -> KeyCode.TAB; - case "backspace" -> KeyCode.BACKSPACE; - case "delete", "del" -> KeyCode.DELETE; - case "up" -> KeyCode.UP; - case "down" -> KeyCode.DOWN; - case "left" -> KeyCode.LEFT; - case "right" -> KeyCode.RIGHT; - case "home" -> KeyCode.HOME; - case "end" -> KeyCode.END; - case "pageup", "pgup" -> KeyCode.PAGE_UP; - case "pagedown", "pgdn" -> KeyCode.PAGE_DOWN; - case "f1" -> KeyCode.F1; - case "f2" -> KeyCode.F2; - case "f3" -> KeyCode.F3; - case "f4" -> KeyCode.F4; - case "f5" -> KeyCode.F5; - case "f6" -> KeyCode.F6; - case "f7" -> KeyCode.F7; - case "f8" -> KeyCode.F8; - case "f9" -> KeyCode.F9; - case "f10" -> KeyCode.F10; - case "f11" -> KeyCode.F11; - case "f12" -> KeyCode.F12; - case "space" -> null; - default -> null; - }; - - if (code != null) { - return KeyEvent.ofKey(code, mods); - } - if ("space".equalsIgnoreCase(remainder)) { - return KeyEvent.ofChar(' ', mods); - } - if (remainder.length() == 1) { - return KeyEvent.ofChar(remainder.charAt(0), mods); - } - return null; - } - - JsonObject getTableData(String tabName) { - if (tabName != null && !tabName.isBlank()) { - String prev = getActiveTabName(); - String switched = navigateToTab(tabName); - if (switched == null) { - return null; - } - } - MonitorTab tab = activeTab(); - return tab != null ? tab.getTableDataAsJson() : null; - } - - boolean executeAction(String actionName) { - return actionsPopup.executeActionByName(actionName); - } - - JsonObject getLogData(int limit, String filter, String level) { - return logTab.getLogDataAsJson(limit, filter, level); - } - - JsonObject getDiagramData() { - MonitorTab tab = activeTab(); - if (tab instanceof DiagramTab dt) { - return dt.getTableDataAsJson(); - } - return diagramTab.getTableDataAsJson(); - } - - void selectTraceExchange(String exchangeId) { - historyTab.selectTraceExchange(exchangeId); - } - - JsonObject getTopologyData() { - return diagramTab.getTopologyDataAsJson(); - } - - @SuppressWarnings("unchecked") - JsonObject getSpanData(String traceId, int limit) { - String pid = ctx.selectedPid; - if (pid == null) { - JsonObject err = new JsonObject(); - err.put("error", "No integration selected"); - return err; - } - try { - Path outputFile = ctx.getOutputFile(pid); - PathUtils.deleteFile(outputFile); - - JsonObject action = new JsonObject(); - action.put("action", "span"); - action.put("dump", "true"); - action.put("limit", String.valueOf(limit)); - Path actionFile = ctx.getActionFile(pid); - PathUtils.writeTextSafely(action.toJson(), actionFile); - - JsonObject response = MonitorContext.pollJsonResponse(outputFile, 3000); - if (response != null) { - PathUtils.deleteFile(outputFile); - Boolean enabled = response.getBoolean("enabled"); - if (enabled == null || !enabled) { - JsonObject err = new JsonObject(); - err.put("error", "OpenTelemetry not enabled (requires --observe flag)"); - return err; - } - if (traceId != null && !traceId.isBlank()) { - JsonArray all = response.getCollection("spans"); - if (all != null) { - JsonArray filtered = new JsonArray(); - for (int i = 0; i < all.size(); i++) { - JsonObject span = (JsonObject) all.get(i); - String tid = span.getString("traceId"); - if (tid != null && tid.contains(traceId)) { - filtered.add(span); - } - } - response.put("spans", filtered); - } - } - return response; - } - JsonObject err = new JsonObject(); - err.put("error", "Timeout waiting for span data"); - return err; - } catch (Exception e) { - JsonObject err = new JsonObject(); - err.put("error", e.getMessage()); - return err; - } - } - - String navigateDiagramToRoute(String routeId) { - navigateToTab("Diagram"); - if (diagramTab.selectRoute(routeId)) { - return routeId; - } - return null; - } - - String navigateDiagramToNode(String routeId, String nodeId) { - navigateToTab("Diagram"); - if (diagramTab.selectNode(routeId, nodeId)) { - return nodeId; - } - return null; - } - - JsonObject getDiagramState() { - return diagramTab.getDiagramStateAsJson(); - } - - JsonArray locateText(String search) { - Buffer buf = lastBuffer; - if (buf == null || search == null || search.isEmpty()) { - return new JsonArray(); - } - String screen = ExportRequest.export(buf).text().toString(); - String[] lines = screen.split("\n", -1); - int searchWidth = 0; - for (int i = 0; i < search.length();) { - int cp = search.codePointAt(i); - searchWidth += Math.max(1, CharWidth.of(cp)); - i += Character.charCount(cp); - } - JsonArray matches = new JsonArray(); - for (int y = 0; y < lines.length; y++) { - String line = lines[y]; - int idx = line.indexOf(search); - while (idx >= 0) { - int visualCol = 0; - for (int i = 0; i < idx;) { - int cp = line.codePointAt(i); - visualCol += Math.max(1, CharWidth.of(cp)); - i += Character.charCount(cp); - } - JsonObject match = new JsonObject(); - match.put("x", visualCol); - match.put("y", y); - match.put("width", searchWidth); - match.put("height", 1); - match.put("text", search); - matches.add(match); - idx = line.indexOf(search, idx + 1); - } - if (matches.size() >= 20) { - break; - } - } - return matches; - } - - JsonObject locateNodes(List nodeIds) { - return diagramTab.locateNodes(nodeIds); - } - - JsonArray getFooterActionsAsJson() { - List spans = new ArrayList<>(); - if (helpOverlay.isVisible()) { - helpOverlay.renderFooter(spans); - } else if (captionOverlay.isCaptionVisible()) { - captionOverlay.renderFooter(spans); - } else if (filesBrowser.isVisible()) { - filesBrowser.renderFooter(spans); - } else if (showSwitchPopup || showMorePopup) { - if (showSwitchPopup) { - hint(spans, "Up/Down", "select"); - hint(spans, "Enter", "switch"); - hint(spans, "Esc", "close"); - } else { - hint(spans, "Up/Down", "select"); - hint(spans, "Enter", "open"); - hint(spans, "Esc", "close"); - } - } else { - MonitorTab tab = activeTab(); - if (tabsState.selected() == TAB_OVERVIEW) { - renderOverviewFooter(spans); - } else if (tab != null) { - tab.renderFooter(spans); - insertFKeyHints(spans); - } - } - JsonArray actions = new JsonArray(); - for (int i = 0; i + 1 < spans.size(); i += 2) { - String key = spans.get(i).content().trim(); - String rawLabel = spans.get(i + 1).content().trim(); - // compact "show BHPV" pattern: key="show", then space, then 4 single-letter spans, then trailing space - if ("show".equals(key) && i + 6 < spans.size()) { - for (int j = 0; j < 4; j++) { - Span letter = spans.get(i + 2 + j); - String ch = letter.content(); - boolean on = ch.equals(ch.toUpperCase()); - JsonObject toggle = new JsonObject(); - toggle.put("key", ch.toLowerCase()); - String label = switch (ch.toLowerCase()) { - case "b" -> "body"; - case "h" -> "headers"; - case "p" -> "properties"; - case "v" -> "variables"; - default -> ch; - }; - toggle.put("label", label); - toggle.put("state", on ? "on" : "off"); - actions.add(toggle); - } - i += 5; // skip the 7-span group (loop adds 2, we consumed 5 more) - continue; - } - JsonObject action = new JsonObject(); - action.put("key", key); - int bracket = rawLabel.indexOf('['); - if (bracket > 0 && rawLabel.endsWith("]")) { - action.put("label", rawLabel.substring(0, bracket).trim()); - action.put("state", rawLabel.substring(bracket + 1, rawLabel.length() - 1)); - } else { - action.put("label", rawLabel); - } - actions.add(action); - } - return actions; - } - - void setLogLevel(String level) { - logTab.setLogLevel(level); - } - - boolean setTabFilter(String tabName, String filter) { - if (tabName != null) { - navigateToTab(tabName); - } - MonitorTab tab = activeTab(); - return tab != null && tab.setFilter(filter); - } - - boolean setTabInputValue(String tabName, String field, String value) { - if (tabName != null) { - navigateToTab(tabName); - } - MonitorTab tab = activeTab(); - return tab != null && tab.setInputValue(field, value); - } - - String toggleTraceDisplay(String section, Boolean enabled) { - return historyTab.toggleDisplaySection(section, enabled); - } - - JsonObject sendMessage(String endpoint, String body, String headers) { - if (ctx.selectedPid == null) { - return null; - } - long pid; - try { - pid = Long.parseLong(ctx.selectedPid); - } catch (NumberFormatException e) { - return null; - } - return RuntimeHelper.sendMessage(pid, endpoint, body, headers); - } - - JsonObject executeSql(String sql, String datasource, int maxRows, int queryTimeout) { - if (ctx.selectedPid == null) { - return null; - } - long pid; - try { - pid = Long.parseLong(ctx.selectedPid); - } catch (NumberFormatException e) { - return null; - } - return RuntimeHelper.executeSqlQuery(pid, sql, datasource, maxRows, queryTimeout); - } - - JsonObject updateRow(String table, String datasource, String pkValuesJson, String colValuesJson) { - if (ctx.selectedPid == null) { - return null; - } - long pid; - try { - pid = Long.parseLong(ctx.selectedPid); - } catch (NumberFormatException e) { - return null; - } - return RuntimeHelper.executeRowUpdate(pid, table, datasource, pkValuesJson, colValuesJson); - } - - String controlIntegration(String action) { - if (action == null || action.isBlank()) { - return "Error: action is required"; - } - if (ctx.selectedPid == null) { - return "Error: no integration selected"; - } - String name = selectedName(); - return switch (action) { - case "stop-routes", "pause" -> { - if (isInfraSelected()) { - yield "Error: cannot stop routes on infra service"; - } - sendRouteCommand(ctx.selectedPid, "*", "stop"); - yield "Routes stopped for " + name; - } - case "start-routes", "resume" -> { - if (isInfraSelected()) { - yield "Error: cannot start routes on infra service"; - } - sendRouteCommand(ctx.selectedPid, "*", "start"); - yield "Routes started for " + name; - } - case "restart" -> { - if (isInfraSelected()) { - yield "Error: cannot restart infra service"; - } - restartSelectedProcess(); - yield "Restarting " + name; - } - case "stop" -> { - stopSelectedProcess(false); - yield "Stopping " + name; - } - case "kill" -> { - stopSelectedProcess(true); - yield "Killed " + name; - } - default -> "Unknown action: " + action - + ". Available: stop-routes, start-routes, restart, stop, kill"; - }; - } - - private record PendingKey(KeyEvent event, long fireAt) { - } - } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java new file mode 100644 index 0000000000000..2556eae8eef32 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java @@ -0,0 +1,663 @@ +/* + * 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.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import dev.tamboui.tui.TuiRunner; +import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.PathUtils; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; + +/** + * Manages all data refresh logic for the TUI monitor: integration scanning, infra services, traces, spans, errors, and + * history data. + *

+ * Extracted from {@link CamelMonitor} to reduce class size and isolate data-loading concerns from event handling and + * rendering. + */ +class DataRefreshService { + + private static final long VANISH_DURATION_MS = 6000; + private static final int MAX_TRACES = 200; + + /** + * Callback interface for operations that remain in {@link CamelMonitor} (UI state queries needed during refresh). + */ + interface RefreshContext { + int selectedTab(); + + boolean isSwitchPopupVisible(); + + String getPendingAutoSelect(); + + void clearPendingAutoSelect(); + + void onInfraAutoSelected(int tableIndex, String pid); + + boolean isInfraSelected(); + } + + // State + private final AtomicReference> data = new AtomicReference<>(Collections.emptyList()); + private final AtomicReference> infraData = new AtomicReference<>(Collections.emptyList()); + private final Map vanishing = new ConcurrentHashMap<>(); + private final Map vanishingInfra = new ConcurrentHashMap<>(); + + // Sparkline/chart history for all metric families + private final MetricsCollector metrics = new MetricsCollector(); + + // Cached PID list -- full process scan throttled to every 2 seconds (1 second in burst mode) + private volatile List cachedPids = Collections.emptyList(); + private volatile long lastFullScanTime; + private volatile long burstModeUntil; + final Set stoppingPids = ConcurrentHashMap.newKeySet(); + + // Trace/history data -- shared between CamelMonitor and tabs + private final AtomicReference> traces = new AtomicReference<>(Collections.emptyList()); + private final Map traceFilePositions = new ConcurrentHashMap<>(); + // OTel span data -- shared between CamelMonitor and SpansTab + private final AtomicReference> otelSpans = new AtomicReference<>(List.of()); + + private volatile long lastRefresh; + private final AtomicBoolean refreshInProgress = new AtomicBoolean(false); + + private MonitorContext ctx; + private final String name; + private final RefreshContext refreshCtx; + private final Function statusFileResolver; + private final Function errorFileResolver; + + DataRefreshService( + String name, + RefreshContext refreshCtx, + Function statusFileResolver, + Function errorFileResolver) { + this.name = name; + this.refreshCtx = refreshCtx; + this.statusFileResolver = statusFileResolver; + this.errorFileResolver = errorFileResolver; + } + + /** + * Set the monitor context. Called after construction to break the circular dependency between DataRefreshService + * (owns the AtomicReferences) and MonitorContext (needs those references). + */ + void setContext(MonitorContext ctx) { + this.ctx = ctx; + } + + // ---- Public accessors ---- + + AtomicReference> data() { + return data; + } + + AtomicReference> infraData() { + return infraData; + } + + AtomicReference> traces() { + return traces; + } + + Map traceFilePositions() { + return traceFilePositions; + } + + AtomicReference> otelSpans() { + return otelSpans; + } + + MetricsCollector metrics() { + return metrics; + } + + Set stoppingPids() { + return stoppingPids; + } + + long lastRefresh() { + return lastRefresh; + } + + // ---- Burst mode ---- + + void enableBurstMode() { + burstModeUntil = System.currentTimeMillis() + 20_000; + } + + boolean isBurstMode() { + return System.currentTimeMillis() < burstModeUntil; + } + + // ---- Refresh orchestration ---- + + /** + * Trigger an asynchronous data refresh. If a refresh is already in progress, the call is a no-op. Falls back to + * synchronous refresh when no TUI runner is available. + * + * @param runner the TUI runner for scheduling background work + * @param logRefresher callback to refresh log data (stays on CamelMonitor) + * @param conditionalRefresher callback to refresh tab-conditional data (stays on CamelMonitor) + */ + void refresh(TuiRunner runner, Runnable logRefresher, Runnable conditionalRefresher) { + if (runner == null) { + refreshSync(logRefresher, conditionalRefresher); + return; + } + if (!refreshInProgress.compareAndSet(false, true)) { + return; + } + lastRefresh = System.currentTimeMillis(); + runner.scheduler().execute(() -> { + try { + refreshSync(logRefresher, conditionalRefresher); + } finally { + refreshInProgress.set(false); + } + }); + } + + /** + * Perform a synchronous data refresh cycle: log data, integration scan, auto-select, and conditional tab data. + */ + void refreshSync(Runnable logRefresher, Runnable conditionalRefresher) { + lastRefresh = System.currentTimeMillis(); + try { + logRefresher.run(); + boolean fullScan = scanIntegrations(); + List infos = data.get(); + handleAutoSelect(infos, fullScan); + conditionalRefresher.run(); + } catch (Exception e) { + // ignore refresh errors + } + } + + // ---- PID helpers ---- + + List selectedPidAsList() { + if (ctx.selectedPid == null) { + return Collections.emptyList(); + } + try { + return List.of(Long.parseLong(ctx.selectedPid)); + } catch (NumberFormatException e) { + return Collections.emptyList(); + } + } + + // ---- Integration scanning ---- + + private boolean scanIntegrations() { + List infos = new ArrayList<>(); + long now = System.currentTimeMillis(); + boolean wantFullScan = refreshCtx.selectedTab() == TabRegistry.TAB_OVERVIEW || refreshCtx.isSwitchPopupVisible() + || cachedPids.isEmpty(); + long scanInterval = isBurstMode() ? 1000 : 2000; + boolean fullScan = wantFullScan && (now - lastFullScanTime >= scanInterval); + List pids; + if (fullScan) { + pids = findPids(name); + cachedPids = pids; + lastFullScanTime = now; + } else { + pids = cachedPids; + } + + List refreshPids; + if (!fullScan && ctx.selectedPid != null) { + try { + refreshPids = List.of(Long.parseLong(ctx.selectedPid)); + } catch (NumberFormatException e) { + refreshPids = pids; + } + } else { + refreshPids = pids; + } + for (Long pid : refreshPids) { + JsonObject root = loadStatus(pid); + if (root != null) { + ProcessHandle ph = ProcessHandle.of(pid).orElse(null); + if (ph == null) { + continue; + } + IntegrationInfo info = StatusParser.parseIntegration(ph, root); + if (info != null) { + infos.add(info); + metrics.updateThroughputHistory(info); + metrics.updateEndpointHistory(info); + metrics.updateCbHistory(info); + metrics.updateHeapHistory(info); + metrics.updateLoadMetrics(ph, info); + } + } + } + if (!fullScan && ctx.selectedPid != null) { + List previous = data.get(); + for (IntegrationInfo prev : previous) { + if (!prev.vanishing && !ctx.selectedPid.equals(prev.pid)) { + infos.add(prev); + } + } + } + + handleVanishing(infos, now); + data.set(infos); + return fullScan; + } + + private void handleVanishing(List infos, long now) { + Set livePids = infos.stream().map(i -> i.pid).collect(Collectors.toSet()); + List previous = data.get(); + for (IntegrationInfo prev : previous) { + if (!prev.vanishing && !livePids.contains(prev.pid) && !vanishing.containsKey(prev.pid)) { + boolean wasExplicitStop = stoppingPids.remove(prev.pid); + if (wasExplicitStop) { + metrics.removeVanished(prev.pid); + } else { + vanishing.put(prev.pid, new VanishingInfo(prev, System.currentTimeMillis())); + } + } + } + + Iterator> it = vanishing.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (now - entry.getValue().startTime > VANISH_DURATION_MS) { + it.remove(); + metrics.removeVanished(entry.getKey()); + } else if (!livePids.contains(entry.getKey())) { + IntegrationInfo ghost = entry.getValue().info; + ghost.vanishing = true; + ghost.vanishStart = entry.getValue().startTime; + infos.add(ghost); + } else { + it.remove(); + } + } + } + + private void handleAutoSelect(List infos, boolean fullScan) { + if (ctx.selectedPid != null && !refreshCtx.isInfraSelected()) { + boolean stillAlive = infos.stream() + .anyMatch(i -> ctx.selectedPid.equals(i.pid) && !i.vanishing); + if (!stillAlive) { + IntegrationInfo gone = infos.stream() + .filter(i -> ctx.selectedPid.equals(i.pid)) + .findFirst().orElse(null); + if (gone != null) { + ctx.lastSelectedName = gone.name; + } + ctx.selectedPid = null; + } + } + + String autoSelect = refreshCtx.getPendingAutoSelect(); + if (autoSelect != null) { + for (IntegrationInfo info : infos) { + if (!info.vanishing && autoSelect.equalsIgnoreCase(info.name)) { + ctx.selectedPid = info.pid; + ctx.lastSelectedName = null; + refreshCtx.clearPendingAutoSelect(); + break; + } + } + } + + if (ctx.selectedPid == null && ctx.lastSelectedName != null && !refreshCtx.isInfraSelected()) { + for (IntegrationInfo info : infos) { + if (!info.vanishing && ctx.lastSelectedName.equalsIgnoreCase(info.name)) { + ctx.selectedPid = info.pid; + ctx.lastSelectedName = null; + break; + } + } + } + + if (fullScan) { + refreshInfraData(); + } + + if (ctx.selectedPid == null && !infraData.get().isEmpty() + && infos.stream().noneMatch(i -> !i.vanishing)) { + List infras = infraData.get(); + if (!infras.isEmpty()) { + int firstInfraIndex = infos.size() + (infras.size() > 0 ? 1 : 0); + refreshCtx.onInfraAutoSelected(firstInfraIndex, infras.get(0).pid); + } + } + } + + // ---- Infra data ---- + + @SuppressWarnings("unchecked") + private void refreshInfraData() { + List infraInfos = new ArrayList<>(); + try { + Path camelDir = CommandLineHelper.getCamelDir(); + if (Files.isDirectory(camelDir)) { + try (var files = Files.list(camelDir)) { + List jsonFiles = files + .filter(p -> { + String n = p.getFileName().toString(); + return n.startsWith("infra-") && n.endsWith(".json"); + }) + .toList(); + for (Path jsonFile : jsonFiles) { + String fn = jsonFile.getFileName().toString(); + // Format: infra-{alias}-{pid}.json + String withoutExt = fn.substring(0, fn.lastIndexOf('.')); + int lastDash = withoutExt.lastIndexOf('-'); + if (lastDash <= 6) { + continue; + } + String alias = withoutExt.substring(6, lastDash); + String pidStr = withoutExt.substring(lastDash + 1); + long pid; + try { + pid = Long.parseLong(pidStr); + } catch (NumberFormatException e) { + continue; + } + boolean alive = ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false); + + InfraInfo info = new InfraInfo(); + info.pid = pidStr; + info.alias = alias; + info.alive = alive; + try { + String json = Files.readString(jsonFile); + Object parsed = Jsoner.deserialize(json); + if (parsed instanceof Map map) { + for (Map.Entry e : map.entrySet()) { + info.properties.put(String.valueOf(e.getKey()), e.getValue()); + } + } + } catch (Exception e) { + // ignore parse errors + } + info.serviceVersion = StatusParser.objToString(info.properties.get("serviceVersion")); + infraInfos.add(info); + } + } + } + } catch (IOException e) { + // ignore + } + + // Handle vanishing infra services + Set liveInfraPids = infraInfos.stream().map(i -> i.pid).collect(Collectors.toSet()); + List previousInfra = infraData.get(); + for (InfraInfo prev : previousInfra) { + if (!prev.vanishing && !liveInfraPids.contains(prev.pid) && !vanishingInfra.containsKey(prev.pid)) { + vanishingInfra.put(prev.pid, new VanishingInfraInfo(prev, System.currentTimeMillis())); + } + } + long now = System.currentTimeMillis(); + Iterator> infraIt = vanishingInfra.entrySet().iterator(); + while (infraIt.hasNext()) { + Map.Entry entry = infraIt.next(); + if (now - entry.getValue().startTime > VANISH_DURATION_MS) { + infraIt.remove(); + } else if (!liveInfraPids.contains(entry.getKey())) { + InfraInfo ghost = entry.getValue().info; + ghost.vanishing = true; + ghost.vanishStart = entry.getValue().startTime; + infraInfos.add(ghost); + } else { + infraIt.remove(); + } + } + + infraInfos.sort((a, b) -> a.alias.compareToIgnoreCase(b.alias)); + infraData.set(infraInfos); + } + + // ---- Trace data ---- + + void refreshTraceData(List pids) { + List allTraces = new ArrayList<>(traces.get()); + + for (Long pid : pids) { + readTraceFile(Long.toString(pid), allTraces); + } + + // Sort by timestamp + allTraces.sort((a, b) -> { + if (a.timestamp == null && b.timestamp == null) { + return 0; + } + if (a.timestamp == null) { + return -1; + } + if (b.timestamp == null) { + return 1; + } + return a.timestamp.compareTo(b.timestamp); + }); + + // Keep only last MAX_TRACES + if (allTraces.size() > MAX_TRACES) { + allTraces = new ArrayList<>(allTraces.subList(allTraces.size() - MAX_TRACES, allTraces.size())); + } + + traces.set(allTraces); + } + + @SuppressWarnings("unchecked") + private void readTraceFile(String pid, List allTraces) { + Path traceFile = CommandLineHelper.getCamelDir().resolve(pid + "-trace.json"); + if (!Files.exists(traceFile)) { + return; + } + + long lastPos = traceFilePositions.getOrDefault(pid, 0L); + + try (RandomAccessFile raf = new RandomAccessFile(traceFile.toFile(), "r")) { + long length = raf.length(); + if (length <= lastPos) { + return; // no new data + } + + raf.seek(lastPos); + // If we're resuming mid-file, skip any partial line + if (lastPos > 0) { + raf.readLine(); + } + + // Read remaining bytes + long startPos = raf.getFilePointer(); + byte[] remaining = new byte[(int) (length - startPos)]; + raf.readFully(remaining); + String content = new String(remaining, StandardCharsets.UTF_8); + + traceFilePositions.put(pid, length); + + // Each line is a JSON object: {"enabled":true,"traces":[...]} + String[] lines = content.split("\n"); + for (String line : lines) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + try { + JsonObject json = (JsonObject) Jsoner.deserialize(line); + Object tracesArray = json.get("traces"); + if (tracesArray instanceof List traceList) { + for (Object traceObj : traceList) { + if (traceObj instanceof JsonObject traceJson) { + TraceEntry entry = StatusParser.parseTraceEntry(traceJson, pid); + if (entry != null) { + allTraces.add(entry); + } + } + } + } else { + // Fallback: try parsing the line itself as a trace entry + TraceEntry entry = StatusParser.parseTraceEntry(json, pid); + if (entry != null) { + allTraces.add(entry); + } + } + } catch (Exception e) { + // skip malformed lines + } + } + } catch (IOException e) { + // ignore + } + } + + // ---- Span data ---- + + @SuppressWarnings("unchecked") + void refreshSpanData() { + String pid = ctx.selectedPid; + if (pid == null) { + return; + } + try { + // Send action to request span data + Path outputFile = ctx.getOutputFile(pid); + PathUtils.deleteFile(outputFile); + + JsonObject action = new JsonObject(); + action.put("action", "span"); + action.put("dump", "true"); + action.put("limit", "500"); + Path actionFile = ctx.getActionFile(pid); + PathUtils.writeTextSafely(action.toJson(), actionFile); + + // Poll for response + JsonObject response = MonitorContext.pollJsonResponse(outputFile, 3000); + if (response != null) { + Boolean enabled = response.getBoolean("enabled"); + if (enabled != null && enabled) { + JsonArray arr = response.getCollection("spans"); + if (arr != null) { + List entries = new ArrayList<>(); + for (int i = 0; i < arr.size(); i++) { + JsonObject spanObj = (JsonObject) arr.get(i); + entries.add(SpanEntry.fromJson(spanObj)); + } + otelSpans.set(entries); + } + } + PathUtils.deleteFile(outputFile); + } + } catch (Exception e) { + // ignore + } + } + + // ---- Error data ---- + + void refreshErrorData(List pids) { + IntegrationInfo sel = ctx.findSelectedIntegration(); + if (sel == null) { + return; + } + try { + long pid = Long.parseLong(sel.pid); + JsonObject root = loadErrorFile(pid); + if (root != null) { + List parsed = StatusParser.parseErrors(root); + sel.errors.clear(); + sel.errors.addAll(parsed); + } + } catch (Exception e) { + // ignore + } + } + + // ---- History data ---- + + /** + * Load history data for the given PIDs and return the parsed entries. The caller is responsible for storing the + * entries (e.g. on HistoryTab). + */ + @SuppressWarnings("unchecked") + List loadHistoryData(List pids) { + List allEntries = new ArrayList<>(); + for (Long pid : pids) { + Path historyFile = CommandLineHelper.getCamelDir().resolve(pid + "-history.json"); + if (!Files.exists(historyFile)) { + continue; + } + try { + String content = Files.readString(historyFile); + if (content == null || content.isBlank()) { + continue; + } + JsonObject json = (JsonObject) Jsoner.deserialize(content); + Object tracesArray = json.get("traces"); + if (tracesArray instanceof List traceList) { + for (Object traceObj : traceList) { + if (traceObj instanceof JsonObject traceJson) { + HistoryEntry entry = StatusParser.parseHistoryEntry(traceJson, Long.toString(pid)); + if (entry != null) { + allEntries.add(entry); + } + } + } + } + } catch (Exception e) { + // ignore + } + } + return allEntries; + } + + // ---- Helpers ---- + + private List findPids(String name) { + return TuiHelper.findPids(name, statusFileResolver); + } + + private JsonObject loadStatus(long pid) { + return TuiHelper.loadStatus(pid, statusFileResolver); + } + + private JsonObject loadErrorFile(long pid) { + return TuiHelper.loadStatus(pid, errorFileResolver); + } + + record VanishingInfo(IntegrationInfo info, long startTime) { + } + + record VanishingInfraInfo(InfraInfo info, long startTime) { + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java new file mode 100644 index 0000000000000..99efb93f2856d --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java @@ -0,0 +1,804 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; + +import dev.tamboui.buffer.Buffer; +import dev.tamboui.export.ExportRequest; +import dev.tamboui.text.CharWidth; +import dev.tamboui.text.Span; +import dev.tamboui.tui.event.KeyCode; +import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.tui.event.KeyModifiers; +import dev.tamboui.widgets.tabs.TabsState; +import org.apache.camel.dsl.jbang.core.common.PathUtils; +import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint; + +/** + * Facade that exposes monitor state and actions to the MCP server. + *

+ * Extracts the MCP-facing accessor and control methods from {@link CamelMonitor} to reduce coupling and class size. + * {@link TuiMcpServer} depends only on this facade, not on the full monitor. + */ +class McpFacade { + + /** + * Pending key event queued for injection into the TUI event loop. + */ + record PendingKey(KeyEvent event, long fireAt) { + } + + /** + * Callback interface for operations that remain in {@link CamelMonitor}. + */ + interface MonitorBridge { + + MonitorTab activeTab(); + + void handleTabKey(int tabIndex); + + void selectMoreTab(int moreIndex); + + boolean isSwitchPopupVisible(); + + boolean isMorePopupVisible(); + + void renderOverviewFooter(List spans); + + void insertFKeyHints(List spans); + + void sendRouteCommand(String pid, String routeId, String command); + + void restartProcess(); + + void stopProcess(boolean forceKill); + } + + // Tab name constants + static final String[] TAB_NAMES = { + "Overview", "Log", "Diagram", "Routes", "Endpoints", + "HTTP", "Health", "Inspect", "Errors", "More" + }; + + static final String[] MORE_TAB_NAMES = { + "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration", + "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "SQL Query", "Spans", "Process", "Startup", + "Threads" + }; + + private final MonitorContext ctx; + private final AtomicReference> data; + private final TabsState tabsState; + private final RecordingManager recordingManager; + private final CaptionOverlay captionOverlay; + private final DrawOverlay drawOverlay; + private final HelpOverlay helpOverlay; + private final ActionsPopup actionsPopup; + private final FilesBrowser filesBrowser; + private final TabRegistry tabRegistry; + private final Queue pendingKeys; + private final MonitorBridge bridge; + + McpFacade( + MonitorContext ctx, + AtomicReference> data, + TabsState tabsState, + RecordingManager recordingManager, + CaptionOverlay captionOverlay, + DrawOverlay drawOverlay, + HelpOverlay helpOverlay, + ActionsPopup actionsPopup, + FilesBrowser filesBrowser, + TabRegistry tabRegistry, + Queue pendingKeys, + MonitorBridge bridge) { + this.ctx = ctx; + this.data = data; + this.tabsState = tabsState; + this.recordingManager = recordingManager; + this.captionOverlay = captionOverlay; + this.drawOverlay = drawOverlay; + this.helpOverlay = helpOverlay; + this.actionsPopup = actionsPopup; + this.filesBrowser = filesBrowser; + this.tabRegistry = tabRegistry; + this.pendingKeys = pendingKeys; + this.bridge = bridge; + } + + // ---- Screen state ---- + + Buffer getLastBuffer() { + return recordingManager.getLastBuffer(); + } + + long getRenderGeneration() { + return recordingManager.getRenderGeneration(); + } + + // ---- Recording / events ---- + + boolean isKeystrokesVisible() { + return recordingManager.isRecording(); + } + + TapeRecorder getTapeRecorder() { + return recordingManager.getTapeRecorder(); + } + + boolean isTapeRecording() { + return recordingManager.isTapeRecording(); + } + + void startTapeRecording(String title) { + recordingManager.startTapeRecording(title); + } + + void clearTapeRecorder() { + recordingManager.clearTapeRecorder(); + } + + TuiEventLog getEventLog() { + return recordingManager.getEventLog(); + } + + // ---- Navigation state ---- + + int getActiveTabIndex() { + return tabsState.selected(); + } + + String getActiveTabName() { + int idx = tabsState.selected(); + return idx >= 0 && idx < TAB_NAMES.length ? TAB_NAMES[idx] : "Unknown"; + } + + String getSelectedPid() { + return ctx != null ? ctx.selectedPid : null; + } + + String getSelectedIntegrationName() { + if (ctx == null) { + return null; + } + IntegrationInfo info = ctx.findSelectedIntegration(); + return info != null ? info.name : null; + } + + int getIntegrationCount() { + List list = data.get(); + return (int) list.stream().filter(i -> !i.vanishing).count(); + } + + // ---- Caption / draw overlays ---- + + boolean isCaptionVisible() { + return captionOverlay.isCaptionVisible(); + } + + void showCaption(String text) { + captionOverlay.showCaption(text); + } + + void showCaption(String text, int durationSeconds) { + captionOverlay.showCaption(text, durationSeconds); + } + + void setDrawing(List cells, int durationSeconds) { + drawOverlay.setDrawing(cells, durationSeconds); + } + + void appendDrawing(List cells) { + drawOverlay.appendDrawing(cells); + } + + void clearDrawing() { + drawOverlay.clear(); + } + + // ---- Tab navigation ---- + + String navigateToTab(String tabName) { + for (int i = 0; i < TAB_NAMES.length; i++) { + if (TAB_NAMES[i].equalsIgnoreCase(tabName)) { + bridge.handleTabKey(i); + return TAB_NAMES[i]; + } + } + // Check More submenu tabs + for (int i = 0; i < MORE_TAB_NAMES.length; i++) { + if (MORE_TAB_NAMES[i].equalsIgnoreCase(tabName)) { + bridge.selectMoreTab(i); + return MORE_TAB_NAMES[i]; + } + } + return null; + } + + String selectIntegration(String nameOrPid) { + List infos = data.get(); + for (IntegrationInfo info : infos) { + if (info.vanishing) { + continue; + } + if (nameOrPid.equals(info.pid) + || (info.name != null && info.name.equalsIgnoreCase(nameOrPid))) { + ctx.selectedPid = info.pid; + return info.name != null ? info.name : info.pid; + } + } + return null; + } + + List getTabNames() { + List names = new ArrayList<>(); + names.addAll(List.of(TAB_NAMES)); + names.addAll(List.of(MORE_TAB_NAMES)); + return names; + } + + List getActionLabels() { + return actionsPopup.getActionLabels(); + } + + SelectionContext getSelectionContext() { + SelectionContext popup = actionsPopup.getSelectionContext(); + if (popup != null) { + return popup; + } + MonitorTab tab = bridge.activeTab(); + return tab != null ? tab.getSelectionContext() : null; + } + + List getIntegrationNames() { + return data.get().stream() + .filter(i -> !i.vanishing) + .map(i -> i.name != null ? i.name : i.pid) + .toList(); + } + + // ---- Key injection ---- + + int injectKeys(List keys, int delayMs) { + long fireAt = System.currentTimeMillis(); + int count = 0; + for (String key : keys) { + KeyEvent ke = parseKey(key); + if (ke != null) { + pendingKeys.add(new PendingKey(ke, fireAt)); + fireAt += delayMs; + count++; + } + } + return count; + } + + static KeyEvent parseKey(String key) { + if (key == null || key.isEmpty()) { + return null; + } + + boolean ctrl = false; + boolean shift = false; + String remainder = key; + while (remainder.contains("+")) { + int plus = remainder.indexOf('+'); + String mod = remainder.substring(0, plus).trim(); + remainder = remainder.substring(plus + 1).trim(); + if (mod.equalsIgnoreCase("Ctrl")) { + ctrl = true; + } else if (mod.equalsIgnoreCase("Shift")) { + shift = true; + } + } + + KeyModifiers mods = KeyModifiers.of(ctrl, false, shift); + + KeyCode code = switch (remainder.toLowerCase(Locale.ROOT)) { + case "enter", "return" -> KeyCode.ENTER; + case "esc", "escape" -> KeyCode.ESCAPE; + case "tab" -> KeyCode.TAB; + case "backspace" -> KeyCode.BACKSPACE; + case "delete", "del" -> KeyCode.DELETE; + case "up" -> KeyCode.UP; + case "down" -> KeyCode.DOWN; + case "left" -> KeyCode.LEFT; + case "right" -> KeyCode.RIGHT; + case "home" -> KeyCode.HOME; + case "end" -> KeyCode.END; + case "pageup", "pgup" -> KeyCode.PAGE_UP; + case "pagedown", "pgdn" -> KeyCode.PAGE_DOWN; + case "f1" -> KeyCode.F1; + case "f2" -> KeyCode.F2; + case "f3" -> KeyCode.F3; + case "f4" -> KeyCode.F4; + case "f5" -> KeyCode.F5; + case "f6" -> KeyCode.F6; + case "f7" -> KeyCode.F7; + case "f8" -> KeyCode.F8; + case "f9" -> KeyCode.F9; + case "f10" -> KeyCode.F10; + case "f11" -> KeyCode.F11; + case "f12" -> KeyCode.F12; + case "space" -> null; + default -> null; + }; + + if (code != null) { + return KeyEvent.ofKey(code, mods); + } + if ("space".equalsIgnoreCase(remainder)) { + return KeyEvent.ofChar(' ', mods); + } + if (remainder.length() == 1) { + return KeyEvent.ofChar(remainder.charAt(0), mods); + } + return null; + } + + // ---- Data access ---- + + JsonObject getTableData(String tabName) { + if (tabName != null && !tabName.isBlank()) { + String switched = navigateToTab(tabName); + if (switched == null) { + return null; + } + } + MonitorTab tab = bridge.activeTab(); + return tab != null ? tab.getTableDataAsJson() : null; + } + + boolean executeAction(String actionName) { + return actionsPopup.executeActionByName(actionName); + } + + JsonObject getLogData(int limit, String filter, String level) { + return tabRegistry.logTab().getLogDataAsJson(limit, filter, level); + } + + JsonObject getDiagramData() { + MonitorTab tab = bridge.activeTab(); + if (tab instanceof DiagramTab dt) { + return dt.getTableDataAsJson(); + } + return tabRegistry.diagramTab().getTableDataAsJson(); + } + + void selectTraceExchange(String exchangeId) { + tabRegistry.historyTab().selectTraceExchange(exchangeId); + } + + JsonObject getTopologyData() { + return tabRegistry.diagramTab().getTopologyDataAsJson(); + } + + @SuppressWarnings("unchecked") + JsonObject getSpanData(String traceId, int limit) { + String pid = ctx.selectedPid; + if (pid == null) { + JsonObject err = new JsonObject(); + err.put("error", "No integration selected"); + return err; + } + try { + Path outputFile = ctx.getOutputFile(pid); + PathUtils.deleteFile(outputFile); + + JsonObject action = new JsonObject(); + action.put("action", "span"); + action.put("dump", "true"); + action.put("limit", String.valueOf(limit)); + Path actionFile = ctx.getActionFile(pid); + PathUtils.writeTextSafely(action.toJson(), actionFile); + + JsonObject response = MonitorContext.pollJsonResponse(outputFile, 3000); + if (response != null) { + PathUtils.deleteFile(outputFile); + Boolean enabled = response.getBoolean("enabled"); + if (enabled == null || !enabled) { + JsonObject err = new JsonObject(); + err.put("error", "OpenTelemetry not enabled (requires --observe flag)"); + return err; + } + if (traceId != null && !traceId.isBlank()) { + JsonArray all = response.getCollection("spans"); + if (all != null) { + JsonArray filtered = new JsonArray(); + for (int i = 0; i < all.size(); i++) { + JsonObject span = (JsonObject) all.get(i); + String tid = span.getString("traceId"); + if (tid != null && tid.contains(traceId)) { + filtered.add(span); + } + } + response.put("spans", filtered); + } + } + return response; + } + JsonObject err = new JsonObject(); + err.put("error", "Timeout waiting for span data"); + return err; + } catch (Exception e) { + JsonObject err = new JsonObject(); + err.put("error", e.getMessage()); + return err; + } + } + + // ---- Diagram navigation ---- + + String navigateDiagramToRoute(String routeId) { + navigateToTab("Diagram"); + if (tabRegistry.diagramTab().selectRoute(routeId)) { + return routeId; + } + return null; + } + + String navigateDiagramToNode(String routeId, String nodeId) { + navigateToTab("Diagram"); + if (tabRegistry.diagramTab().selectNode(routeId, nodeId)) { + return nodeId; + } + return null; + } + + JsonObject getDiagramState() { + return tabRegistry.diagramTab().getDiagramStateAsJson(); + } + + // ---- Screen location ---- + + JsonArray locateText(String search) { + Buffer buf = recordingManager.getLastBuffer(); + if (buf == null || search == null || search.isEmpty()) { + return new JsonArray(); + } + String screen = ExportRequest.export(buf).text().toString(); + String[] lines = screen.split("\n", -1); + int searchWidth = 0; + for (int i = 0; i < search.length();) { + int cp = search.codePointAt(i); + searchWidth += Math.max(1, CharWidth.of(cp)); + i += Character.charCount(cp); + } + JsonArray matches = new JsonArray(); + for (int y = 0; y < lines.length; y++) { + String line = lines[y]; + int idx = line.indexOf(search); + while (idx >= 0) { + int visualCol = 0; + for (int i = 0; i < idx;) { + int cp = line.codePointAt(i); + visualCol += Math.max(1, CharWidth.of(cp)); + i += Character.charCount(cp); + } + JsonObject match = new JsonObject(); + match.put("x", visualCol); + match.put("y", y); + match.put("width", searchWidth); + match.put("height", 1); + match.put("text", search); + matches.add(match); + idx = line.indexOf(search, idx + 1); + } + if (matches.size() >= 20) { + break; + } + } + return matches; + } + + JsonObject locateNodes(List nodeIds) { + return tabRegistry.diagramTab().locateNodes(nodeIds); + } + + // ---- Footer actions ---- + + JsonArray getFooterActionsAsJson() { + List spans = new ArrayList<>(); + if (helpOverlay.isVisible()) { + helpOverlay.renderFooter(spans); + } else if (captionOverlay.isCaptionVisible()) { + captionOverlay.renderFooter(spans); + } else if (filesBrowser.isVisible()) { + filesBrowser.renderFooter(spans); + } else if (bridge.isSwitchPopupVisible() || bridge.isMorePopupVisible()) { + if (bridge.isSwitchPopupVisible()) { + hint(spans, "Up/Down", "select"); + hint(spans, "Enter", "switch"); + hint(spans, "Esc", "close"); + } else { + hint(spans, "Up/Down", "select"); + hint(spans, "Enter", "open"); + hint(spans, "Esc", "close"); + } + } else { + MonitorTab tab = bridge.activeTab(); + if (tabsState.selected() == TabRegistry.TAB_OVERVIEW) { + bridge.renderOverviewFooter(spans); + } else if (tab != null) { + tab.renderFooter(spans); + bridge.insertFKeyHints(spans); + } + } + JsonArray actions = new JsonArray(); + for (int i = 0; i + 1 < spans.size(); i += 2) { + String key = spans.get(i).content().trim(); + String rawLabel = spans.get(i + 1).content().trim(); + // compact "show BHPV" pattern: key="show", then space, then 4 single-letter spans, then trailing space + if ("show".equals(key) && i + 6 < spans.size()) { + for (int j = 0; j < 4; j++) { + Span letter = spans.get(i + 2 + j); + String ch = letter.content(); + boolean on = ch.equals(ch.toUpperCase()); + JsonObject toggle = new JsonObject(); + toggle.put("key", ch.toLowerCase()); + String label = switch (ch.toLowerCase()) { + case "b" -> "body"; + case "h" -> "headers"; + case "p" -> "properties"; + case "v" -> "variables"; + default -> ch; + }; + toggle.put("label", label); + toggle.put("state", on ? "on" : "off"); + actions.add(toggle); + } + i += 5; // skip the 7-span group (loop adds 2, we consumed 5 more) + continue; + } + JsonObject action = new JsonObject(); + action.put("key", key); + int bracket = rawLabel.indexOf('['); + if (bracket > 0 && rawLabel.endsWith("]")) { + action.put("label", rawLabel.substring(0, bracket).trim()); + action.put("state", rawLabel.substring(bracket + 1, rawLabel.length() - 1)); + } else { + action.put("label", rawLabel); + } + actions.add(action); + } + return actions; + } + + // ---- Tab filter / input ---- + + void setLogLevel(String level) { + tabRegistry.logTab().setLogLevel(level); + } + + boolean setTabFilter(String tabName, String filter) { + if (tabName != null) { + navigateToTab(tabName); + } + MonitorTab tab = bridge.activeTab(); + return tab != null && tab.setFilter(filter); + } + + boolean setTabInputValue(String tabName, String field, String value) { + if (tabName != null) { + navigateToTab(tabName); + } + MonitorTab tab = bridge.activeTab(); + return tab != null && tab.setInputValue(field, value); + } + + String toggleTraceDisplay(String section, Boolean enabled) { + return tabRegistry.historyTab().toggleDisplaySection(section, enabled); + } + + // ---- Integration data ---- + + JsonObject getReadme(String name) { + List integrations = data.get(); + IntegrationInfo target = null; + if (name != null && !name.isEmpty()) { + for (IntegrationInfo info : integrations) { + if (!info.vanishing && name.equals(info.name)) { + target = info; + break; + } + } + } else { + target = ctx != null ? ctx.findSelectedIntegration() : null; + } + if (target == null) { + return null; + } + if (target.readmeFiles == null || target.readmeFiles.isEmpty()) { + return null; + } + try { + Path outputFile = ctx.getOutputFile(target.pid); + Files.deleteIfExists(outputFile); + JsonObject action = new JsonObject(); + action.put("action", "readme"); + PathUtils.writeTextSafely(action.toJson(), ctx.getActionFile(target.pid)); + return MonitorContext.pollJsonResponse(outputFile, 5000); + } catch (Exception e) { + return null; + } + } + + JsonObject getFiles(String name, String file) { + List integrations = data.get(); + IntegrationInfo target = null; + if (name != null && !name.isEmpty()) { + for (IntegrationInfo info : integrations) { + if (!info.vanishing && name.equals(info.name)) { + target = info; + break; + } + } + } else { + target = ctx != null ? ctx.findSelectedIntegration() : null; + } + if (target == null) { + return null; + } + Path dir = FilesBrowser.resolveSourceDirectory(target); + if (dir == null || !Files.isDirectory(dir)) { + return null; + } + if (file != null && !file.isEmpty()) { + Path filePath = dir.resolve(file); + if (!Files.isRegularFile(filePath)) { + return null; + } + try { + String content = Files.readString(filePath, StandardCharsets.UTF_8); + JsonObject result = new JsonObject(); + result.put("file", file); + result.put("directory", dir.toString()); + result.put("size", FilesBrowser.formatFileSize(Files.size(filePath))); + result.put("type", FilesBrowser.fileType(filePath)); + result.put("content", content); + return result; + } catch (IOException e) { + return null; + } + } + JsonArray files = new JsonArray(); + try (var stream = Files.list(dir)) { + stream.filter(Files::isRegularFile) + .sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString())) + .limit(99) + .forEach(p -> { + JsonObject entry = new JsonObject(); + entry.put("name", p.getFileName().toString()); + try { + entry.put("size", FilesBrowser.formatFileSize(Files.size(p))); + } catch (IOException e) { + entry.put("size", "0 B"); + } + entry.put("type", FilesBrowser.fileType(p)); + files.add(entry); + }); + } catch (IOException e) { + return null; + } + if (files.isEmpty()) { + return null; + } + JsonObject result = new JsonObject(); + result.put("directory", dir.toString()); + result.put("files", files); + result.put("totalFiles", files.size()); + return result; + } + + // ---- Integration messaging / control ---- + + JsonObject sendMessage(String endpoint, String body, String headers) { + if (ctx.selectedPid == null) { + return null; + } + long pid; + try { + pid = Long.parseLong(ctx.selectedPid); + } catch (NumberFormatException e) { + return null; + } + return RuntimeHelper.sendMessage(pid, endpoint, body, headers); + } + + JsonObject executeSql(String sql, String datasource, int maxRows, int queryTimeout) { + if (ctx.selectedPid == null) { + return null; + } + long pid; + try { + pid = Long.parseLong(ctx.selectedPid); + } catch (NumberFormatException e) { + return null; + } + return RuntimeHelper.executeSqlQuery(pid, sql, datasource, maxRows, queryTimeout); + } + + JsonObject updateRow(String table, String datasource, String pkValuesJson, String colValuesJson) { + if (ctx.selectedPid == null) { + return null; + } + long pid; + try { + pid = Long.parseLong(ctx.selectedPid); + } catch (NumberFormatException e) { + return null; + } + return RuntimeHelper.executeRowUpdate(pid, table, datasource, pkValuesJson, colValuesJson); + } + + String controlIntegration(String action) { + if (action == null || action.isBlank()) { + return "Error: action is required"; + } + if (ctx.selectedPid == null) { + return "Error: no integration selected"; + } + String name = ctx.selectedName(); + return switch (action) { + case "stop-routes", "pause" -> { + if (ctx.isInfraSelected()) { + yield "Error: cannot stop routes on infra service"; + } + bridge.sendRouteCommand(ctx.selectedPid, "*", "stop"); + yield "Routes stopped for " + name; + } + case "start-routes", "resume" -> { + if (ctx.isInfraSelected()) { + yield "Error: cannot start routes on infra service"; + } + bridge.sendRouteCommand(ctx.selectedPid, "*", "start"); + yield "Routes started for " + name; + } + case "restart" -> { + if (ctx.isInfraSelected()) { + yield "Error: cannot restart infra service"; + } + bridge.restartProcess(); + yield "Restarting " + name; + } + case "stop" -> { + bridge.stopProcess(false); + yield "Stopping " + name; + } + case "kill" -> { + bridge.stopProcess(true); + yield "Killed " + name; + } + default -> "Unknown action: " + action + + ". Available: stop-routes, start-routes, restart, stop, kill"; + }; + } + +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java index f10de9aac7de2..b45dea4e93a52 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java @@ -53,6 +53,23 @@ class OverviewTab implements MonitorTab { + /** + * Callback interface for process control actions triggered from overview key shortcuts. + */ + interface OverviewActions { + void sendRouteCommand(String pid, String routeId, String command); + + void stopSelectedProcess(boolean forceKill); + + void restartSelectedProcess(); + + void showKillConfirm(); + + void openDoc(IntegrationInfo info); + + void openFilesPopup(); + } + private static final long VANISH_DURATION_MS = 6000; private static final int MAX_SPARKLINE_POINTS = 60; private static final String[] SORT_COLUMNS = { "pid", "name", "version", "status", "total", "fail" }; @@ -67,6 +84,7 @@ class OverviewTab implements MonitorTab { private final Map cpuLoadAvg; private final Set stoppingPids; private final Runnable onPidChanged; + private OverviewActions actions; final TableState tableState = new TableState(); int dividerIndex = -1; @@ -89,6 +107,10 @@ class OverviewTab implements MonitorTab { this.onPidChanged = onPidChanged; } + void setActions(OverviewActions actions) { + this.actions = actions; + } + @Override public boolean handleKeyEvent(KeyEvent ke) { if (ke.isChar('s')) { @@ -105,6 +127,40 @@ public boolean handleKeyEvent(KeyEvent ke) { chartMode = (chartMode + 1) % 3; return true; } + // Process control keys + if (actions != null) { + if (ke.isChar('p') && ctx.selectedPid != null && !ctx.isInfraSelected()) { + IntegrationInfo selInfo = ctx.findSelectedIntegration(); + if (selInfo != null) { + String cmd = selInfo.routeStarted > 0 ? "stop" : "start"; + actions.sendRouteCommand(ctx.selectedPid, "*", cmd); + } + return true; + } + if (ke.isChar('x') && ctx.selectedPid != null) { + actions.stopSelectedProcess(false); + return true; + } + if (ke.isChar('X') && ctx.selectedPid != null) { + actions.showKillConfirm(); + return true; + } + if (ke.isChar('r') && ctx.selectedPid != null && !ctx.isInfraSelected()) { + actions.restartSelectedProcess(); + return true; + } + if (ke.isChar('d') && ctx.selectedPid != null && !ctx.isInfraSelected()) { + IntegrationInfo selInfo = ctx.findSelectedIntegration(); + if (selInfo != null && selInfo.readmeFiles != null && !selInfo.readmeFiles.isEmpty()) { + actions.openDoc(selInfo); + return true; + } + } + if (ke.isChar('f') && ctx.selectedPid != null && !ctx.isInfraSelected()) { + actions.openFilesPopup(); + return true; + } + } return false; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java new file mode 100644 index 0000000000000..6a615d8080374 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java @@ -0,0 +1,429 @@ +/* + * 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.List; +import java.util.function.Supplier; + +import dev.tamboui.layout.Rect; +import dev.tamboui.style.Color; +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.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.block.Title; +import dev.tamboui.widgets.list.ListItem; +import dev.tamboui.widgets.list.ListState; +import dev.tamboui.widgets.list.ListWidget; +import dev.tamboui.widgets.list.ScrollMode; +import dev.tamboui.widgets.paragraph.Paragraph; + +/** + * Manages the switch-integration, more-tabs, and kill-confirm popups. Extracted from {@link CamelMonitor} to reduce + * class size. + */ +class PopupManager { + + /** + * Callback interface for actions the popup triggers in CamelMonitor. + */ + interface PopupCallbacks { + void selectMoreTab(int index); + + void resetIntegrationTabState(); + + void refreshLogData(); + + void stopSelectedProcess(boolean forceKill); + } + + private final MonitorContext ctx; + private final Supplier> nonVanishingIntegrationsSupplier; + private final PopupCallbacks callbacks; + private final FilesBrowser filesBrowser; + + // "Switch integration" popup state + private boolean showSwitchPopup; + private final ListState switchPopupState = new ListState(); + + // "More" dropdown state + private boolean showMorePopup; + private final ListState morePopupState = new ListState(); + private int lastMoreSelection; + private Line[] currentTabLabels; + + // Kill confirm + private boolean showKillConfirm; + + PopupManager(MonitorContext ctx, Supplier> nonVanishingIntegrationsSupplier, + FilesBrowser filesBrowser, PopupCallbacks callbacks) { + this.ctx = ctx; + this.nonVanishingIntegrationsSupplier = nonVanishingIntegrationsSupplier; + this.filesBrowser = filesBrowser; + this.callbacks = callbacks; + } + + // ---- State queries ---- + + boolean isAnyPopupVisible() { + return showSwitchPopup || showMorePopup || showKillConfirm || filesBrowser.isVisible(); + } + + boolean isSwitchPopupVisible() { + return showSwitchPopup; + } + + boolean isMorePopupVisible() { + return showMorePopup; + } + + boolean isKillConfirmVisible() { + return showKillConfirm; + } + + int getLastMoreSelection() { + return lastMoreSelection; + } + + Line[] getCurrentTabLabels() { + return currentTabLabels; + } + + void setCurrentTabLabels(Line[] labels) { + this.currentTabLabels = labels; + } + + // ---- Open/close ---- + + void openSwitchPopup(String currentPid, List integrations) { + if (integrations.size() > 1) { + showSwitchPopup = true; + for (int i = 0; i < integrations.size(); i++) { + if (integrations.get(i).pid.equals(currentPid)) { + switchPopupState.select(i); + break; + } + } + } + } + + void openMorePopup() { + showMorePopup = !showMorePopup; + if (showMorePopup) { + morePopupState.select(lastMoreSelection); + } + } + + void closeMorePopup() { + showMorePopup = false; + } + + void showKillConfirm() { + showKillConfirm = true; + } + + void selectMorePopupEntry(int index) { + morePopupState.select(index); + lastMoreSelection = index; + } + + // ---- Key handling ---- + + boolean handleKeyEvent(KeyEvent ke, int selectedTab, int tabLog) { + if (filesBrowser.isVisible()) { + return filesBrowser.handleKeyEvent(ke); + } + if (showMorePopup) { + return handleMorePopupKeys(ke); + } + if (showSwitchPopup) { + return handleSwitchPopupKeys(ke, selectedTab, tabLog); + } + if (showKillConfirm) { + return handleKillConfirmKeys(ke); + } + return false; + } + + private boolean handleMorePopupKeys(KeyEvent ke) { + if (ke.isCancel()) { + showMorePopup = false; + return true; + } + if (ke.isUp()) { + morePopupState.selectPrevious(); + return true; + } + if (ke.isDown()) { + morePopupState.selectNext(15); + return true; + } + int shortcutSel = morePopupShortcut(ke); + if (shortcutSel >= 0) { + morePopupState.select(shortcutSel); + } + if (ke.isConfirm() || shortcutSel >= 0) { + showMorePopup = false; + Integer sel = shortcutSel >= 0 ? shortcutSel : morePopupState.selected(); + if (sel != null) { + callbacks.selectMoreTab(sel); + } + return true; + } + return true; + } + + private boolean handleSwitchPopupKeys(KeyEvent ke, int selectedTab, int tabLog) { + if (ke.isCancel()) { + showSwitchPopup = false; + return true; + } + List switchList = nonVanishingIntegrationsSupplier.get(); + if (ke.isUp()) { + switchPopupState.selectPrevious(); + return true; + } + if (ke.isDown()) { + switchPopupState.selectNext(switchList.size()); + return true; + } + if (ke.isConfirm()) { + showSwitchPopup = false; + Integer sel = switchPopupState.selected(); + if (sel != null && sel >= 0 && sel < switchList.size()) { + IntegrationInfo chosen = switchList.get(sel); + ctx.selectedPid = chosen.pid; + ctx.lastSelectedName = chosen.name; + callbacks.resetIntegrationTabState(); + if (selectedTab == tabLog) { + callbacks.refreshLogData(); + } + } + return true; + } + return true; + } + + private boolean handleKillConfirmKeys(KeyEvent ke) { + if (ke.isConfirm()) { + showKillConfirm = false; + callbacks.stopSelectedProcess(true); + } else { + showKillConfirm = false; + } + return true; + } + + // ---- Rendering ---- + + void renderMorePopup(Frame frame, Rect area) { + int popupW = 22; + int popupH = 17; + // Position just below the "0 More▾" tab label + int dividerW = CharWidth.of(" | "); + int tabBarX = 0; + Line[] tabLabels = currentTabLabels; + if (tabLabels != null) { + for (int i = 0; i < tabLabels.length - 1; i++) { + tabBarX += tabLabels[i].width(); + tabBarX += dividerW; + } + } + int x = area.left() + tabBarX; + int y = area.top(); + if (x + popupW > area.right()) { + x = Math.max(area.left(), area.right() - popupW); + } + Rect popup = new Rect(x, y, Math.min(popupW, area.width() - (x - area.left())), Math.min(popupH, area.height())); + + frame.renderWidget(Clear.INSTANCE, popup); + + Style keyStyle = Style.EMPTY.fg(Color.YELLOW).bold(); + ListItem[] items = { + ListItem.from(Line.from(Span.raw(" "), Span.styled("B", keyStyle), Span.raw("eans"))), + ListItem.from(Line.from(Span.raw(" Bro"), Span.styled("w", keyStyle), Span.raw("se"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("C", keyStyle), Span.raw("ircuit Breaker"))), + ListItem.from(Line.from(Span.raw(" Cl"), Span.styled("a", keyStyle), Span.raw("sspath"))), + ListItem.from(Line.from(Span.raw(" Confi"), Span.styled("g", keyStyle), Span.raw("uration"))), + ListItem.from(Line.from(Span.raw(" Co"), Span.styled("n", keyStyle), Span.raw("sumers"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("D", keyStyle), Span.raw("ataSource"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("I", keyStyle), Span.raw("nflight"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("M", keyStyle), Span.raw("emory"))), + ListItem.from(Line.from(Span.raw(" M"), Span.styled("e", keyStyle), Span.raw("trics"))), + ListItem.from(Line.from(Span.raw(" S"), Span.styled("Q", keyStyle), Span.raw("L Query"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("O", keyStyle), Span.raw("Tel Spans"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("P", keyStyle), Span.raw("rocess"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("S", keyStyle), Span.raw("tartup"))), + ListItem.from(Line.from(Span.raw(" "), Span.styled("T", keyStyle), Span.raw("hreads"))), + }; + ListWidget list = ListWidget.builder() + .items(items) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSymbol("") + .scrollMode(ScrollMode.NONE) + .block(Block.builder() + .borderType(BorderType.ROUNDED).borders(Borders.ALL) + .title(Title.from(Line.from(Span.styled(" More Tabs ", Style.EMPTY.fg(Color.YELLOW).bold())))) + .build()) + .build(); + frame.renderStatefulWidget(list, popup, morePopupState); + } + + void renderSwitchPopup(Frame frame, Rect area) { + List integrations = nonVanishingIntegrationsSupplier.get(); + if (integrations.isEmpty()) { + showSwitchPopup = false; + return; + } + + int maxLabelLen = integrations.stream() + .mapToInt(i -> { + String n = i.name != null ? i.name : "?"; + return n.length() + i.pid.length() + 14; + }) + .max().orElse(30); + int popupW = Math.min(area.width() - 4, Math.max(40, maxLabelLen + 4)); + int popupH = Math.min(area.height() - 4, integrations.size() + 2); + + 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)); + + frame.renderWidget(Clear.INSTANCE, popup); + + ListItem[] items = new ListItem[integrations.size()]; + for (int i = 0; i < integrations.size(); i++) { + IntegrationInfo info = integrations.get(i); + String name = info.name != null ? info.name : "?"; + boolean current = info.pid.equals(ctx.selectedPid); + String label = String.format(" 🐪 %s (pid:%s)%s", name, info.pid, current ? " ●" : ""); + if (current) { + items[i] = ListItem.from(Line.from(Span.styled(label, Style.EMPTY.fg(Color.CYAN)))); + } else { + items[i] = ListItem.from(label); + } + } + + ListWidget listWidget = ListWidget.builder() + .items(items) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSymbol("") + .scrollMode(ScrollMode.NONE) + .block(Block.builder() + .borderType(BorderType.ROUNDED).borders(Borders.ALL) + .title(Title.from( + Line.from(Span.styled(" Switch Integration ", Style.EMPTY.fg(Color.YELLOW).bold())))) + .build()) + .build(); + frame.renderStatefulWidget(listWidget, popup, switchPopupState); + } + + void renderKillConfirm(Frame frame, Rect area) { + String name = ctx.selectedName(); + String msg = " Kill " + name + " (PID: " + ctx.selectedPid + ")? "; + int popupW = Math.max(34, msg.length() + 4); + int popupH = 6; + int x = area.left() + Math.max(0, (area.width() - popupW) / 2); + int y = area.top() + Math.max(0, (area.height() - popupH) / 2); + 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) + .borderStyle(Style.EMPTY.fg(Color.LIGHT_RED)) + .title(" Confirm Kill ") + .build(); + frame.renderWidget(block, popup); + Rect inner = block.inner(popup); + frame.renderWidget( + Paragraph.builder() + .text(Text.from( + Line.from(Span.raw("")), + Line.from(Span.styled(msg, Style.EMPTY.fg(Color.LIGHT_RED).bold())), + Line.from(Span.raw("")), + Line.from( + Span.raw(" "), + Span.styled("Enter", Style.EMPTY.bold()), + Span.raw(" confirm "), + Span.styled("Esc", Style.EMPTY.bold()), + Span.raw(" cancel")))) + .build(), + inner); + } + + // ---- Static utilities ---- + + static int morePopupShortcut(KeyEvent ke) { + if (ke.isChar('b')) { + return 0; + } + if (ke.isChar('w')) { + return 1; + } + if (ke.isChar('c')) { + return 2; + } + if (ke.isChar('a')) { + return 3; + } + if (ke.isChar('g')) { + return 4; + } + if (ke.isChar('n')) { + return 5; + } + if (ke.isChar('d')) { + return 6; + } + if (ke.isChar('i')) { + return 7; + } + if (ke.isChar('m')) { + return 8; + } + if (ke.isChar('e')) { + return 9; + } + if (ke.isChar('q')) { + return 10; + } + if (ke.isChar('o')) { + return 11; + } + if (ke.isChar('p')) { + return 12; + } + if (ke.isChar('s')) { + return 13; + } + if (ke.isChar('t')) { + return 14; + } + return -1; + } + + List getNonVanishingIntegrations() { + return nonVanishingIntegrationsSupplier.get(); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java new file mode 100644 index 0000000000000..9aea7a863a2c8 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java @@ -0,0 +1,280 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import dev.tamboui.buffer.Buffer; +import dev.tamboui.export.ExportRequest; +import dev.tamboui.tui.event.KeyCode; +import dev.tamboui.tui.event.KeyEvent; + +/** + * Manages screenshot capture, tape recording, keystroke display, and screen buffer tracking. Extracted from + * {@link CamelMonitor} to reduce class size. + */ +class RecordingManager { + + record KeyRecord(String label, long timestamp) { + } + + private final CaptionOverlay captionOverlay; + + private boolean recording; + private final AtomicReference tapeRecorderRef = new AtomicReference<>(); + private volatile Buffer lastBuffer; + private volatile long renderGeneration; + private volatile String screenshotMessage; + private volatile long screenshotMessageTime; + private volatile boolean pendingScreenshot; + private final List recentKeys = new ArrayList<>(); + private TuiEventLog eventLog; + + RecordingManager(CaptionOverlay captionOverlay) { + this.captionOverlay = captionOverlay; + } + + void init(boolean initialRecording) { + this.recording = initialRecording; + this.eventLog = new TuiEventLog(500); + } + + // ---- Key recording ---- + + void recordKey(KeyEvent ke, boolean mcpInjected) { + String label = keyLabel(ke); + if (label != null) { + if (eventLog != null) { + eventLog.record(label, label); + } + if (recording) { + recentKeys.add(new KeyRecord(label, System.currentTimeMillis())); + } + TapeRecorder tr = tapeRecorderRef.get(); + if (tr != null && tr.isActive() && !mcpInjected) { + tr.recordKey(label); + } + } + } + + // ---- Recording state ---- + + boolean isRecording() { + return recording; + } + + void toggleRecording() { + recording = !recording; + } + + // ---- Screenshot ---- + + void requestScreenshot() { + pendingScreenshot = true; + } + + boolean processPendingScreenshot() { + if (pendingScreenshot) { + pendingScreenshot = false; + takeScreenshot(); + return true; + } + return false; + } + + void takeScreenshot() { + Buffer buf = lastBuffer; + if (buf == null) { + return; + } + try { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); + String baseName = "camel-tui-screenshot-" + timestamp; + Path svgPath = Path.of(baseName + ".svg"); + Path txtPath = Path.of(baseName + ".txt"); + Path ansPath = Path.of(baseName + ".ans"); + ExportRequest.export(buf).svg().toFile(svgPath); + ExportRequest.export(buf).text().toFile(txtPath); + ExportRequest.export(buf).text().options(o -> o.styles(true)).toFile(ansPath); + screenshotMessage = "Screenshot saved to " + svgPath.toAbsolutePath() + " (and .txt, .ans)"; + screenshotMessageTime = System.currentTimeMillis(); + } catch (IOException e) { + screenshotMessage = "Screenshot failed: " + e.getMessage(); + screenshotMessageTime = System.currentTimeMillis(); + } + } + + // ---- Buffer tracking ---- + + void updateBuffer(Buffer buffer) { + lastBuffer = buffer; + renderGeneration++; + } + + long getRenderGeneration() { + return renderGeneration; + } + + Buffer getLastBuffer() { + return lastBuffer; + } + + // ---- Screenshot flash message ---- + + String screenshotFlashMessage() { + String msg = screenshotMessage; + if (msg != null && System.currentTimeMillis() - screenshotMessageTime < 5000) { + return msg; + } + screenshotMessage = null; + return null; + } + + // ---- Tape recording ---- + + AtomicReference tapeRecorderRef() { + return tapeRecorderRef; + } + + TapeRecorder getTapeRecorder() { + return tapeRecorderRef.get(); + } + + boolean isTapeRecording() { + TapeRecorder rec = tapeRecorderRef.get(); + return rec != null && rec.isActive(); + } + + void startTapeRecording(String title) { + TapeRecorder rec = new TapeRecorder(); + rec.start(title); + tapeRecorderRef.set(rec); + } + + void clearTapeRecorder() { + tapeRecorderRef.set(null); + } + + void toggleTapeRecording() { + TapeRecorder rec = tapeRecorderRef.get(); + if (rec != null && rec.isActive()) { + String tape = rec.stop(); + tapeRecorderRef.set(null); + String timestamp = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); + String filename = "camel-tui-tape-" + timestamp + ".tape"; + try { + Files.writeString(Path.of(filename), tape); + captionOverlay.showCaption("Tape saved: " + filename, 5); + } catch (IOException e) { + captionOverlay.showCaption("Failed to save tape: " + e.getMessage(), 5); + } + } else { + rec = new TapeRecorder(); + rec.start(null); + tapeRecorderRef.set(rec); + captionOverlay.showCaption("Tape recording started", 3); + } + } + + // ---- Event log ---- + + TuiEventLog getEventLog() { + return eventLog; + } + + // ---- Recent keys ---- + + List getRecentKeys() { + return recentKeys; + } + + void tickRecentKeys(long now) { + if (recording && !recentKeys.isEmpty()) { + long cutoff = now - 2000; + recentKeys.removeIf(k -> k.timestamp() < cutoff); + } + } + + // ---- Key label utility ---- + + static String keyLabel(KeyEvent ke) { + if (ke.isKey(KeyCode.ENTER)) { + return "Enter"; + } + if (ke.isKey(KeyCode.ESCAPE)) { + return "Esc"; + } + if (ke.isKey(KeyCode.TAB)) { + return ke.hasShift() ? "⇧Tab" : "Tab"; + } + if (ke.isKey(KeyCode.UP)) { + return "↑"; + } + if (ke.isKey(KeyCode.DOWN)) { + return "↓"; + } + if (ke.isKey(KeyCode.LEFT)) { + return "←"; + } + if (ke.isKey(KeyCode.RIGHT)) { + return "→"; + } + if (ke.isKey(KeyCode.PAGE_UP)) { + return "PgUp"; + } + if (ke.isKey(KeyCode.PAGE_DOWN)) { + return "PgDn"; + } + if (ke.isKey(KeyCode.HOME)) { + return "Home"; + } + if (ke.isKey(KeyCode.END)) { + return "End"; + } + if (ke.isKey(KeyCode.BACKSPACE)) { + return "⌫"; + } + for (int i = 1; i <= 12; i++) { + try { + KeyCode fKey = KeyCode.valueOf("F" + i); + if (ke.isKey(fKey)) { + return "F" + i; + } + } catch (IllegalArgumentException e) { + break; + } + } + if (ke.code() == KeyCode.CHAR) { + String s = ke.string(); + if (" ".equals(s)) { + return "Space"; + } + if (!s.isEmpty()) { + return s; + } + } + return null; + } +} 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 new file mode 100644 index 0000000000000..68a948ed51e28 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java @@ -0,0 +1,305 @@ +/* + * 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.List; + +import dev.tamboui.widgets.tabs.TabsState; + +/** + * Owns all tab instances, tab index constants, and tab navigation logic. Extracted from {@link CamelMonitor} to reduce + * class size. + */ +class TabRegistry { + + // Tab indices + static final int TAB_OVERVIEW = 0; + static final int TAB_LOG = 1; + static final int TAB_DIAGRAM = 2; + static final int TAB_ROUTES = 3; + static final int TAB_ENDPOINTS = 4; + static final int TAB_HTTP = 5; + static final int TAB_HEALTH = 6; + static final int TAB_HISTORY = 7; + static final int TAB_ERRORS = 8; + static final int TAB_MORE = 9; + + static final int NUM_TABS = 10; + + /** + * Callbacks for operations that remain in {@link CamelMonitor} or other collaborators. + */ + interface TabCallbacks { + void refreshLogData(); + + void refreshHistoryData(List pids); + + void refreshTraceData(List pids); + + void refreshErrorData(List pids); + + void openMorePopup(); + + void closeMorePopup(); + + void selectMorePopupEntry(int index); + } + + private final TabsState tabsState; + private TabCallbacks callbacks; + + // Tab instances + private LogTab logTab; + private DiagramTab diagramTab; + private RoutesTab routesTab; + private ConsumersTab consumersTab; + private EndpointsTab endpointsTab; + private HttpTab httpTab; + private HealthTab healthTab; + private HistoryTab historyTab; + private CircuitBreakerTab circuitBreakerTab; + private ErrorsTab errorsTab; + private MetricsTab metricsTab; + private StartupTab startupTab; + private ConfigurationTab configurationTab; + private BeansTab beansTab; + private BrowseTab browseTab; + private ClasspathTab classpathTab; + private InflightTab inflightTab; + private MemoryTab memoryTab; + private ThreadsTab threadsTab; + private SpansTab spansTab; + private ProcessTab processTab; + private OverviewTab overviewTab; + private DataSourceTab dataSourceTab; + private SqlQueryTab sqlQueryTab; + + private MonitorTab activeMoreTab; + + TabRegistry(TabsState tabsState) { + this.tabsState = tabsState; + } + + void setCallbacks(TabCallbacks callbacks) { + this.callbacks = callbacks; + } + + void initTabs(MonitorContext ctx, DataRefreshService dataService, Runnable resetIntegrationTabState) { + logTab = new LogTab(ctx); + diagramTab = new DiagramTab(ctx); + routesTab = new RoutesTab(ctx); + consumersTab = new ConsumersTab(ctx); + dataSourceTab = new DataSourceTab(ctx); + sqlQueryTab = new SqlQueryTab(ctx); + endpointsTab = new EndpointsTab(ctx, dataService.metrics()); + httpTab = new HttpTab(ctx); + healthTab = new HealthTab(ctx); + historyTab = new HistoryTab(ctx, dataService.traces(), dataService.traceFilePositions()); + circuitBreakerTab = new CircuitBreakerTab(ctx, dataService.metrics()); + errorsTab = new ErrorsTab(ctx); + metricsTab = new MetricsTab(ctx); + startupTab = new StartupTab(ctx); + configurationTab = new ConfigurationTab(ctx); + beansTab = new BeansTab(ctx); + browseTab = new BrowseTab(ctx); + classpathTab = new ClasspathTab(ctx); + inflightTab = new InflightTab(ctx); + memoryTab = new MemoryTab(ctx, dataService.metrics()); + threadsTab = new ThreadsTab(ctx); + spansTab = new SpansTab(ctx, dataService.otelSpans()); + processTab = new ProcessTab(ctx); + overviewTab = new OverviewTab( + ctx, dataService.metrics(), dataService.stoppingPids(), + resetIntegrationTabState); + } + + // ---- Tab access ---- + + MonitorTab activeTab() { + return switch (tabsState.selected()) { + case TAB_OVERVIEW -> overviewTab; + case TAB_LOG -> logTab; + case TAB_DIAGRAM -> diagramTab; + case TAB_ROUTES -> routesTab; + case TAB_ENDPOINTS -> endpointsTab; + case TAB_HEALTH -> healthTab; + case TAB_HISTORY -> historyTab; + case TAB_HTTP -> httpTab; + case TAB_ERRORS -> errorsTab; + case TAB_MORE -> activeMoreTab; + default -> null; + }; + } + + MonitorTab getActiveMoreTab() { + return activeMoreTab; + } + + int selectedTabIndex() { + return tabsState.selected(); + } + + // ---- Navigation ---- + + boolean handleTabKey(int tab, MonitorContext ctx, DataRefreshService dataService) { + if (tab != TAB_OVERVIEW) { + overviewTab.selectCurrentIntegration(); + routesTab.preloadDiagram(); + diagramTab.preloadDiagram(); + } + if (tab == TAB_LOG) { + callbacks.refreshLogData(); + logTab.onTabSelected(); + } + if (tab == TAB_ROUTES && routesTab != null && routesTab.isShowDiagram()) { + routesTab.closeDiagram(); + } + if (tab == TAB_DIAGRAM) { + diagramTab.onTabSelected(); + } + if (tab == TAB_HISTORY && ctx.selectedPid != null) { + try { + long pid = Long.parseLong(ctx.selectedPid); + historyTab.historyEntries = dataService.loadHistoryData(List.of(pid)); + dataService.refreshTraceData(List.of(pid)); + } catch (NumberFormatException e) { + // ignore + } + historyTab.onTabSelected(); + } + if (tab == TAB_ERRORS && ctx.selectedPid != null) { + try { + long pid = Long.parseLong(ctx.selectedPid); + dataService.refreshErrorData(List.of(pid)); + } catch (NumberFormatException e) { + // ignore + } + errorsTab.onTabSelected(); + } + if (tab == TAB_MORE) { + callbacks.openMorePopup(); + return true; + } + callbacks.closeMorePopup(); + tabsState.select(tab); + return true; + } + + void selectMoreTab(int index) { + callbacks.selectMorePopupEntry(index); + activeMoreTab = switch (index) { + case 0 -> beansTab; + case 1 -> browseTab; + case 2 -> circuitBreakerTab; + case 3 -> classpathTab; + case 4 -> configurationTab; + case 5 -> consumersTab; + case 6 -> dataSourceTab; + case 7 -> inflightTab; + case 8 -> memoryTab; + case 9 -> metricsTab; + case 10 -> sqlQueryTab; + case 11 -> spansTab; + case 12 -> processTab; + case 13 -> startupTab; + case 14 -> threadsTab; + default -> null; + }; + if (activeMoreTab != null) { + overviewTab.selectCurrentIntegration(); + tabsState.select(TAB_MORE); + activeMoreTab.onTabSelected(); + } + } + + void resetIntegrationTabState(DataRefreshService dataService, FilesBrowser filesBrowser) { + diagramTab.onIntegrationChanged(); + routesTab.onIntegrationChanged(); + httpTab.onIntegrationChanged(); + logTab.onIntegrationChanged(); + historyTab.onIntegrationChanged(); + beansTab.onIntegrationChanged(); + browseTab.onIntegrationChanged(); + threadsTab.onIntegrationChanged(); + startupTab.onIntegrationChanged(); + configurationTab.onIntegrationChanged(); + consumersTab.onIntegrationChanged(); + dataSourceTab.onIntegrationChanged(); + sqlQueryTab.onIntegrationChanged(); + circuitBreakerTab.onIntegrationChanged(); + inflightTab.onIntegrationChanged(); + spansTab.onIntegrationChanged(); + processTab.onIntegrationChanged(); + dataService.otelSpans().set(List.of()); + + filesBrowser.reset(); + + // Preload diagram data in background so it's ready when the user switches tabs + routesTab.preloadDiagram(); + diagramTab.preloadDiagram(); + } + + void navigateUp() { + activeTab().navigateUp(); + } + + void navigateDown() { + activeTab().navigateDown(); + } + + // ---- Typed tab accessors ---- + + LogTab logTab() { + return logTab; + } + + DiagramTab diagramTab() { + return diagramTab; + } + + RoutesTab routesTab() { + return routesTab; + } + + HttpTab httpTab() { + return httpTab; + } + + HealthTab healthTab() { + return healthTab; + } + + HistoryTab historyTab() { + return historyTab; + } + + ErrorsTab errorsTab() { + return errorsTab; + } + + SpansTab spansTab() { + return spansTab; + } + + OverviewTab overviewTab() { + return overviewTab; + } + + SqlQueryTab sqlQueryTab() { + return sqlQueryTab; + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java index 73a522768b119..0a3b71cbf7e73 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java @@ -67,16 +67,16 @@ record LogEntry(String timestamp, LogLevel level, String message, String request } private final int port; - private final CamelMonitor monitor; + private final McpFacade facade; private HttpServer server; private volatile String clientName; private volatile long lastActivity; private volatile long lastToolCallTime; private final List activityLog = new ArrayList<>(); - TuiMcpServer(int port, CamelMonitor monitor) { + TuiMcpServer(int port, McpFacade facade) { this.port = port; - this.monitor = monitor; + this.facade = facade; } void start() throws IOException { @@ -645,7 +645,7 @@ private JsonObject handleToolsCall(JsonObject request) { } private void addSelectionContext(JsonObject result) { - SelectionContext ctx = monitor.getSelectionContext(); + SelectionContext ctx = facade.getSelectionContext(); if (ctx != null) { JsonObject sel = new JsonObject(); sel.put("type", ctx.type()); @@ -660,14 +660,14 @@ private void addSelectionContext(JsonObject result) { } private void addFooterActions(JsonObject result) { - JsonArray actions = monitor.getFooterActionsAsJson(); + JsonArray actions = facade.getFooterActionsAsJson(); if (actions != null && !actions.isEmpty()) { result.put("actions", actions); } } private String callGetScreen(Map args) { - Buffer buf = monitor.getLastBuffer(); + Buffer buf = facade.getLastBuffer(); if (buf == null) { return "Screen not yet available"; } @@ -691,7 +691,7 @@ private String callGetEvents(Map args) { limit = n.intValue(); } - TuiEventLog eventLog = monitor.getEventLog(); + TuiEventLog eventLog = facade.getEventLog(); List events = eventLog.getRecent(limit); JsonArray eventsArray = new JsonArray(); @@ -711,23 +711,23 @@ private String callGetEvents(Map args) { private String callGetState() { JsonObject result = new JsonObject(); - result.put("activeTab", monitor.getActiveTabName()); - result.put("tabIndex", monitor.getActiveTabIndex()); + result.put("activeTab", facade.getActiveTabName()); + result.put("tabIndex", facade.getActiveTabIndex()); - String pid = monitor.getSelectedPid(); + String pid = facade.getSelectedPid(); if (pid != null) { result.put("selectedPid", pid); } - String name = monitor.getSelectedIntegrationName(); + String name = facade.getSelectedIntegrationName(); if (name != null) { result.put("selectedIntegration", name); } - result.put("integrationCount", monitor.getIntegrationCount()); - result.put("keystrokesVisible", monitor.isKeystrokesVisible()); - result.put("captionVisible", monitor.isCaptionVisible()); + result.put("integrationCount", facade.getIntegrationCount()); + result.put("keystrokesVisible", facade.isKeystrokesVisible()); + result.put("captionVisible", facade.isCaptionVisible()); addSelectionContext(result); addFooterActions(result); - JsonObject diagramState = monitor.getDiagramState(); + JsonObject diagramState = facade.getDiagramState(); if (diagramState != null) { result.put("diagram", diagramState); } @@ -745,17 +745,17 @@ private String callShowCaption(Map args) { duration = n.intValue(); } - TapeRecorder recorder = monitor.getTapeRecorder(); + TapeRecorder recorder = facade.getTapeRecorder(); if (recorder != null && recorder.isActive()) { recorder.resetClock(); recorder.recordCaption(text, Math.max(duration, 0)); } if (duration > 0) { - monitor.showCaption(text, duration); + facade.showCaption(text, duration); return "Caption displayed (auto-dismiss in " + duration + "s): " + text; } - monitor.showCaption(text); + facade.showCaption(text); return "Caption displayed: " + text; } @@ -768,42 +768,42 @@ private String callNavigate(Map args) { if (tab == null && integration == null && route == null && node == null) { result.put("error", "Provide at least one of: tab, integration, route, node"); - result.put("availableTabs", toJsonArray(monitor.getTabNames())); - result.put("availableIntegrations", toJsonArray(monitor.getIntegrationNames())); + result.put("availableTabs", toJsonArray(facade.getTabNames())); + result.put("availableIntegrations", toJsonArray(facade.getIntegrationNames())); return Jsoner.serialize(result); } if (integration != null) { - String selected = monitor.selectIntegration(integration); + String selected = facade.selectIntegration(integration); if (selected != null) { result.put("selectedIntegration", selected); } else { result.put("integrationError", "Not found: " + integration); - result.put("availableIntegrations", toJsonArray(monitor.getIntegrationNames())); + result.put("availableIntegrations", toJsonArray(facade.getIntegrationNames())); } } if (tab != null) { - String switched = monitor.navigateToTab(tab); + String switched = facade.navigateToTab(tab); if (switched != null) { result.put("activeTab", switched); - TapeRecorder recorder = monitor.getTapeRecorder(); + TapeRecorder recorder = facade.getTapeRecorder(); if (recorder != null && recorder.isActive()) { recorder.resetClock(); - int tabIndex = monitor.getTabNames().indexOf(switched); + int tabIndex = facade.getTabNames().indexOf(switched); if (tabIndex >= 0 && tabIndex < 9) { recorder.recordKey(String.valueOf(tabIndex + 1)); } } } else { result.put("tabError", "Unknown tab: " + tab); - result.put("availableTabs", toJsonArray(monitor.getTabNames())); + result.put("availableTabs", toJsonArray(facade.getTabNames())); } } // Diagram route/node navigation (route selection in topology doesn't need render wait) if (node == null && route != null) { - String selected = monitor.navigateDiagramToRoute(route); + String selected = facade.navigateDiagramToRoute(route); if (selected != null) { result.put("selectedRoute", route); } else { @@ -816,14 +816,14 @@ private String callNavigate(Map args) { if (node != null) { // Drill into the route first (sets topologyMode=false) if (route != null) { - monitor.navigateDiagramToNode(route, null); + facade.navigateDiagramToNode(route, null); } } - long beforeGen = monitor.getRenderGeneration(); + long beforeGen = facade.getRenderGeneration(); long deadline = System.currentTimeMillis() + 2000; while (System.currentTimeMillis() < deadline) { - if (monitor.getRenderGeneration() >= beforeGen + 2) { + if (facade.getRenderGeneration() >= beforeGen + 2) { break; } try { @@ -836,7 +836,7 @@ private String callNavigate(Map args) { // Now that the render has populated EIP node boxes, select the node if (node != null) { - String selected = monitor.navigateDiagramToNode(null, node); + String selected = facade.navigateDiagramToNode(null, node); if (selected != null) { result.put("selectedNode", node); if (route != null) { @@ -847,7 +847,7 @@ private String callNavigate(Map args) { + (route != null ? " in route " + route : "")); } } - Buffer buf = monitor.getLastBuffer(); + Buffer buf = facade.getLastBuffer(); if (buf != null) { result.put("screen", ExportRequest.export(buf).text().toString()); } @@ -873,15 +873,15 @@ private String callSendKeys(Map args) { if (delayArg instanceof Number n) { delay = Math.max(80, n.intValue()); } - TapeRecorder recorder = monitor.getTapeRecorder(); + TapeRecorder recorder = facade.getTapeRecorder(); if (recorder != null && recorder.isActive()) { recorder.resetClock(); recorder.recordKeys(keys, delay); } boolean wait = Boolean.TRUE.equals(args.get("wait")); - long beforeGen = wait ? monitor.getRenderGeneration() : 0; - int sent = monitor.injectKeys(keys, delay); + long beforeGen = wait ? facade.getRenderGeneration() : 0; + int sent = facade.injectKeys(keys, delay); if (!wait) { return "Queued " + sent + " key(s) with " + delay + "ms delay"; @@ -894,7 +894,7 @@ private String callSendKeys(Map args) { while (System.currentTimeMillis() < deadline) { long now = System.currentTimeMillis(); if (now >= lastKeyFireAt) { - long gen = monitor.getRenderGeneration(); + long gen = facade.getRenderGeneration(); if (gen >= beforeGen + sent + 2) { break; } @@ -907,7 +907,7 @@ private String callSendKeys(Map args) { } } - Buffer buf = monitor.getLastBuffer(); + Buffer buf = facade.getLastBuffer(); JsonObject result = new JsonObject(); result.put("sent", sent); result.put("delay", delay); @@ -922,17 +922,17 @@ private String callSendKeys(Map args) { private String callGetOptions() { JsonObject result = new JsonObject(); - result.put("tabs", toJsonArray(monitor.getTabNames())); - result.put("activeTab", monitor.getActiveTabName()); - result.put("activeTabIndex", monitor.getActiveTabIndex()); - result.put("integrations", toJsonArray(monitor.getIntegrationNames())); - String selected = monitor.getSelectedIntegrationName(); + result.put("tabs", toJsonArray(facade.getTabNames())); + result.put("activeTab", facade.getActiveTabName()); + result.put("activeTabIndex", facade.getActiveTabIndex()); + result.put("integrations", toJsonArray(facade.getIntegrationNames())); + String selected = facade.getSelectedIntegrationName(); if (selected != null) { result.put("selectedIntegration", selected); } - result.put("integrationCount", monitor.getIntegrationCount()); + result.put("integrationCount", facade.getIntegrationCount()); - List actions = monitor.getActionLabels(); + List actions = facade.getActionLabels(); JsonArray actionsArray = new JsonArray(); for (int i = 0; i < actions.size(); i++) { JsonObject action = new JsonObject(); @@ -969,14 +969,14 @@ private String callWaitForIdle(Map args) { requiredFrames = Math.max(1, Math.min(10, n.intValue())); } - long startGeneration = monitor.getRenderGeneration(); + long startGeneration = facade.getRenderGeneration(); long start = System.currentTimeMillis(); long deadline = start + timeout; while (System.currentTimeMillis() < deadline) { - long current = monitor.getRenderGeneration(); + long current = facade.getRenderGeneration(); if (current >= startGeneration + requiredFrames) { - Buffer buf = monitor.getLastBuffer(); + Buffer buf = facade.getLastBuffer(); JsonObject result = new JsonObject(); result.put("settled", true); result.put("waitedMs", System.currentTimeMillis() - start); @@ -1003,23 +1003,23 @@ private String callWaitForIdle(Map args) { } private String callTapeStart(Map args) { - if (monitor.isTapeRecording()) { + if (facade.isTapeRecording()) { return "Tape recording is already active. Stop it first with tui_tape_stop."; } String title = args.get("title") instanceof String s ? s : null; - monitor.startTapeRecording(title); + facade.startTapeRecording(title); return "Tape recording started" + (title != null ? ": " + title : ""); } private String callTapeStop(Map args) { - if (!monitor.isTapeRecording()) { + if (!facade.isTapeRecording()) { return "No tape recording is active. Start one with tui_tape_start."; } - TapeRecorder recorder = monitor.getTapeRecorder(); + TapeRecorder recorder = facade.getTapeRecorder(); String tape = recorder.stop(); int keyCount = recorder.getKeyCount(); long durationMs = recorder.getDurationMs(); - monitor.clearTapeRecorder(); + facade.clearTapeRecorder(); JsonObject result = new JsonObject(); result.put("tape", tape); @@ -1047,7 +1047,7 @@ private String callSleep(Map args) { int seconds = secArg instanceof Number n ? n.intValue() : 3; seconds = Math.max(1, Math.min(30, seconds)); - TapeRecorder recorder = monitor.getTapeRecorder(); + TapeRecorder recorder = facade.getTapeRecorder(); if (recorder != null && recorder.isActive()) { recorder.resetClock(); recorder.recordSleep(seconds * 1000L); @@ -1116,9 +1116,9 @@ private String callDraw(Map args) { } if (append) { - monitor.appendDrawing(drawCells); + facade.appendDrawing(drawCells); } else { - monitor.setDrawing(drawCells, duration); + facade.setDrawing(drawCells, duration); } return "Drawing " + drawCells.size() + " cell(s)" @@ -1127,13 +1127,13 @@ private String callDraw(Map args) { } private String callDrawClear() { - monitor.clearDrawing(); + facade.clearDrawing(); return "Drawing cleared"; } private String callGetTable(Map args) { String tab = args.get("tab") instanceof String s ? s : null; - JsonObject data = monitor.getTableData(tab); + JsonObject data = facade.getTableData(tab); if (data == null) { return "No table data available" + (tab != null ? " for tab: " + tab : ""); } @@ -1145,7 +1145,7 @@ private String callAction(Map args) { if (action == null || action.isBlank()) { return "Error: action is required"; } - boolean executed = monitor.executeAction(action); + boolean executed = facade.executeAction(action); if (executed) { return "Action '" + action + "' executed"; } @@ -1161,12 +1161,12 @@ private String callGetLog(Map args) { } String filter = args.get("filter") instanceof String s ? s : null; String level = args.get("level") instanceof String s ? s : null; - JsonObject data = monitor.getLogData(limit, filter, level); + JsonObject data = facade.getLogData(limit, filter, level); return Jsoner.serialize(data); } private String callGetErrors() { - JsonObject data = monitor.getTableData("Errors"); + JsonObject data = facade.getTableData("Errors"); if (data == null) { JsonObject empty = new JsonObject(); empty.put("tab", "Errors"); @@ -1178,7 +1178,7 @@ private String callGetErrors() { } private String callGetDiagram() { - JsonObject data = monitor.getDiagramData(); + JsonObject data = facade.getDiagramData(); if (data == null) { return "No diagram available. Navigate to the Diagram tab first."; } @@ -1188,10 +1188,10 @@ private String callGetDiagram() { private String callGetHistory(Map args) { String exchangeId = args.get("exchangeId") instanceof String s ? s : null; if (exchangeId != null && !exchangeId.isBlank()) { - monitor.navigateToTab("History"); - monitor.selectTraceExchange(exchangeId); + facade.navigateToTab("History"); + facade.selectTraceExchange(exchangeId); } - JsonObject data = monitor.getTableData("History"); + JsonObject data = facade.getTableData("History"); if (data == null) { return "No history data available. Ensure the History tab has data."; } @@ -1199,7 +1199,7 @@ private String callGetHistory(Map args) { } private String callGetTopology() { - JsonObject data = monitor.getTopologyData(); + JsonObject data = facade.getTopologyData(); if (data == null) { return "No topology data available. The Diagram tab may not have loaded yet."; } @@ -1212,7 +1212,7 @@ private String callGetSpans(Map args) { if (args.get("limit") instanceof Number n) { limit = n.intValue(); } - JsonObject data = monitor.getSpanData(traceId, limit); + JsonObject data = facade.getSpanData(traceId, limit); return Jsoner.serialize(data); } @@ -1223,7 +1223,7 @@ private String callSendMessage(Map args) { } String body = args.get("body") instanceof String s ? s : null; String headers = args.get("headers") instanceof String s ? s : null; - JsonObject response = monitor.sendMessage(endpoint, body, headers); + JsonObject response = facade.sendMessage(endpoint, body, headers); if (response == null) { return "Error: no integration selected or PID unavailable"; } @@ -1238,7 +1238,7 @@ private String callExecuteSql(Map args) { String datasource = args.get("datasource") instanceof String s ? s : null; int maxRows = args.get("maxRows") instanceof Number n ? n.intValue() : 100; int queryTimeout = args.get("queryTimeout") instanceof Number n ? n.intValue() : 30; - JsonObject response = monitor.executeSql(query, datasource, maxRows, queryTimeout); + JsonObject response = facade.executeSql(query, datasource, maxRows, queryTimeout); if (response == null) { return "Error: no integration selected or PID unavailable"; } @@ -1259,7 +1259,7 @@ private String callUpdateRow(Map args) { return "Error: columnValues is required (JSON object)"; } String datasource = args.get("datasource") instanceof String s ? s : null; - JsonObject response = monitor.updateRow(table, datasource, pkValues, colValues); + JsonObject response = facade.updateRow(table, datasource, pkValues, colValues); if (response == null) { return "Error: no integration selected or PID unavailable"; } @@ -1276,14 +1276,14 @@ private String callSetLogLevel(Map args) { && !"DEBUG".equals(level) && !"TRACE".equals(level)) { return "Error: invalid level '" + level + "'. Must be ERROR, WARN, INFO, DEBUG, or TRACE"; } - monitor.setLogLevel(level); + facade.setLogLevel(level); return "Log level set to " + level; } private String callFilter(Map args) { String filter = args.get("filter") instanceof String s ? s : ""; String tab = args.get("tab") instanceof String s ? s : null; - boolean applied = monitor.setTabFilter(tab, filter); + boolean applied = facade.setTabFilter(tab, filter); if (!applied) { return "This tab does not support text filtering"; } @@ -1297,7 +1297,7 @@ private String callSetInput(Map args) { } String value = args.get("value") instanceof String s ? s : ""; String tab = args.get("tab") instanceof String s ? s : null; - boolean applied = monitor.setTabInputValue(tab, field, value); + boolean applied = facade.setTabInputValue(tab, field, value); if (!applied) { return "Error: field '" + field + "' not found on " + (tab != null ? tab : "active") + " tab"; } @@ -1310,7 +1310,7 @@ private String callToggleTraceDisplay(Map args) { return "Error: section is required (headers, properties, variables, body, wrap)"; } Boolean enabled = args.get("enabled") instanceof Boolean b ? b : null; - String result = monitor.toggleTraceDisplay(section, enabled); + String result = facade.toggleTraceDisplay(section, enabled); if (result == null) { return "Error: unknown section '" + section + "'. Must be headers, properties, variables, body, or wrap"; } @@ -1319,7 +1319,7 @@ private String callToggleTraceDisplay(Map args) { private String callGetReadme(Map args) { String name = args.get("name") instanceof String s ? s : null; - JsonObject response = monitor.getReadme(name); + JsonObject response = facade.getReadme(name); if (response == null) { return name != null ? "No README found for integration '" + name + "'" @@ -1338,13 +1338,13 @@ private String callControl(Map args) { if (action == null || action.isBlank()) { return "Error: action is required"; } - return monitor.controlIntegration(action); + return facade.controlIntegration(action); } private String callGetFiles(Map args) { String name = args.get("name") instanceof String s ? s : null; String file = args.get("file") instanceof String s ? s : null; - JsonObject response = monitor.getFiles(name, file); + JsonObject response = facade.getFiles(name, file); if (response == null) { return name != null ? "No source files found for integration '" + name + "'" @@ -1364,11 +1364,11 @@ private String callLocate(Map args) { JsonObject result = new JsonObject(); if (text != null) { - JsonArray matches = monitor.locateText(text); + JsonArray matches = facade.locateText(text); result.put("matches", matches); } else if (node != null || nodes != null) { List ids = nodes != null ? nodes : List.of(node); - JsonObject located = monitor.locateNodes(ids); + JsonObject located = facade.locateNodes(ids); if (located != null) { result.put("matches", located.get("matches")); if (located.containsKey("bounds")) { @@ -1421,9 +1421,9 @@ private String callDrawShape(Map args) { boolean append = args.get("append") instanceof Boolean b && b; if (append) { - monitor.appendDrawing(cells); + facade.appendDrawing(cells); } else { - monitor.setDrawing(cells, duration); + facade.setDrawing(cells, duration); } return "Drew " + shape + " at (" + x + "," + y + ")"; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java index 656ab31674901..cade44fb0baf1 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java @@ -29,7 +29,7 @@ class CamelMonitorParseKeyTest { @Test void parseKeySingleChar() { - KeyEvent ke = CamelMonitor.parseKey("a"); + KeyEvent ke = McpFacade.parseKey("a"); assertNotNull(ke); assertEquals(KeyCode.CHAR, ke.code()); assertEquals('a', ke.character()); @@ -37,92 +37,92 @@ void parseKeySingleChar() { @Test void parseKeyEnter() { - KeyEvent ke = CamelMonitor.parseKey("enter"); + KeyEvent ke = McpFacade.parseKey("enter"); assertNotNull(ke); assertEquals(KeyCode.ENTER, ke.code()); } @Test void parseKeyReturn() { - KeyEvent ke = CamelMonitor.parseKey("return"); + KeyEvent ke = McpFacade.parseKey("return"); assertNotNull(ke); assertEquals(KeyCode.ENTER, ke.code()); } @Test void parseKeyEscape() { - KeyEvent ke = CamelMonitor.parseKey("esc"); + KeyEvent ke = McpFacade.parseKey("esc"); assertNotNull(ke); assertEquals(KeyCode.ESCAPE, ke.code()); } @Test void parseKeyEscapeFull() { - KeyEvent ke = CamelMonitor.parseKey("escape"); + KeyEvent ke = McpFacade.parseKey("escape"); assertNotNull(ke); assertEquals(KeyCode.ESCAPE, ke.code()); } @Test void parseKeyTab() { - KeyEvent ke = CamelMonitor.parseKey("tab"); + KeyEvent ke = McpFacade.parseKey("tab"); assertNotNull(ke); assertEquals(KeyCode.TAB, ke.code()); } @Test void parseKeyBackspace() { - KeyEvent ke = CamelMonitor.parseKey("backspace"); + KeyEvent ke = McpFacade.parseKey("backspace"); assertNotNull(ke); assertEquals(KeyCode.BACKSPACE, ke.code()); } @Test void parseKeyDelete() { - KeyEvent ke = CamelMonitor.parseKey("delete"); + KeyEvent ke = McpFacade.parseKey("delete"); assertNotNull(ke); assertEquals(KeyCode.DELETE, ke.code()); } @Test void parseKeyDeleteShort() { - KeyEvent ke = CamelMonitor.parseKey("del"); + KeyEvent ke = McpFacade.parseKey("del"); assertNotNull(ke); assertEquals(KeyCode.DELETE, ke.code()); } @Test void parseKeyArrows() { - assertEquals(KeyCode.UP, CamelMonitor.parseKey("up").code()); - assertEquals(KeyCode.DOWN, CamelMonitor.parseKey("down").code()); - assertEquals(KeyCode.LEFT, CamelMonitor.parseKey("left").code()); - assertEquals(KeyCode.RIGHT, CamelMonitor.parseKey("right").code()); + assertEquals(KeyCode.UP, McpFacade.parseKey("up").code()); + assertEquals(KeyCode.DOWN, McpFacade.parseKey("down").code()); + assertEquals(KeyCode.LEFT, McpFacade.parseKey("left").code()); + assertEquals(KeyCode.RIGHT, McpFacade.parseKey("right").code()); } @Test void parseKeyHomeEnd() { - assertEquals(KeyCode.HOME, CamelMonitor.parseKey("home").code()); - assertEquals(KeyCode.END, CamelMonitor.parseKey("end").code()); + assertEquals(KeyCode.HOME, McpFacade.parseKey("home").code()); + assertEquals(KeyCode.END, McpFacade.parseKey("end").code()); } @Test void parseKeyPageUpDown() { - assertEquals(KeyCode.PAGE_UP, CamelMonitor.parseKey("pageup").code()); - assertEquals(KeyCode.PAGE_UP, CamelMonitor.parseKey("pgup").code()); - assertEquals(KeyCode.PAGE_DOWN, CamelMonitor.parseKey("pagedown").code()); - assertEquals(KeyCode.PAGE_DOWN, CamelMonitor.parseKey("pgdn").code()); + assertEquals(KeyCode.PAGE_UP, McpFacade.parseKey("pageup").code()); + assertEquals(KeyCode.PAGE_UP, McpFacade.parseKey("pgup").code()); + assertEquals(KeyCode.PAGE_DOWN, McpFacade.parseKey("pagedown").code()); + assertEquals(KeyCode.PAGE_DOWN, McpFacade.parseKey("pgdn").code()); } @Test void parseKeyFKeys() { - assertEquals(KeyCode.F1, CamelMonitor.parseKey("f1").code()); - assertEquals(KeyCode.F6, CamelMonitor.parseKey("f6").code()); - assertEquals(KeyCode.F12, CamelMonitor.parseKey("f12").code()); + assertEquals(KeyCode.F1, McpFacade.parseKey("f1").code()); + assertEquals(KeyCode.F6, McpFacade.parseKey("f6").code()); + assertEquals(KeyCode.F12, McpFacade.parseKey("f12").code()); } @Test void parseKeySpace() { - KeyEvent ke = CamelMonitor.parseKey("space"); + KeyEvent ke = McpFacade.parseKey("space"); assertNotNull(ke); assertEquals(KeyCode.CHAR, ke.code()); assertEquals(' ', ke.character()); @@ -130,7 +130,7 @@ void parseKeySpace() { @Test void parseKeyCtrlModifier() { - KeyEvent ke = CamelMonitor.parseKey("Ctrl+c"); + KeyEvent ke = McpFacade.parseKey("Ctrl+c"); assertNotNull(ke); assertEquals(KeyCode.CHAR, ke.code()); assertEquals('c', ke.character()); @@ -139,7 +139,7 @@ void parseKeyCtrlModifier() { @Test void parseKeyShiftModifier() { - KeyEvent ke = CamelMonitor.parseKey("Shift+F6"); + KeyEvent ke = McpFacade.parseKey("Shift+F6"); assertNotNull(ke); assertEquals(KeyCode.F6, ke.code()); assertTrue(ke.hasShift()); @@ -147,7 +147,7 @@ void parseKeyShiftModifier() { @Test void parseKeyCtrlAndShift() { - KeyEvent ke = CamelMonitor.parseKey("Ctrl+Shift+a"); + KeyEvent ke = McpFacade.parseKey("Ctrl+Shift+a"); assertNotNull(ke); assertTrue(ke.hasCtrl()); assertTrue(ke.hasShift()); @@ -155,21 +155,21 @@ void parseKeyCtrlAndShift() { @Test void parseKeyNullReturnsNull() { - assertNull(CamelMonitor.parseKey(null)); + assertNull(McpFacade.parseKey(null)); } @Test void parseKeyEmptyReturnsNull() { - assertNull(CamelMonitor.parseKey("")); + assertNull(McpFacade.parseKey("")); } @Test void parseKeyCaseInsensitive() { - KeyEvent ke1 = CamelMonitor.parseKey("ENTER"); + KeyEvent ke1 = McpFacade.parseKey("ENTER"); assertNotNull(ke1); assertEquals(KeyCode.ENTER, ke1.code()); - KeyEvent ke2 = CamelMonitor.parseKey("Enter"); + KeyEvent ke2 = McpFacade.parseKey("Enter"); assertNotNull(ke2); assertEquals(KeyCode.ENTER, ke2.code()); } @@ -177,6 +177,6 @@ void parseKeyCaseInsensitive() { @Test void parseKeyUnknownMultiCharReturnsNull() { // Multi-character string that is not a known key name - assertNull(CamelMonitor.parseKey("xyz")); + assertNull(McpFacade.parseKey("xyz")); } }