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
+
+