From c8bdba9832605db44245a390cbb477de99f75345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Pedersen?= Date: Wed, 18 Feb 2026 15:06:09 +0100 Subject: [PATCH] Added aesh-tamboui integration module for TamboUI TUI framework Provides three integration levels for building rich TUI interfaces in aesh commands: TuiSupport utility, TuiCommand (event-loop based), and TuiAppCommand (declarative Element rendering). Includes demo commands showcasing gauges, tables, sparklines, bar charts, tabs, calendar, and dashboard layouts. --- .gitignore | 3 + aesh-tamboui/pom.xml | 128 ++++ .../aesh/tamboui/NonClosingConnection.java | 155 +++++ .../java/org/aesh/tamboui/TuiAppCommand.java | 133 +++++ .../java/org/aesh/tamboui/TuiCommand.java | 108 ++++ .../java/org/aesh/tamboui/TuiSupport.java | 107 ++++ .../aesh/tamboui/examples/TuiDemoExample.java | 545 ++++++++++++++++++ .../java/org/aesh/command/shell/Shell.java | 12 + .../main/java/org/aesh/console/ShellImpl.java | 5 + pom.xml | 10 + 10 files changed, 1206 insertions(+) create mode 100644 aesh-tamboui/pom.xml create mode 100644 aesh-tamboui/src/main/java/org/aesh/tamboui/NonClosingConnection.java create mode 100644 aesh-tamboui/src/main/java/org/aesh/tamboui/TuiAppCommand.java create mode 100644 aesh-tamboui/src/main/java/org/aesh/tamboui/TuiCommand.java create mode 100644 aesh-tamboui/src/main/java/org/aesh/tamboui/TuiSupport.java create mode 100644 aesh-tamboui/src/main/java/org/aesh/tamboui/examples/TuiDemoExample.java diff --git a/.gitignore b/.gitignore index fca5066b..c7c18b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ classes # ignore maven versionsBackup *.versionsBackup + +# ignore shade plugin output +dependency-reduced-pom.xml diff --git a/aesh-tamboui/pom.xml b/aesh-tamboui/pom.xml new file mode 100644 index 00000000..ca7de05a --- /dev/null +++ b/aesh-tamboui/pom.xml @@ -0,0 +1,128 @@ + + + + + + + + org.aesh + aesh-all + 3.0 + + 4.0.0 + + org.aesh + aesh-tamboui + jar + 3.0 + Æsh TamboUI Integration + TamboUI TUI framework integration for Æsh commands + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + + + + 11 + 11 + + + + + org.aesh + aesh + ${project.version} + + + + org.aesh + readline + ${readline.version} + + + + dev.tamboui + tamboui-core + ${tamboui.version} + + + + dev.tamboui + tamboui-widgets + ${tamboui.version} + + + + dev.tamboui + tamboui-tui + ${tamboui.version} + + + + dev.tamboui + tamboui-toolkit + ${tamboui.version} + + + + dev.tamboui + tamboui-aesh-backend + ${tamboui.version} + + + + + + + maven-compiler-plugin + + 11 + 11 + + + + maven-jar-plugin + + + maven-shade-plugin + + + package + + shade + + + + + org.aesh.tamboui.examples.TuiDemoExample + + + + + + + + + + diff --git a/aesh-tamboui/src/main/java/org/aesh/tamboui/NonClosingConnection.java b/aesh-tamboui/src/main/java/org/aesh/tamboui/NonClosingConnection.java new file mode 100644 index 00000000..de9850cb --- /dev/null +++ b/aesh-tamboui/src/main/java/org/aesh/tamboui/NonClosingConnection.java @@ -0,0 +1,155 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed 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.aesh.tamboui; + +import java.nio.charset.Charset; +import java.util.function.Consumer; + +import org.aesh.terminal.Attributes; +import org.aesh.terminal.Connection; +import org.aesh.terminal.Device; +import org.aesh.terminal.tty.Capability; +import org.aesh.terminal.tty.Signal; +import org.aesh.terminal.tty.Size; + +/** + * A Connection wrapper that delegates everything to the underlying connection + * but makes {@link #close()} a no-op. This prevents TamboUI's + * {@code AeshBackend.close()} from closing the aesh Connection, which would + * terminate the aesh session. + * + * @author Aesh team + */ +class NonClosingConnection implements Connection { + + private final Connection delegate; + + NonClosingConnection(Connection delegate) { + this.delegate = delegate; + } + + @Override + public void close() { + // intentionally empty — aesh owns this connection's lifecycle + } + + @Override + public void close(int exit) { + // intentionally empty + } + + @Override + public Device device() { + return delegate.device(); + } + + @Override + public Size size() { + return delegate.size(); + } + + @Override + public Consumer getSizeHandler() { + return delegate.getSizeHandler(); + } + + @Override + public void setSizeHandler(Consumer handler) { + delegate.setSizeHandler(handler); + } + + @Override + public Consumer getSignalHandler() { + return delegate.getSignalHandler(); + } + + @Override + public void setSignalHandler(Consumer handler) { + delegate.setSignalHandler(handler); + } + + @Override + public Consumer getStdinHandler() { + return delegate.getStdinHandler(); + } + + @Override + public void setStdinHandler(Consumer handler) { + delegate.setStdinHandler(handler); + } + + @Override + public Consumer stdoutHandler() { + return delegate.stdoutHandler(); + } + + @Override + public void setCloseHandler(Consumer closeHandler) { + delegate.setCloseHandler(closeHandler); + } + + @Override + public Consumer getCloseHandler() { + return delegate.getCloseHandler(); + } + + @Override + public void openBlocking() { + delegate.openBlocking(); + } + + @Override + public void openNonBlocking() { + delegate.openNonBlocking(); + } + + @Override + public boolean reading() { + return delegate.reading(); + } + + @Override + public boolean put(Capability capability, Object... params) { + return delegate.put(capability, params); + } + + @Override + public Attributes getAttributes() { + return delegate.getAttributes(); + } + + @Override + public void setAttributes(Attributes attr) { + delegate.setAttributes(attr); + } + + @Override + public Charset inputEncoding() { + return delegate.inputEncoding(); + } + + @Override + public Charset outputEncoding() { + return delegate.outputEncoding(); + } + + @Override + public boolean supportsAnsi() { + return delegate.supportsAnsi(); + } +} diff --git a/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiAppCommand.java b/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiAppCommand.java new file mode 100644 index 00000000..b324d41d --- /dev/null +++ b/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiAppCommand.java @@ -0,0 +1,133 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed 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.aesh.tamboui; + +import org.aesh.command.Command; +import org.aesh.command.CommandException; +import org.aesh.command.CommandResult; +import org.aesh.command.invocation.CommandInvocation; +import org.aesh.command.shell.Shell; +import org.aesh.terminal.Connection; + +import dev.tamboui.backend.aesh.AeshBackend; +import dev.tamboui.toolkit.app.ToolkitRunner; +import dev.tamboui.toolkit.element.Element; +import dev.tamboui.toolkit.event.EventResult; +import dev.tamboui.tui.TuiConfig; +import dev.tamboui.tui.event.KeyEvent; + +/** + * Abstract base class for aesh commands that use TamboUI's ToolkitRunner + * with declarative Element-based rendering. + *

+ * Subclasses implement {@link #render()} to return the UI element tree. + * Optionally override {@link #onKeyEvent(KeyEvent, ToolkitRunner)} for + * custom key handling and {@link #onStart(ToolkitRunner)} for initialization. + *

+ * Example: + *

+ * {@literal @}CommandDefinition(name = "status", description = "System status")
+ * public class StatusCommand extends TuiAppCommand {
+ *     {@literal @}Override
+ *     protected Element render() {
+ *         return panel("System Status",
+ *             text("Hello from TamboUI!")
+ *         ).rounded();
+ *     }
+ * }
+ * 
+ * + * @author Aesh team + */ +public abstract class TuiAppCommand implements Command { + + /** + * Return the UI element tree. Called each frame to produce the UI. + * + * @return the root Element to render + */ + protected abstract Element render(); + + /** + * Handle key events. Return true if handled, false to pass through. + * Default implementation quits on 'q' or Ctrl+C. + * + * @param event the key event + * @param runner the ToolkitRunner + * @return true if the event was handled + */ + protected boolean onKeyEvent(KeyEvent event, ToolkitRunner runner) { + if (event.isQuit()) { + runner.quit(); + return true; + } + return false; + } + + /** + * Called after the TUI starts. Override to run initialization logic + * such as scheduling background tasks. + * + * @param runner the ToolkitRunner + */ + protected void onStart(ToolkitRunner runner) { + } + + /** + * Override to customize TUI configuration (tick rate, mouse capture, etc.). + * The backend is already set. + * + * @param builder the pre-configured TuiConfig builder + * @return the builder (for chaining) + */ + protected TuiConfig.Builder configure(TuiConfig.Builder builder) { + return builder; + } + + @Override + public final CommandResult execute(CommandInvocation invocation) throws CommandException, InterruptedException { + Shell shell = invocation.getShell(); + Connection conn = shell.connection(); + if (conn == null) { + invocation.println("TUI commands require a terminal connection."); + return CommandResult.FAILURE; + } + try { + AeshBackend backend = new AeshBackend(new NonClosingConnection(conn)); + TuiConfig.Builder builder = TuiConfig.builder() + .backend(backend) + .shutdownHook(false); // aesh manages the lifecycle + TuiConfig config = configure(builder).build(); + try (ToolkitRunner runner = ToolkitRunner.create(config)) { + runner.eventRouter().addGlobalHandler(event -> { + if (event instanceof KeyEvent) { + return onKeyEvent((KeyEvent) event, runner) + ? EventResult.HANDLED + : EventResult.UNHANDLED; + } + return EventResult.UNHANDLED; + }); + onStart(runner); + runner.run(this::render); + } + return CommandResult.SUCCESS; + } catch (Exception e) { + throw new CommandException("TUI error: " + e.getMessage(), e); + } + } +} diff --git a/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiCommand.java b/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiCommand.java new file mode 100644 index 00000000..8b39bf51 --- /dev/null +++ b/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiCommand.java @@ -0,0 +1,108 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed 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.aesh.tamboui; + +import org.aesh.command.Command; +import org.aesh.command.CommandException; +import org.aesh.command.CommandResult; +import org.aesh.command.invocation.CommandInvocation; +import org.aesh.command.shell.Shell; +import org.aesh.terminal.Connection; + +import dev.tamboui.backend.aesh.AeshBackend; +import dev.tamboui.tui.TuiConfig; +import dev.tamboui.tui.TuiRunner; + +/** + * Abstract base class for aesh commands that use TamboUI's TuiRunner + * for event-loop based TUI rendering. + *

+ * Subclasses implement {@link #runTui(TuiRunner, CommandInvocation)} to define + * the event handling and rendering logic. Optionally override + * {@link #configure(TuiConfig.Builder)} to customize tick rate, mouse capture, etc. + *

+ * Example: + *

+ * {@literal @}CommandDefinition(name = "dashboard", description = "Show dashboard")
+ * public class DashboardCommand extends TuiCommand {
+ *     {@literal @}Override
+ *     protected void runTui(TuiRunner runner, CommandInvocation inv) throws Exception {
+ *         runner.run(
+ *             (event, r) -> {
+ *                 if (event instanceof KeyEvent key && key.isQuit()) {
+ *                     r.quit();
+ *                     return true;
+ *                 }
+ *                 return false;
+ *             },
+ *             frame -> {
+ *                 // render widgets to frame
+ *             }
+ *         );
+ *     }
+ * }
+ * 
+ * + * @author Aesh team + */ +public abstract class TuiCommand implements Command { + + /** + * Implement TUI logic using the TuiRunner. + * The runner is already configured and connected to the terminal. + * + * @param runner the TuiRunner to use for event loop and rendering + * @param invocation the command invocation context + * @throws Exception if TUI execution fails + */ + protected abstract void runTui(TuiRunner runner, CommandInvocation invocation) throws Exception; + + /** + * Override to customize TUI configuration (tick rate, mouse capture, etc.). + * The backend is already set; this lets you configure other options. + * + * @param builder the pre-configured TuiConfig builder + * @return the builder (for chaining) + */ + protected TuiConfig.Builder configure(TuiConfig.Builder builder) { + return builder; + } + + @Override + public final CommandResult execute(CommandInvocation invocation) throws CommandException, InterruptedException { + Shell shell = invocation.getShell(); + Connection conn = shell.connection(); + if (conn == null) { + invocation.println("TUI commands require a terminal connection."); + return CommandResult.FAILURE; + } + try { + AeshBackend backend = new AeshBackend(new NonClosingConnection(conn)); + TuiConfig.Builder builder = TuiConfig.builder() + .backend(backend) + .shutdownHook(false); // aesh manages the lifecycle + TuiConfig config = configure(builder).build(); + try (TuiRunner runner = TuiRunner.create(config)) { + runTui(runner, invocation); + } + return CommandResult.SUCCESS; + } catch (Exception e) { + throw new CommandException("TUI error: " + e.getMessage(), e); + } + } +} diff --git a/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiSupport.java b/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiSupport.java new file mode 100644 index 00000000..1bce6ed0 --- /dev/null +++ b/aesh-tamboui/src/main/java/org/aesh/tamboui/TuiSupport.java @@ -0,0 +1,107 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed 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.aesh.tamboui; + +import java.io.IOException; + +import org.aesh.command.shell.Shell; +import org.aesh.terminal.Connection; + +import dev.tamboui.backend.aesh.AeshBackend; +import dev.tamboui.toolkit.app.ToolkitRunner; +import dev.tamboui.tui.TuiConfig; +import dev.tamboui.tui.TuiRunner; + +/** + * Static utility class providing factory methods for integrating TamboUI + * with aesh commands via the Shell's Connection. + * + * @author Aesh team + */ +public final class TuiSupport { + + private TuiSupport() { + } + + /** + * Create an AeshBackend from a Shell's Connection. + * + * @param shell the aesh Shell + * @return a new AeshBackend wrapping the shell's connection + * @throws IllegalStateException if the shell has no connection + */ + public static AeshBackend createBackend(Shell shell) throws IOException { + Connection conn = shell.connection(); + if (conn == null) { + throw new IllegalStateException("Shell does not have a terminal connection"); + } + return new AeshBackend(new NonClosingConnection(conn)); + } + + /** + * Create a TuiConfig.Builder pre-configured with the Shell's Connection as backend. + * + * @param shell the aesh Shell + * @return a TuiConfig.Builder with backend already set + * @throws IllegalStateException if the shell has no connection + */ + public static TuiConfig.Builder configBuilder(Shell shell) throws IOException { + return TuiConfig.builder() + .backend(createBackend(shell)) + .shutdownHook(false); + } + + /** + * Create a TuiRunner wired to the Shell's terminal using default configuration. + * + * @param shell the aesh Shell + * @return a new TuiRunner ready to use + * @throws Exception if runner creation fails + * @throws IllegalStateException if the shell has no connection + */ + public static TuiRunner createRunner(Shell shell) throws Exception { + return TuiRunner.create(configBuilder(shell).build()); + } + + /** + * Create a TuiRunner with custom config, wired to the Shell's terminal. + * The backend on the provided builder will be overridden with the Shell's connection. + * + * @param shell the aesh Shell + * @param configBuilder a pre-configured TuiConfig.Builder (backend will be set) + * @return a new TuiRunner ready to use + * @throws Exception if runner creation fails + * @throws IllegalStateException if the shell has no connection + */ + public static TuiRunner createRunner(Shell shell, TuiConfig.Builder configBuilder) throws Exception { + configBuilder.backend(createBackend(shell)); + return TuiRunner.create(configBuilder.build()); + } + + /** + * Create a ToolkitRunner wired to the Shell's terminal using default configuration. + * + * @param shell the aesh Shell + * @return a new ToolkitRunner ready to use + * @throws Exception if runner creation fails + * @throws IllegalStateException if the shell has no connection + */ + public static ToolkitRunner createToolkitRunner(Shell shell) throws Exception { + return ToolkitRunner.create(configBuilder(shell).build()); + } +} diff --git a/aesh-tamboui/src/main/java/org/aesh/tamboui/examples/TuiDemoExample.java b/aesh-tamboui/src/main/java/org/aesh/tamboui/examples/TuiDemoExample.java new file mode 100644 index 00000000..92465f12 --- /dev/null +++ b/aesh-tamboui/src/main/java/org/aesh/tamboui/examples/TuiDemoExample.java @@ -0,0 +1,545 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed 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.aesh.tamboui.examples; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.aesh.AeshConsoleRunner; +import org.aesh.command.CommandDefinition; +import org.aesh.command.invocation.CommandInvocation; +import org.aesh.command.option.Option; +import org.aesh.tamboui.TuiAppCommand; +import org.aesh.tamboui.TuiCommand; + +import dev.tamboui.layout.Constraint; +import dev.tamboui.style.Color; +import dev.tamboui.style.Style; +import dev.tamboui.toolkit.app.ToolkitRunner; +import dev.tamboui.toolkit.element.Element; +import dev.tamboui.tui.TuiConfig; +import dev.tamboui.tui.TuiRunner; +import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.tui.event.TickEvent; +import dev.tamboui.widgets.barchart.Bar; +import dev.tamboui.widgets.barchart.BarGroup; +import dev.tamboui.widgets.block.Block; +import dev.tamboui.widgets.gauge.Gauge; +import dev.tamboui.widgets.sparkline.Sparkline; +import dev.tamboui.widgets.table.TableState; +import dev.tamboui.widgets.tabs.TabsState; + +import static dev.tamboui.toolkit.Toolkit.*; + +/** + * Demo example showing TUI commands integrated with aesh. + * Run this class and type the command names at the aesh prompt. + * + * @author Aesh team + */ +public class TuiDemoExample { + + /** + * Minimal TuiAppCommand that shows a styled panel with a welcome message. + * Press 'q' to quit (handled by TuiAppCommand default key handling). + */ + @CommandDefinition(name = "tui-hello", description = "Show a hello panel") + public static class HelloCommand extends TuiAppCommand { + + @Override + protected Element render() { + return panel("Hello TamboUI!", + text("Welcome to aesh + TamboUI integration.\n\nPress 'q' to quit.") + ).rounded().borderColor(Color.CYAN).fill(); + } + } + + /** + * TuiCommand with an animated progress bar that advances on each tick event. + * Uses TuiRunner's event loop directly for fine-grained control. + */ + @CommandDefinition(name = "tui-gauge", description = "Animated progress bar") + public static class GaugeCommand extends TuiCommand { + + @Option(name = "speed", shortName = 's', defaultValue = {"100"}, + description = "Tick rate in milliseconds") + private int speedMs; + + @Override + protected TuiConfig.Builder configure(TuiConfig.Builder builder) { + return builder.tickRate(Duration.ofMillis(speedMs)); + } + + @Override + protected void runTui(TuiRunner runner, CommandInvocation invocation) throws Exception { + AtomicInteger progress = new AtomicInteger(0); + + runner.run( + (event, r) -> { + if (event instanceof KeyEvent) { + KeyEvent key = (KeyEvent) event; + if (key.isQuit()) { + r.quit(); + return false; + } + } + if (event instanceof TickEvent) { + progress.getAndUpdate(v -> (v + 1) % 101); + return true; + } + return false; + }, + frame -> { + Gauge gauge = Gauge.builder() + .percent(progress.get()) + .label("Loading... " + progress.get() + "%") + .gaugeColor(Color.GREEN) + .block(Block.bordered()) + .build(); + + frame.renderWidget(gauge, frame.area()); + } + ); + } + } + + /** + * TuiAppCommand showing sample data in a table with keyboard navigation. + * Uses the declarative Element DSL via ToolkitRunner. + */ + @CommandDefinition(name = "tui-table", description = "Show a data table") + public static class TableCommand extends TuiAppCommand { + + private final TableState tableState = new TableState(); + + @Override + protected Element render() { + return column( + panel("Employee Directory", + table() + .header("ID", "Name", "Role", "City") + .widths( + Constraint.length(4), + Constraint.percentage(25), + Constraint.percentage(25), + Constraint.fill(1) + ) + .row("1", "Alice", "Engineer", "San Francisco") + .row("2", "Bob", "Designer", "New York") + .row("3", "Carol", "Manager", "London") + .row("4", "Dave", "Analyst", "Berlin") + .row("5", "Eve", "Developer", "Tokyo") + .highlightStyle(Style.EMPTY.bg(Color.DARK_GRAY)) + .highlightSymbol(">> ") + .state(tableState) + .fill() + ).rounded().borderColor(Color.BLUE).fill(), + text("Navigate: Up/Down | Quit: q").dim() + ).fill(); + } + + @Override + protected boolean onKeyEvent(KeyEvent event, ToolkitRunner runner) { + if (event.isQuit()) { + runner.quit(); + return true; + } + if (event.isUp()) { + tableState.selectPrevious(); + return true; + } + if (event.isDown()) { + tableState.selectNext(5); + return true; + } + return false; + } + } + + /** + * Live sparkline chart with randomly generated data points. + * New data points are added on each tick, simulating a live feed. + */ + @CommandDefinition(name = "tui-sparkline", description = "Live sparkline chart") + public static class SparklineCommand extends TuiCommand { + + @Override + protected TuiConfig.Builder configure(TuiConfig.Builder builder) { + return builder.tickRate(Duration.ofMillis(200)); + } + + @Override + protected void runTui(TuiRunner runner, CommandInvocation invocation) throws Exception { + Random rng = new Random(); + List data = new ArrayList<>(); + for (int i = 0; i < 40; i++) { + data.add((long) rng.nextInt(100)); + } + + runner.run( + (event, r) -> { + if (event instanceof KeyEvent && ((KeyEvent) event).isQuit()) { + r.quit(); + return false; + } + if (event instanceof TickEvent) { + data.add((long) rng.nextInt(100)); + if (data.size() > 200) { + data.remove(0); + } + return true; + } + return false; + }, + frame -> { + long[] arr = new long[data.size()]; + for (int i = 0; i < data.size(); i++) { + arr[i] = data.get(i); + } + Sparkline sparkline = Sparkline.builder() + .data(arr) + .block(Block.builder().title("Live Data Feed").build()) + .style(Style.EMPTY.fg(Color.YELLOW)) + .build(); + + frame.renderWidget(sparkline, frame.area()); + } + ); + } + } + + /** + * Bar chart showing simulated server metrics. + */ + @CommandDefinition(name = "tui-barchart", description = "Server metrics bar chart") + public static class BarChartCommand extends TuiAppCommand { + + @Override + protected Element render() { + return column( + barChart() + .groups( + BarGroup.of("web-1", + bar(72, "CPU", Color.RED), + bar(45, "Mem", Color.GREEN), + bar(28, "IO", Color.BLUE) + ), + BarGroup.of("web-2", + bar(55, "CPU", Color.RED), + bar(68, "Mem", Color.GREEN), + bar(15, "IO", Color.BLUE) + ), + BarGroup.of("db-1", + bar(90, "CPU", Color.RED), + bar(82, "Mem", Color.GREEN), + bar(63, "IO", Color.BLUE) + ), + BarGroup.of("cache", + bar(20, "CPU", Color.RED), + bar(95, "Mem", Color.GREEN), + bar(5, "IO", Color.BLUE) + ) + ) + .barWidth(5) + .barGap(1) + .groupGap(3) + .max(100) + .title("Server Metrics (%)") + .rounded() + .borderColor(Color.MAGENTA) + .fill(), + text("Press 'q' to quit").dim() + ).fill(); + } + + private static Bar bar(long value, String label, Color color) { + return Bar.builder().value(value).label(label).style(Style.EMPTY.fg(color)).build(); + } + } + + /** + * Tabbed interface with different content per tab. + * Use left/right arrow keys to switch tabs. + */ + @CommandDefinition(name = "tui-tabs", description = "Tabbed interface") + public static class TabsCommand extends TuiAppCommand { + + private final TabsState tabsState = new TabsState(0); + + @Override + protected Element render() { + Element content; + int selected = tabsState.selected() != null ? tabsState.selected() : 0; + switch (selected) { + case 0: + content = column( + text("System Overview").bold(), + spacer(1), + row( + gauge(72).label("CPU: 72%").gaugeColor(Color.RED).title("CPU").rounded().fill(), + gauge(45).label("Mem: 45%").gaugeColor(Color.GREEN).title("Memory").rounded().fill() + ).fill(), + row( + gauge(28).label("Disk: 28%").gaugeColor(Color.BLUE).title("Disk").rounded().fill(), + gauge(12).label("Net: 12%").gaugeColor(Color.YELLOW).title("Network").rounded().fill() + ).fill() + ).fill(); + break; + case 1: + content = column( + text("Process List").bold(), + spacer(1), + list(" java - 12.3% CPU", " postgres - 8.1% CPU", + " nginx - 2.4% CPU", " redis - 1.8% CPU", + " node - 1.2% CPU", " cron - 0.1% CPU") + .highlightColor(Color.CYAN) + .title("Top Processes") + .rounded() + .fill() + ).fill(); + break; + case 2: + content = column( + text("Event Log").bold(), + spacer(1), + list("[INFO] Service started on port 8080", + "[INFO] Connected to database", + "[WARN] High memory usage detected", + "[INFO] Cache warmed up (1234 entries)", + "[ERROR] Connection timeout to upstream", + "[INFO] Retry succeeded", + "[INFO] Health check passed") + .title("Recent Events") + .rounded() + .borderColor(Color.YELLOW) + .displayOnly() + .fill() + ).fill(); + break; + default: + content = text("Unknown tab"); + break; + } + + return column( + tabs("Overview", "Processes", "Logs") + .state(tabsState) + .highlightColor(Color.CYAN) + .divider(" | ") + .rounded() + .borderColor(Color.WHITE), + panel(content).fill() + ).fill(); + } + + @Override + protected boolean onKeyEvent(KeyEvent event, ToolkitRunner runner) { + if (event.isQuit()) { + runner.quit(); + return true; + } + if (event.isLeft()) { + int current = tabsState.selected() != null ? tabsState.selected() : 0; + tabsState.select(Math.max(0, current - 1)); + return true; + } + if (event.isRight()) { + int current = tabsState.selected() != null ? tabsState.selected() : 0; + tabsState.select(Math.min(2, current + 1)); + return true; + } + return false; + } + } + + /** + * Calendar widget showing the current month with today highlighted. + * Use left/right arrows to navigate months. + */ + @CommandDefinition(name = "tui-calendar", description = "Calendar view") + public static class CalendarCommand extends TuiAppCommand { + + private final AtomicReference currentDate = + new AtomicReference<>(LocalDate.now()); + + @Override + protected Element render() { + LocalDate date = currentDate.get(); + return column( + calendar(date) + .showMonthHeader(Style.EMPTY.bold().fg(Color.CYAN)) + .showWeekdaysHeader(Style.EMPTY.fg(Color.YELLOW)) + .highlightToday(Color.GREEN) + .showSurrounding(Style.EMPTY.dim()) + .rounded() + .borderColor(Color.WHITE) + .fill(), + text("<< Left/Right to change month | 'q' to quit >>").dim() + ).fill(); + } + + @Override + protected boolean onKeyEvent(KeyEvent event, ToolkitRunner runner) { + if (event.isQuit()) { + runner.quit(); + return true; + } + if (event.isLeft()) { + currentDate.updateAndGet(d -> d.minusMonths(1)); + return true; + } + if (event.isRight()) { + currentDate.updateAndGet(d -> d.plusMonths(1)); + return true; + } + return false; + } + } + + /** + * Multi-panel dashboard combining gauges, sparkline, and bar chart. + * All data updates live on each tick. + */ + @CommandDefinition(name = "tui-dashboard", description = "Live system dashboard") + public static class DashboardCommand extends TuiCommand { + + @Override + protected TuiConfig.Builder configure(TuiConfig.Builder builder) { + return builder.tickRate(Duration.ofMillis(500)); + } + + @Override + protected void runTui(TuiRunner runner, CommandInvocation invocation) throws Exception { + Random rng = new Random(); + AtomicInteger cpu = new AtomicInteger(50); + AtomicInteger mem = new AtomicInteger(60); + AtomicInteger disk = new AtomicInteger(35); + List cpuHistory = new ArrayList<>(); + for (int i = 0; i < 60; i++) { + cpuHistory.add(50L); + } + + runner.run( + (event, r) -> { + if (event instanceof KeyEvent && ((KeyEvent) event).isQuit()) { + r.quit(); + return false; + } + if (event instanceof TickEvent) { + cpu.set(clamp(cpu.get() + rng.nextInt(11) - 5, 0, 100)); + mem.set(clamp(mem.get() + rng.nextInt(7) - 3, 0, 100)); + disk.set(clamp(disk.get() + rng.nextInt(3) - 1, 0, 100)); + cpuHistory.add((long) cpu.get()); + if (cpuHistory.size() > 120) { + cpuHistory.remove(0); + } + return true; + } + return false; + }, + frame -> { + dev.tamboui.layout.Rect area = frame.area(); + List rows = dev.tamboui.layout.Layout.vertical() + .constraints( + Constraint.length(3), + Constraint.fill(1), + Constraint.length(1) + ) + .split(area); + + // Top row: three gauges side by side + List gaugeCols = dev.tamboui.layout.Layout.horizontal() + .constraints( + Constraint.percentage(33), + Constraint.percentage(34), + Constraint.percentage(33) + ) + .split(rows.get(0)); + + Gauge cpuGauge = Gauge.builder() + .percent(cpu.get()) + .label("CPU " + cpu.get() + "%") + .gaugeColor(cpu.get() > 80 ? Color.RED : cpu.get() > 50 ? Color.YELLOW : Color.GREEN) + .build(); + Gauge memGauge = Gauge.builder() + .percent(mem.get()) + .label("MEM " + mem.get() + "%") + .gaugeColor(mem.get() > 80 ? Color.RED : mem.get() > 50 ? Color.YELLOW : Color.GREEN) + .build(); + Gauge diskGauge = Gauge.builder() + .percent(disk.get()) + .label("DISK " + disk.get() + "%") + .gaugeColor(disk.get() > 80 ? Color.RED : disk.get() > 50 ? Color.YELLOW : Color.GREEN) + .build(); + + frame.renderWidget(cpuGauge, gaugeCols.get(0)); + frame.renderWidget(memGauge, gaugeCols.get(1)); + frame.renderWidget(diskGauge, gaugeCols.get(2)); + + // Middle: CPU history sparkline + long[] histArr = new long[cpuHistory.size()]; + for (int i = 0; i < cpuHistory.size(); i++) { + histArr[i] = cpuHistory.get(i); + } + Sparkline spark = Sparkline.builder() + .data(histArr) + .max(100) + .block(Block.builder().title("CPU History").build()) + .style(Style.EMPTY.fg(Color.CYAN)) + .build(); + frame.renderWidget(spark, rows.get(1)); + + // Bottom: status line + dev.tamboui.widgets.paragraph.Paragraph status = + dev.tamboui.widgets.paragraph.Paragraph.builder() + .text(dev.tamboui.text.Text.from("Press 'q' to quit")) + .style(Style.EMPTY.dim()) + .build(); + frame.renderWidget(status, rows.get(2)); + } + ); + } + + private static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + } + + @SuppressWarnings("unchecked") + public static void main(String[] args) { + AeshConsoleRunner.builder() + .commands( + HelloCommand.class, + GaugeCommand.class, + TableCommand.class, + SparklineCommand.class, + BarChartCommand.class, + TabsCommand.class, + CalendarCommand.class, + DashboardCommand.class + ) + .addExitCommand() + .prompt("[tui-demo]$ ") + .start(); + } +} diff --git a/aesh/src/main/java/org/aesh/command/shell/Shell.java b/aesh/src/main/java/org/aesh/command/shell/Shell.java index a4517146..782e0d77 100644 --- a/aesh/src/main/java/org/aesh/command/shell/Shell.java +++ b/aesh/src/main/java/org/aesh/command/shell/Shell.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import org.aesh.readline.Prompt; +import org.aesh.terminal.Connection; import org.aesh.terminal.Key; import org.aesh.terminal.tty.Size; @@ -128,4 +129,15 @@ default String readLine(String prompt) throws InterruptedException { * Clear the terminal */ void clear(); + + /** + * Returns the underlying terminal connection. + * Useful for integrating with frameworks that need direct terminal access + * (e.g., TamboUI TUI framework). + * + * @return the Connection, or null if not available + */ + default Connection connection() { + return null; + } } diff --git a/aesh/src/main/java/org/aesh/console/ShellImpl.java b/aesh/src/main/java/org/aesh/console/ShellImpl.java index 8307dc1e..81be2b79 100644 --- a/aesh/src/main/java/org/aesh/console/ShellImpl.java +++ b/aesh/src/main/java/org/aesh/console/ShellImpl.java @@ -179,4 +179,9 @@ public Size size() { public void clear() { connection.put(Capability.clear_screen); } + + @Override + public Connection connection() { + return connection; + } } diff --git a/pom.xml b/pom.xml index d4f6c844..3d43f3c8 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,9 @@ aesh + + + @@ -51,6 +54,7 @@ 1.8 1.8 3.1 + 0.1.0 UTF-8 @@ -341,6 +345,12 @@ + + tamboui + + aesh-tamboui + +