Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ public class CamelMonitor extends CamelCommand {
private final Queue<PendingKey> pendingKeys = new ConcurrentLinkedQueue<>();
private final List<KeyRecord> recentKeys = new ArrayList<>();
private final CaptionOverlay captionOverlay = new CaptionOverlay();
private final DrawOverlay drawOverlay = new DrawOverlay();
private final HelpOverlay helpOverlay = new HelpOverlay();

private final ActionsPopup actionsPopup = new ActionsPopup(
Expand Down Expand Up @@ -781,6 +782,7 @@ private boolean handleEvent(Event event, TuiRunner runner) {
return true;
}
actionsPopup.tick(now);
drawOverlay.tick(now);
captionOverlay.tick(now);
if (recording && !recentKeys.isEmpty()) {
long cutoff = now - 2000;
Expand Down Expand Up @@ -956,6 +958,9 @@ private void render(Frame frame) {
renderTabs(frame, mainChunks.get(2));
// mainChunks.get(3) is the empty spacer row between tabs and content
renderContent(frame, mainChunks.get(4));
if (drawOverlay.isVisible()) {
drawOverlay.render(frame, mainChunks.get(4));
}
if (showKillConfirm) {
renderKillConfirm(frame, mainChunks.get(4));
}
Expand Down Expand Up @@ -2675,6 +2680,22 @@ void showCaption(String text, int durationSeconds) {
captionOverlay.showCaption(text, durationSeconds);
}

boolean isDrawVisible() {
return drawOverlay.isVisible();
}

void setDrawing(List<DrawOverlay.DrawCell> cells, int durationSeconds) {
drawOverlay.setDrawing(cells, durationSeconds);
}

void appendDrawing(List<DrawOverlay.DrawCell> cells) {
drawOverlay.appendDrawing(cells);
}

void clearDrawing() {
drawOverlay.clear();
}

String navigateToTab(String tabName) {
for (int i = 0; i < TAB_NAMES.length; i++) {
if (TAB_NAMES[i].equalsIgnoreCase(tabName)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.dsl.jbang.core.commands.tui;

import java.util.ArrayList;
import java.util.List;

import dev.tamboui.buffer.Buffer;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Color;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;

class DrawOverlay {

record DrawCell(int x, int y, String symbol, Style style) {
}

private List<DrawCell> cells;
private long autoDismissTime;

boolean isVisible() {
return cells != null && !cells.isEmpty();
}

void setDrawing(List<DrawCell> newCells, int durationSeconds) {
this.cells = new ArrayList<>(newCells);
if (durationSeconds > 0) {
this.autoDismissTime = System.currentTimeMillis() + (durationSeconds * 1000L);
} else {
this.autoDismissTime = 0;
}
}

void appendDrawing(List<DrawCell> newCells) {
if (this.cells == null) {
this.cells = new ArrayList<>(newCells);
} else {
this.cells.addAll(newCells);
}
}

void clear() {
cells = null;
autoDismissTime = 0;
}

void tick(long now) {
if (autoDismissTime > 0 && now > autoDismissTime) {
clear();
}
}

void render(Frame frame, Rect area) {
if (cells == null || cells.isEmpty()) {
return;
}
Buffer buffer = frame.buffer();
Rect screenArea = buffer.area();
for (DrawCell cell : cells) {
if (cell.x >= 0 && cell.y >= 0
&& cell.x < screenArea.width() && cell.y < screenArea.height()) {
buffer.setString(cell.x, cell.y, cell.symbol, cell.style);
}
}
}

static Color parseColor(String name) {
if (name == null || name.isBlank()) {
return null;
}
return switch (name.toLowerCase().trim()) {
case "red" -> Color.RED;
case "green" -> Color.GREEN;
case "blue" -> Color.BLUE;
case "yellow" -> Color.YELLOW;
case "cyan" -> Color.CYAN;
case "magenta" -> Color.MAGENTA;
case "white" -> Color.WHITE;
case "gray", "grey" -> Color.GRAY;
case "dark_gray", "dark_grey", "darkgray", "darkgrey" -> Color.DARK_GRAY;
case "light_red", "lightred" -> Color.LIGHT_RED;
case "light_green", "lightgreen" -> Color.LIGHT_GREEN;
case "light_blue", "lightblue" -> Color.LIGHT_BLUE;
case "light_yellow", "lightyellow" -> Color.LIGHT_YELLOW;
case "light_cyan", "lightcyan" -> Color.LIGHT_CYAN;
case "light_magenta", "lightmagenta" -> Color.LIGHT_MAGENTA;
case "black" -> Color.BLACK;
default -> null;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,34 @@ private JsonObject handleToolsList() {
Map.of("seconds", propDef("integer",
"Number of seconds to sleep (1-30)")),
List.of("seconds")));
toolList.add(toolDef(
"tui_draw",
"Draws characters at specific screen coordinates as an overlay on top of the TUI. "
+ "Use this to highlight areas, annotate the screen for the human, "
+ "draw shapes, or create fun emoji art. "
+ "All cells are sent in a single call to avoid chatty networking. "
+ "Coordinates are 0-based and match the screen grid from tui_get_screen. "
+ "Characters can be any unicode including emoji. "
+ "The drawing overlays on top of existing content without modifying it. "
+ "Use with tui_show_caption to explain what you drew.",
Map.of("cells", propDef("array",
"Array of cell objects to draw. Each cell has: "
+ "x (integer, column), y (integer, row), "
+ "char (string, character to draw), "
+ "fg (string, optional foreground color: red/green/blue/yellow/cyan/magenta/white/gray/black), "
+ "bg (string, optional background color, same values), "
+ "bold (boolean, optional)"),
"duration", propDef("integer",
"Auto-dismiss drawing after this many seconds. "
+ "If omitted, drawing stays until cleared with tui_draw_clear or replaced by another tui_draw call."),
"append", propDef("boolean",
"If true, add cells to the existing drawing instead of replacing it. Default false.")),
List.of("cells")));
toolList.add(toolDef(
"tui_draw_clear",
"Clears the drawing overlay and restores the screen to its normal state. "
+ "The underlying content is unchanged since drawing is an overlay.",
Map.of()));

JsonObject result = new JsonObject();
result.put("tools", toolList);
Expand Down Expand Up @@ -350,6 +378,8 @@ private JsonObject handleToolsCall(JsonObject request) {
case "tui_tape_start" -> callTapeStart(args);
case "tui_tape_stop" -> callTapeStop(args);
case "tui_sleep" -> callSleep(args);
case "tui_draw" -> callDraw(args);
case "tui_draw_clear" -> callDrawClear();
default -> {
isError = true;
yield "Unknown tool: " + toolName;
Expand Down Expand Up @@ -745,6 +775,75 @@ private String callSleep(Map<String, Object> args) {
return "Slept for " + seconds + "s";
}

@SuppressWarnings("unchecked")
private String callDraw(Map<String, Object> args) {
Object cellsArg = args.get("cells");
if (!(cellsArg instanceof List)) {
return "Error: cells must be an array";
}
List<Object> cellsList = (List<Object>) cellsArg;
if (cellsList.isEmpty()) {
return "Error: cells array is empty";
}

List<DrawOverlay.DrawCell> drawCells = new ArrayList<>();
for (Object item : cellsList) {
if (!(item instanceof Map)) {
continue;
}
Map<String, Object> cell = (Map<String, Object>) item;

int x = cell.get("x") instanceof Number n ? n.intValue() : -1;
int y = cell.get("y") instanceof Number n ? n.intValue() : -1;
String ch = cell.get("char") instanceof String s ? s : " ";
if (x < 0 || y < 0) {
continue;
}

dev.tamboui.style.Style style = dev.tamboui.style.Style.EMPTY;
dev.tamboui.style.Color fg = DrawOverlay.parseColor(
cell.get("fg") instanceof String s ? s : null);
dev.tamboui.style.Color bg = DrawOverlay.parseColor(
cell.get("bg") instanceof String s ? s : null);
if (fg != null) {
style = style.fg(fg);
}
if (bg != null) {
style = style.bg(bg);
}
if (Boolean.TRUE.equals(cell.get("bold"))) {
style = style.bold();
}

drawCells.add(new DrawOverlay.DrawCell(x, y, ch, style));
}

if (drawCells.isEmpty()) {
return "Error: no valid cells in array";
}

boolean append = Boolean.TRUE.equals(args.get("append"));
int duration = 0;
if (args.get("duration") instanceof Number n) {
duration = n.intValue();
}

if (append) {
monitor.appendDrawing(drawCells);
} else {
monitor.setDrawing(drawCells, duration);
}

return "Drawing " + drawCells.size() + " cell(s)"
+ (append ? " (appended)" : "")
+ (duration > 0 ? ", auto-dismiss in " + duration + "s" : "");
}

private String callDrawClear() {
monitor.clearDrawing();
return "Drawing cleared";
}

private static JsonArray toJsonArray(List<String> list) {
JsonArray arr = new JsonArray();
arr.addAll(list);
Expand Down