This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch worktree-tui8 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 29ead53d5a1630cae1c20beef74c762ec58268d3 Author: Claus Ibsen <[email protected]> AuthorDate: Thu May 21 15:07:19 2026 +0200 CAMEL-23572: camel-tui: Add documentation viewer with MarkdownView - Add "Show Documentation" to F2 actions menu for viewing README files from running integrations via the readme action - Add `d` key in example browser to view bundled or online README docs - Use TamboUI MarkdownView widget for rendering with scroll support - Convert bundled example READMEs from AsciiDoc to Markdown - Include AsciiDoc-to-Markdown converter for online .adoc fallback - Esc from doc viewer returns to example browser when opened from there Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../examples/camel-jbang-example-catalog.json | 42 +-- .../resources/examples/circuit-breaker/README.adoc | 61 ---- .../resources/examples/circuit-breaker/README.md | 24 ++ .../examples/cron-log/{README.adoc => README.md} | 4 +- .../src/main/resources/examples/groovy/README.adoc | 76 ---- .../src/main/resources/examples/groovy/README.md | 39 +++ .../examples/rest-api/{README.adoc => README.md} | 6 +- .../src/main/resources/examples/routes/README.adoc | 78 ----- .../src/main/resources/examples/routes/README.md | 41 +++ .../examples/timer-log/{README.adoc => README.md} | 4 +- .../src/main/resources/examples/xslt/README.adoc | 66 ---- .../src/main/resources/examples/xslt/README.md | 31 ++ dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml | 5 + .../dsl/jbang/core/commands/tui/ActionsPopup.java | 386 ++++++++++++++++++++- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 5 + .../jbang/core/commands/tui/IntegrationInfo.java | 1 + 16 files changed, 550 insertions(+), 319 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json index a45a48e1027b..a9b467615531 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json @@ -13,7 +13,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "consumer.camel.yaml", "producer.camel.yaml" @@ -35,7 +35,7 @@ "requiresDocker": false, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "application.properties", "aws-s3-cdc-log.camel.yaml", "example-file.txt", @@ -58,7 +58,7 @@ "requiresDocker": false, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "application.properties", "http-to-aws-sqs.camel.yaml" ] @@ -76,7 +76,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "route.camel.yaml" ] }, @@ -95,7 +95,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "cron-log.camel.yaml" ] }, @@ -114,7 +114,7 @@ "requiresDocker": true, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "compose.yaml", "docling-langchain4j-rag.yaml", @@ -136,7 +136,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "examples/banking-sector-brief.pdf", "examples/magnificent-seven-update.pdf", @@ -159,7 +159,7 @@ "requiresDocker": true, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "application.properties", "compose.yaml", "ftp.camel.yaml", @@ -180,7 +180,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "groovy.camel.yaml" ] @@ -200,7 +200,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "rest-api.camel.yaml" ] @@ -220,7 +220,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "rest-api.camel.yaml" ] @@ -239,7 +239,7 @@ "requiresDocker": true, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "application.properties", "compose.yaml", "infra/mosquitto.conf", @@ -262,7 +262,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "pii-redaction.camel.yaml", "pii.schema.json" @@ -282,7 +282,7 @@ "requiresDocker": false, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "application.properties", "examples/1001.json", "petstore-api.json", @@ -303,7 +303,7 @@ "requiresDocker": false, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "application.properties", "examples/pet/1000.json", "petstore-api.json", @@ -324,7 +324,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "rest-api.camel.yaml" ] }, @@ -343,7 +343,7 @@ "hasCitrusTests": false, "files": [ "Greeter.java", - "README.adoc", + "README.md", "beans.yaml", "routes.camel.yaml" ] @@ -363,7 +363,7 @@ "requiresDocker": false, "hasCitrusTests": true, "files": [ - "README.adoc", + "README.md", "analyzer/application-dev.properties", "analyzer/error-analyzer.camel.yaml", "containers/caches/infinispan-events-config.json", @@ -410,7 +410,7 @@ "requiresDocker": true, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "application.properties", "compose.yaml", "sql.camel.yaml" @@ -430,7 +430,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "timer-log.camel.yaml" ] }, @@ -448,7 +448,7 @@ "requiresDocker": false, "hasCitrusTests": false, "files": [ - "README.adoc", + "README.md", "consumer.camel.yaml", "input/account.xml", "stylesheet.xsl" diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.adoc deleted file mode 100644 index cca5a275abf1..000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.adoc +++ /dev/null @@ -1,61 +0,0 @@ -== Circuit Breaker - -This example shows how Camel JBang can use circuit breaker EIP. - -=== Install JBang - -First install JBang according to https://www.jbang.dev - -When JBang is installed then you should be able to run from a shell: - -[source,sh] ----- -$ jbang --version ----- - -This will output the version of JBang. - -To run this example you can either install Camel on JBang via: - -[source,sh] ----- -$ jbang app install camel@apache/camel ----- - -Which allows to run Camel JBang with `camel` as shown below. - -=== How to run - -You can run this example using: - -[source,sh] ----- -$ camel run * ----- - -While the Camel integration is running, then from another terminal type: - -[source,sh] ----- -$ camel get circuit-breaker ----- - -Which then output the state of the circuit breaker. You can run this command with `--watch` and see -how the state of the circuit breaker changes from closed to open due to many failures. - -[source,sh] ----- -$ camel get circuit-breaker --watch ----- - - - -=== Help and contributions - -If you hit any problem using Camel or have some feedback, then please -https://camel.apache.org/community/support/[let us know]. - -We also love contributors, so -https://camel.apache.org/community/contributing/[get involved] :-) - -The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md new file mode 100644 index 000000000000..38a0209d9641 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md @@ -0,0 +1,24 @@ +# Circuit Breaker + +This example shows how Camel JBang can use circuit breaker EIP. + +## How to run + +You can run this example using: + +```sh +camel run * +``` + +While the Camel integration is running, then from another terminal type: + +```sh +camel get circuit-breaker +``` + +Which then output the state of the circuit breaker. You can run this command with `--watch` and see +how the state of the circuit breaker changes from closed to open due to many failures. + +```sh +camel get circuit-breaker --watch +``` diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.md similarity index 81% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.adoc rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.md index 7a5721140db9..cd926f1f9b8f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.adoc +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.md @@ -1,7 +1,7 @@ -== Cron Log +# Cron Log This example shows a scheduled task that logs the current time every 5 seconds. -=== How to run +## How to run camel run cron-log.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.adoc deleted file mode 100644 index d6910f56215e..000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.adoc +++ /dev/null @@ -1,76 +0,0 @@ -== Groovy - -This example shows how to use Groovy with extra dependencies in Camel JBang. - -The route uses `EmailValidator` from https://commons.apache.org/proper/commons-validator/[Apache Commons Validator] -to validate an email address and route the message accordingly using content-based routing. - -The extra dependency is declared in `application.properties` using the -`camel.jbang.dependencies` property: - -[source,properties] ----- -camel.jbang.dependencies=commons-validator:commons-validator:1.10.1 ----- - -=== Install JBang - -First install JBang according to https://www.jbang.dev - -When JBang is installed then you should be able to run from a shell: - -[source,sh] ----- -$ jbang --version ----- - -This will output the version of JBang. - -To run this example you can either install Camel on JBang via: - -[source,sh] ----- -$ jbang app install camel@apache/camel ----- - -Which allows to run Camel JBang with `camel` as shown below. - -=== How to run - -You can run this example using: - -[source,sh] ----- -$ camel run * ----- - -To see the invalid email branch, edit `groovy.camel.yaml` and change the `contactEmail` header in the `once` URI to an invalid value: - -[source,yaml] ----- -uri: once:validate?header.contactEmail=not-a-valid-email ----- - -You can also declare dependencies as a modeline comment at the top of the YAML route file: - -[source,yaml] ----- -#//DEPS commons-validator:commons-validator:1.10.1 ----- - -Or pass the dependency on the command line: - -[source,sh] ----- -$ camel run groovy.camel.yaml --dep=commons-validator:commons-validator:1.10.1 ----- - -=== Help and contributions - -If you hit any problem using Camel or have some feedback, then please -https://camel.apache.org/community/support/[let us know]. - -We also love contributors, so -https://camel.apache.org/community/contributing/[get involved] :-) - -The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.md new file mode 100644 index 000000000000..46dcc024a0bb --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.md @@ -0,0 +1,39 @@ +# Groovy + +This example shows how to use Groovy with extra dependencies in Camel JBang. + +The route uses `EmailValidator` from [Apache Commons Validator](https://commons.apache.org/proper/commons-validator/) +to validate an email address and route the message accordingly using content-based routing. + +The extra dependency is declared in `application.properties` using the +`camel.jbang.dependencies` property: + +```properties +camel.jbang.dependencies=commons-validator:commons-validator:1.10.1 +``` + +## How to run + +You can run this example using: + +```sh +camel run * +``` + +To see the invalid email branch, edit `groovy.camel.yaml` and change the `contactEmail` header in the `once` URI to an invalid value: + +```yaml +uri: once:validate?header.contactEmail=not-a-valid-email +``` + +You can also declare dependencies as a modeline comment at the top of the YAML route file: + +```yaml +#//DEPS commons-validator:commons-validator:1.10.1 +``` + +Or pass the dependency on the command line: + +```sh +camel run groovy.camel.yaml --dep=commons-validator:commons-validator:1.10.1 +``` diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.md similarity index 82% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.adoc rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.md index c68c2872843b..9ade804eab16 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.adoc +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.md @@ -1,12 +1,12 @@ -== REST API +# REST API This example shows a REST API with hello endpoints. -=== How to run +## How to run camel run rest-api.camel.yaml -=== Try it +## Try it curl http://localhost:8080/api/hello curl http://localhost:8080/api/hello/World diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.adoc deleted file mode 100644 index 5cf864ca5b61..000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.adoc +++ /dev/null @@ -1,78 +0,0 @@ -== Routes - -This example shows how routes are defined in Yaml. - -=== Install JBang - -First install JBang according to https://www.jbang.dev - -When JBang is installed then you should be able to run from a shell: - -[source,sh] ----- -$ jbang --version ----- - -This will output the version of JBang. - -To run this example you can either install Camel on JBang via: - -[source,sh] ----- -$ jbang app install camel@apache/camel ----- - -Which allows to run Camel JBang with `camel` as shown below. - -=== How to run - -You can run this example using: - -[source,sh] ----- -$ camel run * ----- - -Camel will start a route that periodically provides a greeting message. - -=== Live reload - -You can run the example in dev mode which allows you to edit the example, -and hot-reload when the file is saved. - -[source,sh] ----- -$ camel run * --dev ----- - -=== Run directly from GitHub - -The example can also be run directly by referring to the GitHub URL as shown: - -[source,sh] ----- -$ camel run https://github.com/apache/camel-jbang-examples/tree/main/routes ----- - -=== Developer Web Console - -You can enable the developer console via `--console` flag as show: - -[source,sh] ----- -$ camel run * --console ----- - -Then you can browse: http://localhost:8080/q/dev to introspect the running Camel Application. -Under "beans" Camel should display bean `greeter`. - - -=== Help and contributions - -If you hit any problem using Camel or have some feedback, then please -https://camel.apache.org/community/support/[let us know]. - -We also love contributors, so -https://camel.apache.org/community/contributing/[get involved] :-) - -The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.md new file mode 100644 index 000000000000..7dc86fc5a31c --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.md @@ -0,0 +1,41 @@ +# Routes + +This example shows how routes are defined in Yaml. + +## How to run + +You can run this example using: + +```sh +camel run * +``` + +Camel will start a route that periodically provides a greeting message. + +## Live reload + +You can run the example in dev mode which allows you to edit the example, +and hot-reload when the file is saved. + +```sh +camel run * --dev +``` + +## Run directly from GitHub + +The example can also be run directly by referring to the GitHub URL as shown: + +```sh +camel run https://github.com/apache/camel-jbang-examples/tree/main/routes +``` + +## Developer Web Console + +You can enable the developer console via `--console` flag as show: + +```sh +camel run * --console +``` + +Then you can browse: http://localhost:8080/q/dev to introspect the running Camel Application. +Under "beans" Camel should display bean `greeter`. diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.md similarity index 80% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.adoc rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.md index 3abb3a0f9b31..fd5b8ac6199f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.adoc +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.md @@ -1,7 +1,7 @@ -== Timer Log +# Timer Log This example shows a simple timer that logs a hello message every second. -=== How to run +## How to run camel run timer-log.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.adoc b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.adoc deleted file mode 100644 index 0d3cc77abf8e..000000000000 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.adoc +++ /dev/null @@ -1,66 +0,0 @@ -== XSLT Transformation - -This example shows a basic XML transformation using XSLT style sheet. - -=== Install JBang - -First install JBang according to https://www.jbang.dev - -When JBang is installed then you should be able to run from a shell: - -[source,sh] ----- -$ jbang --version ----- - -This will output the version of JBang. - -To run this example you can either install Camel on JBang via: - -[source,sh] ----- -$ jbang app install camel@apache/camel ----- - -Which allows to run Camel JBang with `camel` as shown below. - -=== How to run - -Then you can run this example using: - -[source,sh] ----- -$ camel run * ----- - -This reads the XML input file from _./input/account.xml_ and applies XSL transformation. - -=== Live updates of message transformation - -You can do live changes to the stylesheet and see the output in real-time with Camel JBang by running: - -[source,bash] ----- -$ camel transform message --body=file:input/account.xml --component=xslt --template=file:stylesheet.xsl --pretty --watch ----- - -You can then edit the `stylesheet.xsl` file, and save the file, and watch the terminal for updated result. - -=== Run directly from GitHub - -The example can also be run directly by referring to the GitHub URL as shown: - -[source,sh] ----- -$ camel run https://github.com/apache/camel-jbang-examples/tree/main/xslt ----- - -=== Help and contributions - -If you hit any problem using Camel or have some feedback, then please -https://camel.apache.org/community/support/[let us know]. - -We also love contributors, so -https://camel.apache.org/community/contributing/[get involved] :-) - -The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.md new file mode 100644 index 000000000000..ec989f624348 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.md @@ -0,0 +1,31 @@ +# XSLT Transformation + +This example shows a basic XML transformation using XSLT style sheet. + +## How to run + +You can run this example using: + +```sh +camel run * +``` + +This reads the XML input file from `./input/account.xml` and applies XSL transformation. + +## Live updates of message transformation + +You can do live changes to the stylesheet and see the output in real-time with Camel JBang by running: + +```sh +camel transform message --body=file:input/account.xml --component=xslt --template=file:stylesheet.xsl --pretty --watch +``` + +You can then edit the `stylesheet.xsl` file, and save the file, and watch the terminal for updated result. + +## Run directly from GitHub + +The example can also be run directly by referring to the GitHub URL as shown: + +```sh +camel run https://github.com/apache/camel-jbang-examples/tree/main/xslt +``` diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml index 64f490af9b89..4a3745e64f3b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml @@ -73,6 +73,11 @@ <artifactId>tamboui-image</artifactId> <version>${tamboui-version}</version> </dependency> + <dependency> + <groupId>dev.tamboui</groupId> + <artifactId>tamboui-markdown</artifactId> + <version>${tamboui-version}</version> + </dependency> </dependencies> </project> diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java index 964db24c4712..5a1958921028 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java @@ -17,6 +17,9 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -24,8 +27,10 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Collectors; import dev.tamboui.layout.Rect; +import dev.tamboui.markdown.MarkdownView; import dev.tamboui.style.Color; import dev.tamboui.style.Style; import dev.tamboui.terminal.Frame; @@ -47,6 +52,7 @@ import dev.tamboui.widgets.list.ScrollMode; import dev.tamboui.widgets.paragraph.Paragraph; import org.apache.camel.dsl.jbang.core.common.ExampleHelper; import org.apache.camel.dsl.jbang.core.common.LauncherHelper; +import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.util.json.JsonObject; import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint; @@ -55,11 +61,14 @@ import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLa class ActionsPopup { private static final int ACTION_RUN_EXAMPLE = 0; - private static final int ACTION_SCREENSHOT = 1; - private static final int ACTION_COUNT = 2; + private static final int ACTION_SHOW_DOCS = 1; + private static final int ACTION_SCREENSHOT = 2; + private static final int ACTION_COUNT = 3; private final Supplier<Set<String>> runningNames; + private final Supplier<List<IntegrationInfo>> integrations; private final Runnable screenshotAction; + private MonitorContext ctx; private boolean showActionsMenu; private final ListState actionsMenuState = new ListState(); @@ -72,18 +81,33 @@ class ActionsPopup { private TextInputState nameInputState; private JsonObject selectedExample; + private boolean showDocPicker; + private final ListState docPickerState = new ListState(); + private List<IntegrationInfo> docPickerIntegrations; + private boolean showDocViewer; + private boolean docViewerFromExampleBrowser; + private String docContent; + private String docTitle; + private int docScroll; + private final List<PendingLaunch> pendingLaunches = new ArrayList<>(); private String launchNotification; private boolean launchNotificationError; private long launchNotificationExpiry; - ActionsPopup(Supplier<Set<String>> runningNames, Runnable screenshotAction) { + ActionsPopup(Supplier<Set<String>> runningNames, Supplier<List<IntegrationInfo>> integrations, + Runnable screenshotAction) { this.runningNames = runningNames; + this.integrations = integrations; this.screenshotAction = screenshotAction; } + void setContext(MonitorContext ctx) { + this.ctx = ctx; + } + boolean isVisible() { - return showActionsMenu || showExampleBrowser || showNameInput; + return showActionsMenu || showExampleBrowser || showNameInput || showDocPicker || showDocViewer; } void open() { @@ -95,6 +119,8 @@ class ActionsPopup { showActionsMenu = false; showExampleBrowser = false; showNameInput = false; + showDocPicker = false; + showDocViewer = false; } String notification() { @@ -106,6 +132,37 @@ class ActionsPopup { } boolean handleKeyEvent(KeyEvent ke) { + if (showDocViewer) { + if (ke.isCancel()) { + showDocViewer = false; + if (docViewerFromExampleBrowser) { + docViewerFromExampleBrowser = false; + showExampleBrowser = true; + } + } else if (ke.isUp() || ke.isChar('k')) { + docScroll = Math.max(0, docScroll - 1); + } else if (ke.isDown() || ke.isChar('j')) { + docScroll++; + } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) { + docScroll = Math.max(0, docScroll - 10); + } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) { + docScroll += 10; + } + return true; + } + if (showDocPicker) { + if (ke.isCancel()) { + showDocPicker = false; + showActionsMenu = true; + } else if (ke.isUp()) { + docPickerState.selectPrevious(); + } else if (ke.isDown()) { + docPickerState.selectNext(docPickerIntegrations != null ? docPickerIntegrations.size() : 0); + } else if (ke.isConfirm()) { + loadDocFromSelectedIntegration(); + } + return true; + } if (showNameInput) { if (ke.isCancel()) { showNameInput = false; @@ -143,6 +200,8 @@ class ActionsPopup { navigateExampleBrowser(10); } else if (ke.isChar('r')) { openNameInput(); + } else if (ke.isChar('d')) { + loadDocFromExample(); } else if (ke.isConfirm()) { launchSelectedExample(); } @@ -157,11 +216,15 @@ class ActionsPopup { actionsMenuState.selectNext(ACTION_COUNT); } else if (ke.isConfirm()) { Integer sel = actionsMenuState.selected(); - if (sel != null && sel == ACTION_SCREENSHOT) { - showActionsMenu = false; - screenshotAction.run(); - } else { - openExampleBrowser(); + if (sel != null) { + if (sel == ACTION_RUN_EXAMPLE) { + openExampleBrowser(); + } else if (sel == ACTION_SHOW_DOCS) { + openDocPicker(); + } else if (sel == ACTION_SCREENSHOT) { + showActionsMenu = false; + screenshotAction.run(); + } } } return true; @@ -179,9 +242,26 @@ class ActionsPopup { if (showNameInput) { renderNameInput(frame, area); } + if (showDocPicker) { + renderDocPicker(frame, area); + } + if (showDocViewer) { + renderDocViewer(frame, area); + } } void renderFooter(List<Span> spans) { + if (showDocViewer) { + hint(spans, "↑↓", "scroll"); + hintLast(spans, "Esc", "back"); + return; + } + if (showDocPicker) { + hint(spans, "↑↓", "navigate"); + hint(spans, "Enter", "view"); + hintLast(spans, "Esc", "back"); + return; + } if (showNameInput) { hint(spans, "Enter", "launch"); hintLast(spans, "Esc", "back"); @@ -190,7 +270,8 @@ class ActionsPopup { if (showExampleBrowser) { hint(spans, "↑↓", "navigate"); hint(spans, "Enter", "run"); - hint(spans, "n", "name"); + hint(spans, "r", "run..."); + hint(spans, "d", "docs"); hintLast(spans, "Esc", "back"); return; } @@ -220,6 +301,7 @@ class ActionsPopup { frame.renderWidget(Clear.INSTANCE, popup); ListWidget list = ListWidget.builder() .items(ListItem.from(" Run an example..."), + ListItem.from(" Show Documentation"), ListItem.from(" Take Screenshot")) .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) .highlightSymbol("") @@ -256,6 +338,7 @@ class ActionsPopup { .titleBottom(Title.from(Line.from( Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), Span.raw(" run │"), Span.styled(" r", MonitorContext.HINT_KEY_STYLE), Span.raw(" run... │"), + Span.styled(" d", MonitorContext.HINT_KEY_STYLE), Span.raw(" docs │"), Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), Span.raw(" navigate │"), Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), Span.raw(" back ")))) .build()) @@ -334,6 +417,289 @@ class ActionsPopup { frame.renderStatefulWidget(textInput, inputArea, nameInputState); } + // ---- Doc Viewer & Picker ---- + + private void renderDocViewer(Frame frame, Rect area) { + Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() - 4, area.height() - 2); + frame.renderWidget(Clear.INSTANCE, popup); + MarkdownView view = MarkdownView.builder() + .source(docContent) + .scroll(docScroll) + .block(Block.builder() + .borderType(BorderType.ROUNDED) + .title(" " + docTitle + " ") + .titleBottom(Title.from(Line.from( + Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), Span.raw(" scroll │"), + Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), Span.raw(" back ")))) + .build()) + .build(); + frame.renderWidget(view, popup); + } + + private void renderDocPicker(Frame frame, Rect area) { + if (docPickerIntegrations == null || docPickerIntegrations.isEmpty()) { + return; + } + int popupW = Math.min(60, area.width() - 4); + int popupH = Math.min(docPickerIntegrations.size() + 2, Math.min(15, area.height() - 6)); + int x = area.left() + Math.max(0, (area.width() - popupW) / 2); + int y = area.top() + Math.max(0, (area.height() - popupH) / 2); + Rect popup = new Rect(x, y, Math.min(popupW, area.width()), Math.min(popupH, area.height())); + + frame.renderWidget(Clear.INSTANCE, popup); + List<ListItem> items = new ArrayList<>(); + for (IntegrationInfo info : docPickerIntegrations) { + String label = " " + (info.name != null ? info.name : info.pid); + items.add(ListItem.from(label)); + } + ListWidget list = ListWidget.builder() + .items(items.toArray(ListItem[]::new)) + .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue()) + .highlightSymbol("") + .scrollMode(ScrollMode.AUTO_SCROLL) + .block(Block.builder() + .borderType(BorderType.ROUNDED) + .title(" Show Documentation ") + .titleBottom(Title.from(Line.from( + Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), Span.raw(" view │"), + Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), Span.raw(" back ")))) + .build()) + .build(); + frame.renderStatefulWidget(list, popup, docPickerState); + } + + private void openDocPicker() { + showActionsMenu = false; + List<IntegrationInfo> withDocs = integrations.get().stream() + .filter(i -> !i.vanishing && i.readmeFiles != null && !i.readmeFiles.isEmpty()) + .collect(Collectors.toList()); + if (withDocs.isEmpty()) { + launchNotification = "No integrations with documentation found"; + launchNotificationError = true; + launchNotificationExpiry = System.currentTimeMillis() + 5000; + return; + } + if (withDocs.size() == 1) { + loadDocFromIntegration(withDocs.get(0)); + return; + } + docPickerIntegrations = withDocs; + showDocPicker = true; + docPickerState.select(0); + } + + private void loadDocFromSelectedIntegration() { + Integer sel = docPickerState.selected(); + if (sel == null || docPickerIntegrations == null || sel >= docPickerIntegrations.size()) { + return; + } + IntegrationInfo info = docPickerIntegrations.get(sel); + loadDocFromIntegration(info); + } + + private void loadDocFromIntegration(IntegrationInfo info) { + if (ctx == null) { + return; + } + showDocPicker = false; + try { + Path outputFile = ctx.getOutputFile(info.pid); + Files.deleteIfExists(outputFile); + JsonObject action = new JsonObject(); + action.put("action", "readme"); + PathUtils.writeTextSafely(action.toJson(), ctx.getActionFile(info.pid)); + JsonObject response = MonitorContext.pollJsonResponse(outputFile, 5000); + if (response != null && response.getString("content") != null) { + String raw = response.getString("content"); + String file = response.getStringOrDefault("file", "README"); + docContent = file.endsWith(".adoc") ? asciidocToMarkdown(raw) : raw; + docTitle = (info.name != null ? info.name : info.pid) + " - " + Path.of(file).getFileName(); + docScroll = 0; + showDocViewer = true; + docViewerFromExampleBrowser = false; + } else { + launchNotification = "Could not load documentation"; + launchNotificationError = true; + launchNotificationExpiry = System.currentTimeMillis() + 5000; + } + } catch (Exception e) { + launchNotification = "Error loading documentation: " + e.getMessage(); + launchNotificationError = true; + launchNotificationExpiry = System.currentTimeMillis() + 5000; + } + } + + private void loadDocFromExample() { + Integer sel = exampleBrowserState.selected(); + if (sel == null || isSeparatorIndex(sel)) { + return; + } + JsonObject example = getExampleAtListIndex(sel); + if (example == null) { + return; + } + String name = example.getStringOrDefault("name", ""); + boolean bundled = ExampleHelper.isBundled(example); + String content = null; + boolean isAdoc = false; + if (bundled) { + content = loadResourceContent("examples/" + name + "/README.md"); + } else { + String base = "https://raw.githubusercontent.com/apache/camel-jbang-examples/main/" + name + "/"; + content = downloadContent(base + "README.md"); + if (content == null) { + content = downloadContent(base + "README.adoc"); + isAdoc = content != null; + } + } + if (content != null && !content.isEmpty()) { + docContent = isAdoc ? asciidocToMarkdown(content) : content; + docTitle = name; + docScroll = 0; + showExampleBrowser = false; + showDocViewer = true; + docViewerFromExampleBrowser = true; + } else { + setNotification("No documentation available for: " + name, true); + } + } + + private static String loadResourceContent(String resourcePath) { + try (InputStream is = ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) { + if (is != null) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } catch (IOException e) { + // ignore + } + return null; + } + + private static String downloadContent(String url) { + try (InputStream is = URI.create(url).toURL().openStream()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + return null; + } + } + + static String asciidocToMarkdown(String adoc) { + StringBuilder sb = new StringBuilder(); + String[] lines = adoc.split("\n", -1); + String pendingLang = null; + boolean inCodeBlock = false; + for (String line : lines) { + if (!inCodeBlock && line.startsWith("[source")) { + int comma = line.indexOf(','); + int end = line.indexOf(']'); + if (comma >= 0 && end > comma) { + pendingLang = line.substring(comma + 1, end).trim(); + } else { + pendingLang = ""; + } + continue; + } + if (line.equals("----")) { + if (inCodeBlock) { + sb.append("```\n"); + inCodeBlock = false; + } else { + sb.append("```").append(pendingLang != null ? pendingLang : "").append('\n'); + pendingLang = null; + inCodeBlock = true; + } + continue; + } + if (inCodeBlock) { + sb.append(line).append('\n'); + continue; + } + pendingLang = null; + if (line.startsWith("include::")) { + continue; + } + if (line.startsWith("=")) { + if (line.startsWith("==== ")) { + sb.append("#### ").append(line.substring(5)).append('\n'); + } else if (line.startsWith("=== ")) { + sb.append("### ").append(line.substring(4)).append('\n'); + } else if (line.startsWith("== ")) { + sb.append("## ").append(line.substring(3)).append('\n'); + } else if (line.startsWith("= ")) { + sb.append("# ").append(line.substring(2)).append('\n'); + } else { + sb.append(line).append('\n'); + } + continue; + } + String converted = line; + converted = convertImages(converted); + converted = convertLinks(converted); + sb.append(converted).append('\n'); + } + if (inCodeBlock) { + sb.append("```\n"); + } + return sb.toString(); + } + + private static String convertImages(String line) { + int idx = 0; + StringBuilder sb = new StringBuilder(); + while (idx < line.length()) { + int imgStart = line.indexOf("image::", idx); + if (imgStart < 0) { + sb.append(line, idx, line.length()); + break; + } + sb.append(line, idx, imgStart); + int bracketOpen = line.indexOf('[', imgStart); + int bracketClose = bracketOpen >= 0 ? line.indexOf(']', bracketOpen) : -1; + if (bracketOpen >= 0 && bracketClose >= 0) { + String file = line.substring(imgStart + 7, bracketOpen); + String alt = line.substring(bracketOpen + 1, bracketClose); + sb.append(".append(file).append(')'); + idx = bracketClose + 1; + } else { + sb.append("image::"); + idx = imgStart + 7; + } + } + return sb.toString(); + } + + private static String convertLinks(String line) { + int idx = 0; + StringBuilder sb = new StringBuilder(); + while (idx < line.length()) { + int linkStart = line.indexOf("link:", idx); + if (linkStart < 0) { + // also handle bare URL[text] pattern + sb.append(line, idx, line.length()); + break; + } + sb.append(line, idx, linkStart); + int bracketOpen = line.indexOf('[', linkStart); + int bracketClose = bracketOpen >= 0 ? line.indexOf(']', bracketOpen) : -1; + if (bracketOpen >= 0 && bracketClose >= 0) { + String url = line.substring(linkStart + 5, bracketOpen); + String text = line.substring(bracketOpen + 1, bracketClose); + sb.append('[').append(text).append("](").append(url).append(')'); + idx = bracketClose + 1; + } else { + sb.append("link:"); + idx = linkStart + 5; + } + } + return sb.toString(); + } + + private void setNotification(String msg, boolean error) { + launchNotification = msg; + launchNotificationError = error; + launchNotificationExpiry = System.currentTimeMillis() + 10000; + } + // ---- Name Input ---- private void openNameInput() { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 0bb354a258ac..172add2ed08c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -206,6 +206,9 @@ public class CamelMonitor extends CamelCommand { .filter(i -> !i.vanishing && i.name != null) .map(i -> i.name) .collect(Collectors.toSet()), + () -> data.get().stream() + .filter(i -> !i.vanishing) + .collect(Collectors.toList()), () -> pendingScreenshot = true); private final AtomicBoolean refreshInProgress = new AtomicBoolean(false); @@ -250,6 +253,7 @@ public class CamelMonitor extends CamelCommand { // Create shared context and tab instances ctx = new MonitorContext(data, infraData); + actionsPopup.setContext(ctx); logTab = new LogTab(ctx); routesTab = new RoutesTab(ctx); consumersTab = new ConsumersTab(ctx); @@ -2367,6 +2371,7 @@ public class CamelMonitor extends CamelCommand { info.javaVersion = runtime != null ? runtime.getString("javaVersion") : null; info.javaVendor = runtime != null ? runtime.getString("javaVendor") : null; info.javaVmName = runtime != null ? runtime.getString("javaVmName") : null; + info.readmeFiles = runtime != null ? runtime.getString("readmeFiles") : null; Map<String, ?> stats = context.getMap("statistics"); if (stats != null) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java index 63f700c6eec0..921ab26ad816 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java @@ -64,4 +64,5 @@ class IntegrationInfo { final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>(); final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>(); String httpServer; + String readmeFiles; }
