This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26700-about in repository https://gitbox.apache.org/repos/asf/camel.git
commit 3c19bca9ee3ad16bc151ee6dc44e47621db5a455 Author: Claus Ibsen <[email protected]> AuthorDate: Sat Nov 7 15:58:03 2026 +0100 CAMEL-26700: About - zoom and move What happened, counts on the Out panel, green counts +/- zooms What happened between 5, 2 and 1 minutes (as the windows of the Timeline); zooming in centres on the last trouble, so the other routes at that moment show. Left/right move a zoomed window, with a time axis under the strips. The height of a column is the exchanges done, the redelivery attempts only colour it. The Out panel has how many times each route sent to the endpoint, from the endpoint statistics. A plain + can be sent over MCP (tui_send_keys). Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/AboutView.java | 168 +++++++++++++++++++-- .../dsl/jbang/core/commands/tui/McpFacade.java | 3 +- .../dsl/jbang/core/commands/tui/OverviewTab.java | 6 +- .../src/main/resources/tui/help/overview.md | 4 + .../dsl/jbang/core/commands/tui/AboutViewTest.java | 57 ++++++- .../commands/tui/CamelMonitorParseKeyTest.java | 12 ++ 6 files changed, 231 insertions(+), 19 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java index c4d7788cc236..5eeab4899e27 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java @@ -20,6 +20,9 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -37,6 +40,7 @@ import dev.tamboui.terminal.Frame; import dev.tamboui.text.Line; import dev.tamboui.text.Span; import dev.tamboui.text.Text; +import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.widgets.block.Block; import dev.tamboui.widgets.block.BorderType; import dev.tamboui.widgets.block.Borders; @@ -58,6 +62,9 @@ final class AboutView { private static final long RELOAD_MILLIS = 10_000; private static final int HAPPENED_SECONDS = 300; private static final String BLOCKS = " ▁▂▃▄▅▆▇█"; + // the strips have this many columns, whatever the zoom + private static final int STRIP_COLUMNS = 60; + private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault()); private final MonitorContext ctx; private volatile ProjectOverview.Overview overview; @@ -67,6 +74,10 @@ final class AboutView { private volatile String error; private Path dir; private long loadedAt; + // the zoom of What happened, as the windows of the Timeline tab (+ and -), and its last second when moved (← →); + // -1 follows the last second recorded + private int zoom = TimelineTab.WINDOWS.length - 1; + private long stripsEnd = -1; AboutView(MonitorContext ctx) { this.ctx = ctx; @@ -113,6 +124,77 @@ final class AboutView { t.start(); } + /** + * The keys of What happened: + zooms in on the last trouble in the window (a failure, a handled error, a + * redelivery, a route down), - zooms out, and ← → move a zoomed window. + */ + boolean handleKeyEvent(KeyEvent ke, IntegrationInfo info) { + if (info == null) { + return false; + } + long end = ctx.timeline.endSecond(info.pid); + if (ke.isChar('+') || ke.isChar('=')) { + if (zoom > 0 && end >= 0) { + long trouble = lastTrouble(info, windowEnd(end), seconds()); + zoom--; + if (trouble >= 0) { + // the trouble in the middle of the window, when there is time after it + moveTo(trouble + seconds() / 2, end); + } + } + return true; + } + if (ke.isChar('-')) { + if (zoom < TimelineTab.WINDOWS.length - 1) { + zoom++; + if (end >= 0) { + moveTo(windowEnd(end) + seconds() / 4, end); + } + } + return true; + } + if ((ke.isLeft() || ke.isRight()) && end >= 0) { + int by = Math.max(1, seconds() / 4); + moveTo(windowEnd(end) + (ke.isLeft() ? -by : by), end); + return true; + } + return false; + } + + private int seconds() { + return TimelineTab.WINDOWS[zoom]; + } + + private long windowEnd(long end) { + return stripsEnd >= 0 ? Math.min(stripsEnd, end) : end; + } + + /** Moves the window to end at the given second, as far as there are seconds recorded; at the end it follows. */ + private void moveTo(long second, long end) { + long first = Math.max(0, end - Timeline.SECONDS + 1); + long to = Math.max(first + seconds() - 1, second); + stripsEnd = to >= end ? -1 : to; + } + + /** The last second in the window that a route had trouble in, or -1. */ + private long lastTrouble(IntegrationInfo info, long windowEnd, int seconds) { + long last = -1; + for (Timeline.RouteSeries r : ctx.timeline.routes(info.pid, routeOrder(info), windowEnd, seconds)) { + for (int i = seconds - 1; i >= 0; i--) { + if (r.failed()[i] > 0 || r.handled()[i] > 0 || r.recovered()[i] > 0 || r.attempts()[i] > 0 + || r.down()[i] > 0) { + last = Math.max(last, windowEnd - seconds + 1 + i); + break; + } + } + } + return last; + } + + private static List<String> routeOrder(IntegrationInfo info) { + return info.routes.stream().map(r -> r.routeId).filter(id -> id != null).toList(); + } + void render(Frame frame, Rect area, IntegrationInfo info) { load(info, Runnable::run); ProjectOverview.Overview o = overview; @@ -156,7 +238,7 @@ final class AboutView { .split(area); if (happenedHeight > 2) { frame.renderWidget(Paragraph.builder().text(Text.from(routeStrips)) - .block(block(" What happened ─ last 5m, a column is 5s ─ 4 for each second ")).build(), rows.get(3)); + .block(block(happenedTitle(info))).build(), rows.get(3)); } frame.renderWidget(Paragraph.builder().text(Text.from(head)) .block(block(" " + (info.name != null ? info.name : info.pid) + " ")).build(), rows.get(0)); @@ -173,30 +255,49 @@ final class AboutView { * A line per route over the last 5 minutes, a column of 5 seconds coloured by the worst that happened to its * exchanges then (failed, handled, recovered, completed), as the Timeline tab colours its seconds. */ + private String happenedTitle(IntegrationInfo info) { + int seconds = seconds(); + String window = seconds % 60 == 0 ? seconds / 60 + "m" : seconds + "s"; + long end = ctx.timeline.endSecond(info.pid); + String when = stripsEnd >= 0 && end >= 0 + ? window + " to " + TIME.format(Instant.ofEpochSecond(windowEnd(end))) : "last " + window; + String keys = zoom == 0 ? "- zoom out" : zoom < TimelineTab.WINDOWS.length - 1 ? "+ zoom in, - out" : "+ zoom in"; + if (seconds < Timeline.SECONDS) { + keys += ", ← → move"; + } + return " What happened ─ " + when + ", a column is " + seconds / STRIP_COLUMNS + "s ─ " + keys + + ", 4 for each second "; + } + private List<Line> routeStrips(IntegrationInfo info, int width) { List<Line> lines = new ArrayList<>(); - long end = ctx.timeline.endSecond(info.pid); - if (end < 0) { + long last = ctx.timeline.endSecond(info.pid); + if (last < 0) { return lines; } - List<String> order = info.routes.stream().map(r -> r.routeId).filter(id -> id != null).toList(); + long end = windowEnd(last); + int seconds = seconds(); + List<String> order = routeOrder(info); // the names, then one space: every line starts its columns at the same place int label = Math.min(18, Math.max(8, order.stream().mapToInt(String::length).max().orElse(8))) + 4; - int per = 5; - int columns = HAPPENED_SECONDS / per; - for (Timeline.RouteSeries r : ctx.timeline.routes(info.pid, order, end, HAPPENED_SECONDS)) { + int per = seconds / STRIP_COLUMNS; + int columns = STRIP_COLUMNS; + for (Timeline.RouteSeries r : ctx.timeline.routes(info.pid, order, end, seconds)) { List<Span> spans = new ArrayList<>(); String name = r.route().length() > label - 3 ? r.route().substring(0, label - 4) + "…" : r.route(); spans.add(Span.raw(" " + name + " ".repeat(Math.max(1, label - 3 - name.length())))); long max = 1; - long[][] col = new long[columns][5]; - for (int i = 0; i < HAPPENED_SECONDS; i++) { + // completed, recovered, handled, failed, down, redelivery attempts: the height is the exchanges done, the + // attempts only colour the column (an exchange retried many times is still one exchange) + long[][] col = new long[columns][6]; + for (int i = 0; i < seconds; i++) { long[] c = col[i / per]; c[0] += Math.max(0, r.completed()[i]); - c[1] += Math.max(0, r.recovered()[i]) + Math.max(0, r.attempts()[i]); + c[1] += Math.max(0, r.recovered()[i]); c[2] += Math.max(0, r.handled()[i]); c[3] += Math.max(0, r.failed()[i]); c[4] += Math.max(0, r.down()[i]); + c[5] += Math.max(0, r.attempts()[i]); } for (long[] c : col) { max = Math.max(max, c[0] + c[1] + c[2] + c[3]); @@ -205,17 +306,18 @@ final class AboutView { long t = c[0] + c[1] + c[2] + c[3]; if (c[4] > 0) { spans.add(Span.styled("░", Theme.error())); - } else if (t == 0) { + } else if (t == 0 && c[5] == 0) { spans.add(Span.styled("·", Theme.muted())); } else { int level = (int) Math.max(1, Math.min(8, Math.ceil(t * 8.0 / max))); Style style = c[3] > 0 ? Theme.error() - : c[2] > 0 ? Style.EMPTY.fg(Theme.ACCENT) : c[1] > 0 ? Theme.warning() : Theme.success(); + : c[2] > 0 ? Style.EMPTY.fg(Theme.ACCENT) + : c[1] > 0 || c[5] > 0 ? Theme.warning() : Theme.success(); spans.add(Span.styled(String.valueOf(BLOCKS.charAt(level)), style)); } } long done = r.total(r.completed()) + r.total(r.recovered()) + r.total(r.handled()) + r.total(r.failed()); - spans.add(Span.raw(" " + done)); + spans.add(Span.styled(" " + done, Theme.success())); if (r.total(r.failed()) > 0) { spans.add(Span.styled(" " + r.total(r.failed()) + " failed", Theme.error())); } else if (r.total(r.handled()) > 0) { @@ -225,6 +327,12 @@ final class AboutView { } lines.add(Line.from(spans)); } + // the time axis: the first second of the window and the last + String from = TIME.format(Instant.ofEpochSecond(end - seconds + 1)); + String to = stripsEnd >= 0 ? TIME.format(Instant.ofEpochSecond(end)) : "now"; + lines.add(Line.from(Span.styled(" ".repeat(label) + from + + " ".repeat(Math.max(1, columns - from.length() - to.length())) + to, + Theme.muted()))); return lines; } @@ -308,8 +416,10 @@ final class AboutView { } String key = s.uri() + "|" + s.direction(); if (systems.add(key)) { + Long hits = outHits(info, s.uri(), s.route()); out.add(Line.from(Span.raw(" " + icon(s.category()) + " "), Span.styled(shortUri(s.uri()), Style.EMPTY.bold()), - Span.styled(" " + s.direction(), Theme.muted()))); + Span.styled(" " + s.direction(), Theme.muted()), + Span.styled(hits != null ? " " + hits : "", Theme.success()))); out.add(Line.from(Span.styled(" from " + s.route(), Theme.muted()))); } } @@ -318,6 +428,33 @@ final class AboutView { } } + /** + * How many times the route sent to the endpoint, from the endpoint statistics of the integration; null when the + * integration has no such endpoint (a URI with a placeholder, or no statistics). + */ + static Long outHits(IntegrationInfo info, String uri, String route) { + String base = baseUri(uri); + Long answer = null; + for (EndpointInfo ep : info.endpoints) { + if ("out".equals(ep.direction) && (route == null || route.equals(ep.routeId)) + && base.equals(baseUri(ep.uri))) { + answer = (answer != null ? answer : 0) + ep.hits; + } + } + return answer; + } + + /** The URI without its options, and the same with or without the // after the scheme. */ + private static String baseUri(String uri) { + if (uri == null) { + return ""; + } + int q = uri.indexOf('?'); + String answer = q >= 0 ? uri.substring(0, q) : uri; + answer = answer.replaceFirst("^([a-zA-Z0-9+.-]+):/{0,2}", "$1:"); + return answer.endsWith("/") ? answer.substring(0, answer.length() - 1) : answer; + } + private static void routeLines( ProjectOverview.Overview o, ProjectRoutes.Route r, int depth, String how, Map<String, List<ProjectOverview.Link>> calledBy, Map<String, Long> totals, Set<String> shown, @@ -434,7 +571,8 @@ final class AboutView { } spans.add(Span.styled(spark(perSecond, 30), Theme.info())); long done = completed + recovered + handled + failed; - spans.add(Span.raw(" " + done + (done == 1 ? " exchange" : " exchanges"))); + spans.add(Span.styled(" " + done, Theme.success())); + spans.add(Span.raw(done == 1 ? " exchange" : " exchanges")); if (recovered > 0) { spans.add(Span.styled(" · " + recovered + " recovered", Theme.warning())); } 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 index fdaebe1aa6a8..2486b66e6d78 100644 --- 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 @@ -678,7 +678,8 @@ class McpFacade { boolean ctrl = false; boolean shift = false; String remainder = key; - while (remainder.contains("+")) { + // a + on its own (or last, as in Shift++) is the key itself, not the + between a modifier and a key + while (remainder.length() > 1 && remainder.indexOf('+') >= 0 && remainder.indexOf('+') < remainder.length() - 1) { int plus = remainder.indexOf('+'); String mod = remainder.substring(0, plus).trim(); remainder = remainder.substring(plus + 1).trim(); 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 92ee2fdb9cb3..b84a8cede24c 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 @@ -205,8 +205,8 @@ class OverviewTab extends AbstractTab { return true; } if (showsAbout()) { - // About has no table: the keys of the table do not apply - return false; + // About has no table: the keys of the table do not apply, only the zoom of What happened + return about.handleKeyEvent(ke, aboutIntegration()); } if (ke.isChar('s')) { if (infraFocused) { @@ -1315,6 +1315,8 @@ class OverviewTab extends AbstractTab { hint(spans, "q", "quit"); if (showsAbout()) { // the keys of the table do not apply to About + hint(spans, "+/-", "zoom"); + hint(spans, "←→", "move"); hint(spans, "4", "timeline"); hint(spans, "5", "diagram"); hint(spans, "v", "integrations"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md index 0da66155ea0d..b844355ac634 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md @@ -17,6 +17,10 @@ The Overview has two views, switched with **v**: it records (the exchanges, how they ended, the error that happened most), what to **Watch**, and where to go **Next**: **4** the Timeline second by second, **5** the Diagram, **2** the source. + **What happened** has a line per route, coloured as the Timeline: + **+/-** zooms between 5, 2 and 1 minutes (zooming in centres on the + last trouble, so you see what the other routes did at that moment) + and **←→** moves a zoomed window back and forward. - **Integrations** — the table of all running integrations below, shown when several run. diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java index 044cebd06308..7dcf5d658814 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java @@ -18,6 +18,9 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -25,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference; import dev.tamboui.buffer.Buffer; import dev.tamboui.layout.Rect; import dev.tamboui.terminal.Frame; +import dev.tamboui.tui.event.KeyEvent; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -87,6 +91,55 @@ class AboutViewTest { assertThat(AboutView.spark(new long[] { 0, 0, 2, 2, 0, 8 }, 3)).isEqualTo(" ▄█"); } + @Test + void zoomingInGoesToTheTrouble() { + IntegrationInfo info = new IntegrationInfo(); + info.pid = "1234"; + RouteInfo r = new RouteInfo(); + r.routeId = "orders"; + info.routes.add(r); + MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); + long end = T0 / 1000 + 299; + // the outage was 4.5 minutes ago, for 15 seconds + ctx.timeline.merge("1234", TimelineTabRenderTest.outage(end, 300, 0).json(), true); + AboutView about = new AboutView(ctx); + assertThat(render(about, info)).contains("What happened ─ last 5m, a column is 5s ─ + zoom in, 4 for each"); + + assertThat(about.handleKeyEvent(KeyEvent.ofChar('+'), info)).isTrue(); + // 2 minutes from the first second recorded, with the outage in it + String to = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochSecond(end - 180)); + assertThat(render(about, info)).contains("What happened ─ 2m to " + to + ", a column is 2s") + .contains("+ zoom in, - out, ← → move").contains(to).contains("30 handled"); + + assertThat(about.handleKeyEvent(KeyEvent.ofChar('+'), info)).isTrue(); + assertThat(render(about, info)).contains("a column is 1s"); + assertThat(about.handleKeyEvent(KeyEvent.ofChar('-'), info)).isTrue(); + assertThat(about.handleKeyEvent(KeyEvent.ofChar('-'), info)).isTrue(); + assertThat(render(about, info)).contains("What happened ─ last 5m").contains("now"); + assertThat(about.handleKeyEvent(KeyEvent.ofChar('x'), info)).isFalse(); + } + + @Test + void theSendsOfARouteToAnEndpoint() { + IntegrationInfo info = new IntegrationInfo(); + info.endpoints.add(endpoint("http://localhost:8765/ship?bridgeEndpoint=true", "out", "orders", 7)); + info.endpoints.add(endpoint("http://localhost:8765/ship", "out", "returns", 2)); + info.endpoints.add(endpoint("http://localhost:8765/ship", "in", "orders", 99)); + assertThat(AboutView.outHits(info, "http:localhost:8765/ship", "orders")).isEqualTo(7); + assertThat(AboutView.outHits(info, "http://localhost:8765/ship/", "returns")).isEqualTo(2); + assertThat(AboutView.outHits(info, "{{ship.url}}", "orders")).isNull(); + } + + private static EndpointInfo endpoint(String uri, String direction, String routeId, long hits) { + EndpointInfo ep = new EndpointInfo(); + ep.uri = uri; + ep.direction = direction; + ep.routeId = routeId; + ep.hits = hits; + return ep; + } + @Test void aboutAnIntegrationWithWhatHasHappened() throws Exception { Files.writeString(dir.resolve("shop.camel.yaml"), """ @@ -119,6 +172,7 @@ class AboutViewTest { r.total = 120; info.routes.add(r); } + info.endpoints.add(endpoint("http://localhost:8765/ship", "out", "orders", 118)); MonitorContext ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())); ctx.timeline.merge("1234", TimelineTabRenderTest.outage(T0 / 1000 + 59, 60, 0).json(), true); AboutView about = new AboutView(ctx); @@ -128,7 +182,8 @@ class AboutViewTest { assertThat(screen).contains("Every order is checked against the stock and shipped.") .contains("from the comment at the top of shop.camel.yaml"); assertThat(screen).contains(" In ").contains("timer:orders").contains(" Routes ").contains("orders") - .contains("↳ stock").contains(" Out ").contains("localhost:8765/ship").contains("from orders"); + .contains("↳ stock").contains(" Out ").contains("localhost:8765/ship").contains("118") + .contains("from orders"); assertThat(screen).contains("Last 5m").contains("30 handled") .contains("HttpOperationFailedException 503 ×30").contains("Watch").contains("Next"); assertThat(screen).contains("What happened").contains("30 handled"); 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 cade44fb0baf..0588f7d4d0de 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 @@ -35,6 +35,18 @@ class CamelMonitorParseKeyTest { assertEquals('a', ke.character()); } + @Test + void parseKeyPlus() { + KeyEvent ke = McpFacade.parseKey("+"); + assertNotNull(ke); + assertEquals(KeyCode.CHAR, ke.code()); + assertEquals('+', ke.character()); + ke = McpFacade.parseKey("Ctrl++"); + assertNotNull(ke); + assertEquals('+', ke.character()); + assertTrue(ke.hasCtrl()); + } + @Test void parseKeyEnter() { KeyEvent ke = McpFacade.parseKey("enter");
