├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml └── workflows │ ├── commits.yml │ ├── vaadin14.yml │ ├── vaadin23.yml │ └── vaadin24.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── assembly ├── MANIFEST.MF └── assembly.xml ├── drivers.xml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── flowingcode │ │ └── vaadin │ │ └── addons │ │ └── xterm │ │ ├── ClientTerminalAddon.java │ │ ├── ITerminal.java │ │ ├── ITerminalClipboard.java │ │ ├── ITerminalConsole.java │ │ ├── ITerminalFit.java │ │ ├── ITerminalOptions.java │ │ ├── ITerminalSelection.java │ │ ├── PreserveStateAddon.java │ │ ├── TerminalAddon.java │ │ ├── TerminalHistory.java │ │ ├── TerminalTheme.java │ │ ├── XTerm.java │ │ ├── XTermBase.java │ │ └── utils │ │ └── StateMemoizer.java └── resources │ └── META-INF │ ├── VAADIN │ └── package.properties │ ├── frontend │ └── fc-xterm │ │ ├── xterm-clipboard-mixin.ts │ │ ├── xterm-console-mixin.ts │ │ ├── xterm-element.ts │ │ ├── xterm-fit-mixin.ts │ │ ├── xterm-insertfix-mixin.ts │ │ ├── xterm-selection-mixin.ts │ │ └── xterm.ts │ └── native-image │ └── com.flowingcode.addons │ └── xterm-console │ ├── proxy-config.json │ └── reflect-config.json └── test └── java └── com └── flowingcode └── vaadin └── addons ├── DemoLayout.java └── xterm ├── DemoView.java ├── PreserveStateAddonTest.java ├── XtermDemoView.java ├── integration ├── AbstractViewTest.java ├── ClipboardFeatureIT.java ├── ConsoleFeatureIT.java ├── FitFeatureIT.java ├── Position.java ├── SelectionFeatureIT.java ├── TerminalHistoryIT.java ├── XTermElement.java ├── XTermIT.java └── XTermTestUtils.java ├── test └── SerializationTest.java └── utils └── StateMemoizerTest.java /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Please report issues related to XTerm add-on here. 3 | body: 4 | - type: textarea 5 | id: problem-description 6 | attributes: 7 | label: Describe the bug 8 | description: A clear description of the issue you're experiencing. 9 | validations: 10 | required: true 11 | - type: textarea 12 | id: expected-behavior 13 | attributes: 14 | label: Expected behavior 15 | description: A clear and concise description of the expected behavior. 16 | validations: 17 | required: false 18 | - type: textarea 19 | id: minimal-reproduction 20 | attributes: 21 | label: Minimal reproducible example 22 | description: If possible, add a concise code snippet that reproduces the issue and describe the steps needed to follow to reproduce it. 23 | validations: 24 | required: false 25 | - type: input 26 | id: addon-version 27 | attributes: 28 | label: Add-on Version 29 | description: The version of the add-on on which you're experiencing the issue. 30 | validations: 31 | required: true 32 | - type: input 33 | id: vaadin-version 34 | attributes: 35 | label: Vaadin Version 36 | description: The complete Vaadin version (X.Y.Z) on which the issue is reproducible. 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: additional-information 41 | attributes: 42 | label: Additional information 43 | description: "Any other context/information about the issue can be added here (browser, OS, etc.)." 44 | validations: 45 | required: false 46 | 47 | 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Please add feature suggestions related to XTerm add-on here. 3 | body: 4 | - type: textarea 5 | id: feature-proposal 6 | attributes: 7 | label: Feature proposal 8 | description: A concise but detailed description of the feature that you would like to see in the add-on. 9 | validations: 10 | required: true 11 | - type: textarea 12 | id: feature-implementation 13 | attributes: 14 | label: Describe solution expectations 15 | description: Do you have an idea/expectations of how it could be implemented? Did you try a possible solution that you want to share? 16 | validations: 17 | required: false 18 | - type: textarea 19 | id: additional-information 20 | attributes: 21 | label: Additional information 22 | description: Add any extra information you think it might be relevant to the request. 23 | validations: 24 | required: false 25 | -------------------------------------------------------------------------------- /.github/workflows/commits.yml: -------------------------------------------------------------------------------- 1 | name: Check Commits 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | check-commits: 8 | uses: FlowingCode/GithubActions/.github/workflows/check-commits.yml@main 9 | -------------------------------------------------------------------------------- /.github/workflows/vaadin14.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "1.x" ] 14 | pull_request: 15 | branches: [ "1.x" ] 16 | 17 | jobs: 18 | build-vaadin14: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up JDK 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: '8' 26 | distribution: 'temurin' 27 | cache: maven 28 | - name: Build (Vaadin 14) 29 | run: mvn -B package --file pom.xml 30 | -------------------------------------------------------------------------------- /.github/workflows/vaadin23.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "2.x" ] 14 | pull_request: 15 | branches: [ "2.x" ] 16 | 17 | jobs: 18 | 19 | build-vaadin23: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up JDK 24 | uses: actions/setup-java@v3 25 | with: 26 | java-version: '11' 27 | distribution: 'temurin' 28 | cache: maven 29 | - name: Build (Vaadin 23) 30 | run: mvn -B package --file pom.xml -Pv23 31 | -------------------------------------------------------------------------------- /.github/workflows/vaadin24.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "master" ] 14 | pull_request: 15 | branches: [ "master" ] 16 | 17 | jobs: 18 | 19 | build-vaadin24: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up JDK 24 | uses: actions/setup-java@v3 25 | with: 26 | java-version: '17' 27 | distribution: 'temurin' 28 | cache: maven 29 | - name: Build (Vaadin 24) 30 | run: mvn -B package --file pom.xml 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target 3 | .vscode 4 | .settings 5 | .project 6 | .classpath 7 | webpack.generated.js 8 | package-lock.json 9 | package.json 10 | webpack.config.js 11 | /error-screenshots 12 | drivers 13 | tsconfig.json 14 | .idea 15 | types.d.ts 16 | /frontend/generated 17 | /frontend/index.html 18 | vite.generated.ts 19 | /.npmrc 20 | /.pnpm-debug.log 21 | /pnpm-lock.yaml 22 | /pnpmfile.js 23 | /src/main/bundles 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Published on Vaadin Directory](https://img.shields.io/badge/Vaadin%20Directory-published-00b4f0.svg)](https://vaadin.com/directory/component/xterm-console-addon) 2 | [![Stars on vaadin.com/directory](https://img.shields.io/vaadin-directory/star/xterm-console-addon.svg)](https://vaadin.com/directory/component/xterm-console-addon) 3 | [![Build Status](https://jenkins.flowingcode.com/job/XTerm-2-addon/badge/icon)](https://jenkins.flowingcode.com/job/XTerm-2-addon) 4 | [![Javadoc](https://img.shields.io/badge/javadoc-00b4f0)](https://javadoc.flowingcode.com/artifact/com.flowingcode.addons/xterm-console) 5 | 6 | # XTerm Console Add-on 7 | 8 | Vaadin 14+ Java integration of [xterm.js](https://xtermjs.org/) terminal emulator. 9 | 10 | ## Features 11 | 12 | * Send input text to server 13 | * Programmatically write to the console 14 | * Clipboard support 15 | * Command line edition (cursor keys, insert, etc.) 16 | * ANSI escape sequences 17 | * And much more... 18 | 19 | ## Online demo 20 | 21 | * [Vaadin 14](http://addonsv14.flowingcode.com/xterm) (Add-on version 1.x) 22 | * [Vaadin 23](http://addonsv23.flowingcode.com/xterm) (Add-on version 2.x) 23 | * [Vaadin 24](https://addonsv24.flowingcode.com/xterm) (Add-on version 3.x) 24 | 25 | ## Download release 26 | 27 | [Available in Vaadin Directory](https://vaadin.com/directory/component/xterm-console-addon) 28 | 29 | ### Maven install 30 | 31 | Add the following dependencies in your pom.xml file: 32 | 33 | ```xml 34 | 35 | com.flowingcode.addons 36 | xterm-console 37 | X.Y.Z 38 | 39 | ``` 40 | 41 | 42 | ```xml 43 | 44 | vaadin-addons 45 | https://maven.vaadin.com/vaadin-addons 46 | 47 | ``` 48 | 49 | For SNAPSHOT versions see [here](https://maven.flowingcode.com/snapshots/). 50 | 51 | ## Building and running demo 52 | 53 | - git clone repository 54 | - mvn clean install jetty:run 55 | 56 | To see the demo, navigate to http://localhost:8080/ 57 | 58 | ## Release notes 59 | 60 | See [here](https://github.com/FlowingCode/XTermConsoleAddon/releases) 61 | 62 | ## Issue tracking 63 | 64 | The issues for this add-on are tracked on its github.com page. All bug reports and feature requests are appreciated. 65 | 66 | ## Contributions 67 | 68 | Contributions are welcome, but there are no guarantees that they are accepted as such. 69 | 70 | As first step, please refer to our [Development Conventions](https://github.com/FlowingCode/DevelopmentConventions) page to find information about Conventional Commits & Code Style requeriments. 71 | 72 | Then, follow these steps for creating a contribution: 73 | 74 | - Fork this project. 75 | - Create an issue to this project about the contribution (bug or feature) if there is no such issue about it already. Try to keep the scope minimal. 76 | - Develop and test the fix or functionality carefully. Only include minimum amount of code needed to fix the issue. 77 | - For commit message, use [Conventional Commits](https://github.com/FlowingCode/DevelopmentConventions/blob/main/conventional-commits.md) to describe your change. 78 | - Send a pull request for the original project. 79 | - Comment on the original issue that you have implemented a fix for it. 80 | 81 | ## License & Author 82 | 83 | This add-on is distributed under Apache License 2.0. For license terms, see LICENSE.txt. 84 | 85 | XTermConsoleAddon is written by Flowing Code S.A. 86 | 87 | # Developer Guide 88 | 89 | ## Getting started 90 | 91 | Initialize a terminal with autosize, clipboard support and listener: 92 | ``` 93 | XTerm xterm = new XTerm(); 94 | xterm.writeln("Hello world.\n\n"); 95 | xterm.setCursorBlink(true); 96 | xterm.setCursorStyle(CursorStyle.UNDERLINE); 97 | 98 | xterm.setSizeFull(); 99 | xterm.setCopySelection(true); 100 | xterm.setUseSystemClipboard(UseSystemClipboard.READWRITE); 101 | xterm.setPasteWithRightClick(true); 102 | 103 | xterm.addLineListener(ev->{ 104 | String line = ev.getLine(); 105 | System.out.println(line); 106 | }); 107 | 108 | xterm.focus(); 109 | 110 | xterm.fit(); 111 | ``` 112 | 113 | ## Special configuration when using Spring 114 | 115 | By default, Vaadin Flow only includes ```com/vaadin/flow/component``` to be always scanned for UI components and views. For this reason, the addon might need to be whitelisted in order to display correctly. 116 | 117 | To do so, just add ```com.flowingcode``` to the ```vaadin.whitelisted-packages``` property in ```src/main/resources/application.properties```, like: 118 | 119 | ```vaadin.whitelisted-packages = com.vaadin,org.vaadin,dev.hilla,com.flowingcode``` 120 | 121 | More information on Spring whitelisted configuration [here](https://vaadin.com/docs/latest/integrations/spring/configuration/#configure-the-scanning-of-packages). 122 | 123 | ## Special configuration for Vaadin 23.3 and Vaadin 24 124 | 125 | Vaadin 23.3 and 24 validate addons sources as part of the build (see vaadin/flow#15485). 126 | In order to exclude the code in the addons from validation, you need to add the following to tsconfig.json: 127 | 128 | ``` 129 | "exclude": [ 130 | "frontend/generated/jar-resources" 131 | ] 132 | ``` 133 | -------------------------------------------------------------------------------- /assembly/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Vaadin-Package-Version: 1 3 | Vaadin-Addon: ${project.build.finalName}.${project.packaging} 4 | Implementation-Vendor: ${organization.name} 5 | Implementation-Title: ${project.name} 6 | Implementation-Version: ${project.version} 7 | -------------------------------------------------------------------------------- /assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | xterm-console 7 | 8 | 9 | zip 10 | 11 | 12 | 13 | false 14 | 15 | 16 | 17 | . 18 | 19 | LICENSE 20 | README.md 21 | 22 | 23 | 24 | target 25 | 26 | 27 | *.jar 28 | *.pdf 29 | 30 | 31 | *-demo.jar 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | assembly/MANIFEST.MF 41 | META-INF 42 | true 43 | 44 | 45 | -------------------------------------------------------------------------------- /drivers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | https://chromedriver.storage.googleapis.com/83.0.4103.39/chromedriver_win32.zip 8 | e06f7553e68df7eec76a6fd0e1a21317807bf945 9 | sha1 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | https://chromedriver.storage.googleapis.com/83.0.4103.39/chromedriver_linux64.zip 19 | 9bd05c0f37c8eb4a0a20a18b5dac11e7c1a516ef 20 | sha1 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | https://chromedriver.storage.googleapis.com/83.0.4103.39/chromedriver_mac64.zip 30 | d1229532b6ffa4c2799de8f46194622b0e2c5b63 31 | sha1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.flowingcode.addons 6 | xterm-console 7 | 3.2.0-SNAPSHOT 8 | XTerm Console Addon 9 | Integration of xterm.js for Vaadin Flow 10 | 11 | 12 | 24.3.5 13 | 17 14 | 17 15 | UTF-8 16 | UTF-8 17 | ${project.basedir}/drivers 18 | 11.0.12 19 | 20 | 21 | 22 | Flowing Code 23 | https://www.flowingcode.com 24 | 25 | 2020 26 | 27 | 28 | Apache 2 29 | http://www.apache.org/licenses/LICENSE-2.0.txt 30 | repo 31 | 32 | 33 | 34 | 35 | https://github.com/FlowingCode/XTermConsoleAddon 36 | scm:git:git://github.com/FlowingCode/XTermConsoleAddon.git 37 | scm:git:ssh://git@github.com:/FlowingCode/XTermConsoleAddon.git 38 | HEAD 39 | 40 | 41 | 42 | 43 | flowingcode 44 | Flowing Code 45 | https://www.flowingcode.com 46 | 47 | 48 | 49 | 50 | 51 | 52 | com.vaadin 53 | vaadin-bom 54 | pom 55 | import 56 | ${vaadin.version} 57 | 58 | 59 | 60 | 61 | 62 | 63 | central 64 | https://repo.maven.apache.org/maven2 65 | 66 | false 67 | 68 | 69 | 70 | Vaadin Directory 71 | https://maven.vaadin.com/vaadin-addons 72 | 73 | 74 | FlowingCode Releases 75 | https://maven.flowingcode.com/releases 76 | 77 | 78 | FlowingCode Snapshots 79 | https://maven.flowingcode.com/snapshots 80 | 81 | true 82 | 83 | 84 | false 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.projectlombok 92 | lombok 93 | provided 94 | 1.18.34 95 | 96 | 97 | 98 | com.vaadin 99 | vaadin-core 100 | true 101 | 102 | 103 | 104 | org.slf4j 105 | slf4j-simple 106 | test 107 | 108 | 109 | com.vaadin 110 | vaadin-testbench 111 | test 112 | 113 | 114 | org.hamcrest 115 | hamcrest-library 116 | 1.3 117 | test 118 | 119 | 120 | javax.servlet 121 | javax.servlet-api 122 | 3.1.0 123 | jar 124 | test 125 | 126 | 127 | com.flowingcode.vaadin.addons.demo 128 | commons-demo 129 | 4.0.0 130 | test 131 | 132 | 133 | io.github.bonigarcia 134 | webdrivermanager 135 | 3.8.1 136 | test 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-release-plugin 146 | 2.5.3 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-deploy-plugin 151 | 2.8.2 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-surefire-plugin 156 | 2.22.1 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | org.codehaus.mojo 168 | license-maven-plugin 169 | 1.14 170 | 171 | apache_v2 172 | false 173 | 174 | 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-jar-plugin 179 | 3.1.2 180 | 181 | 182 | true 183 | 184 | false 185 | true 186 | 187 | 188 | 1 189 | 190 | 191 | 192 | 193 | 194 | 195 | com.vaadin 196 | vaadin-maven-plugin 197 | ${vaadin.version} 198 | 199 | 200 | 201 | prepare-frontend 202 | 203 | 204 | 205 | 206 | 207 | org.eclipse.jetty 208 | jetty-maven-plugin 209 | ${jetty.version} 210 | 211 | 3 212 | 214 | true 215 | 216 | 222 | 223 | 224 | jar 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | directory 234 | 235 | 236 | 237 | org.apache.maven.plugins 238 | maven-assembly-plugin 239 | 3.3.0 240 | 241 | false 242 | 243 | assembly/assembly.xml 244 | 245 | 246 | 247 | 248 | 249 | single 250 | 251 | install 252 | 253 | 254 | 255 | 256 | org.apache.maven.plugins 257 | maven-source-plugin 258 | 3.0.1 259 | 260 | 261 | attach-sources 262 | verify 263 | 264 | jar-no-fork 265 | 266 | 267 | 268 | 269 | 270 | org.apache.maven.plugins 271 | maven-javadoc-plugin 272 | 3.11.1 273 | 274 | 275 | attach-javadocs 276 | package 277 | 278 | jar 279 | 280 | 281 | 282 | 283 | true 284 | none 285 | true 286 | 287 | https://javadoc.io/doc/com.vaadin/vaadin-platform-javadoc/${vaadin.version} 288 | 289 | 290 | 291 | 292 | org.apache.maven.plugins 293 | maven-jar-plugin 294 | 3.1.2 295 | 296 | 297 | 298 | META-INF/VAADIN/config/flow-build-info.json 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | production 309 | 310 | true 311 | 312 | 313 | 314 | com.vaadin 315 | flow-server-production-mode 316 | 317 | 318 | 319 | 320 | 321 | 322 | com.vaadin 323 | vaadin-maven-plugin 324 | 325 | 326 | 327 | build-frontend 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | integration-tests 338 | 339 | 340 | 341 | org.eclipse.jetty 342 | jetty-maven-plugin 343 | ${jetty.version} 344 | 345 | 0 346 | 347 | jar 348 | 349 | ${project.artifactId} 350 | 8081 351 | 352 | 353 | 354 | start-jetty 355 | pre-integration-test 356 | 357 | start 358 | 359 | 360 | 361 | stop-jetty 362 | post-integration-test 363 | 364 | stop 365 | 366 | 367 | 368 | 369 | 370 | 371 | com.lazerycode.selenium 372 | driver-binary-downloader-maven-plugin 373 | 374 | 1.0.17 375 | 376 | 377 | true 378 | 379 | 380 | ${drivers.dir}/driver 381 | 382 | 383 | ${drivers.dir}/driver_zips 384 | 385 | 386 | ${project.basedir}/drivers.xml 387 | 388 | 389 | 390 | 391 | 392 | selenium 393 | 394 | 395 | 396 | 397 | 398 | 399 | org.apache.maven.plugins 400 | maven-failsafe-plugin 401 | 2.22.2 402 | 403 | 404 | 405 | integration-test 406 | verify 407 | 408 | 409 | 410 | 411 | false 412 | true 413 | 414 | 415 | 416 | ${webdriver.chrome.driver} 417 | 418 | 419 | 420 | 421 | 422 | maven-resources-plugin 423 | 3.1.0 424 | 425 | 426 | 429 | copy-test-to-classes 430 | process-test-classes 431 | 432 | copy-resources 433 | 434 | 435 | ${project.build.outputDirectory} 436 | 437 | 438 | ${project.build.testOutputDirectory} 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | demo-jar 451 | 452 | 453 | 454 | org.apache.maven.plugins 455 | maven-jar-plugin 456 | 457 | 458 | 459 | test-jar 460 | 461 | 462 | demo 463 | 464 | **/test/* 465 | **/integration/* 466 | **/DemoView.class 467 | **/DemoLayout.class 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ClientTerminalAddon.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2025 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.dom.Element; 23 | import com.vaadin.flow.internal.JsonCodec; 24 | import elemental.json.Json; 25 | import elemental.json.JsonArray; 26 | import java.io.Serializable; 27 | 28 | /** 29 | * Represents an abstract base class for server-side terminal add-ons that have a corresponding 30 | * client-side (JavaScript) component or require interaction with the client-side terminal 31 | * environment. It extends {@link TerminalAddon} and specializes its use for client-aware 32 | * operations. 33 | * 34 | * @author Javier Godoy / Flowing Code S.A. 35 | */ 36 | @SuppressWarnings("serial") 37 | public abstract class ClientTerminalAddon extends TerminalAddon { 38 | 39 | private final XTermBase xterm; 40 | 41 | /** 42 | * Constructs a new {@code ClientTerminalAddon} and associates it with the specified 43 | * {@link XTermBase} instance. 44 | *

45 | * This constructor ensures the add-on is registered with the terminal and verifies that the 46 | * add-on's name, as returned by {@link #getName()}, is not {@code null}. A non-null name is 47 | * required for client-side add-ons to be uniquely identified and targeted for JavaScript 48 | * execution. 49 | *

50 | * 51 | * @param xterm the {@link XTermBase} instance this add-on will be attached to. Must not be 52 | * {@code null}. 53 | * @throws NullPointerException if {@code xterm} is {@code null} 54 | * @throws IllegalStateException if {@link #getName()} returns {@code null} immediately after 55 | * superclass construction. This check relies on {@code getName()} being a static value. 56 | */ 57 | protected ClientTerminalAddon(XTermBase xterm) { 58 | super(xterm); 59 | this.xterm = xterm; 60 | if (getName() == null) { 61 | throw new IllegalStateException("getName() must return a non-null value"); 62 | } 63 | } 64 | 65 | /** 66 | * The xterm instance that this add-on is associated with. 67 | */ 68 | protected XTermBase getXterm() { 69 | return xterm; 70 | } 71 | 72 | /** 73 | * Retrieves the unique name of this client-side add-on. 74 | *

75 | * This name is used by {@link #executeJs(String, Serializable...)} to target the corresponding 76 | * JavaScript object on the client (i.e., {@code this.addons[name]} within the client-side 77 | * terminal's scope). The name effectively acts as a key in a client-side add-ons collection 78 | * managed by the terminal. 79 | *

80 | * 81 | * @return the unique, non-null string identifier for the client-side counterpart of this add-on. 82 | * Subclasses must implement this to provide a name for add-on-specific JavaScript 83 | * execution. 84 | */ 85 | protected abstract String getName(); 86 | 87 | /** 88 | * Executes a JavaScript {@code expression} in the context of this add-on, with the specified 89 | * {@code parameters}. 90 | * 91 | * @see #getName() 92 | * @see Element#executeJs(String, Serializable...) 93 | */ 94 | protected final void executeJs(String expression, Serializable... parameters) { 95 | String name = getName(); 96 | 97 | JsonArray args = Json.createArray(); 98 | for (int i = 0; i < parameters.length; i++) { 99 | args.set(i, JsonCodec.encodeWithTypeInfo(parameters[i])); 100 | } 101 | 102 | expression = expression.replaceAll("\\$(\\d+)", "\\$1[$1]"); 103 | xterm.executeJs("(function(){" + expression + "}).apply(this.addons[$0],$1);", name, args); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ITerminal.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import java.io.Serializable; 23 | import java.util.concurrent.CompletableFuture; 24 | 25 | /** The API that represents an xterm.js terminal. */ 26 | public interface ITerminal extends Serializable { 27 | 28 | /** Unfocus the terminal. */ 29 | void blur(); 30 | 31 | /** Focus the terminal. */ 32 | void focus(); 33 | 34 | /** Gets whether the terminal has an active selection. */ 35 | CompletableFuture hasSelection(); 36 | 37 | /** 38 | * Gets the terminal's current selection, this is useful for implementing copy behavior outside of 39 | * xterm.js. 40 | */ 41 | CompletableFuture getSelection(); 42 | 43 | // Gets the selection position or undefined if there is no selection./ 44 | // getSelectionPosition(): ISelectionPosition | undefined; 45 | 46 | /** Clears the current terminal selection. */ 47 | void clearSelection(); 48 | 49 | /** 50 | * Selects text within the terminal. 51 | * 52 | * @param column The column the selection starts at. 53 | * @param row The row the selection starts at. 54 | * @param length The length of the selection. 55 | */ 56 | void select(int column, int row, int length); 57 | 58 | /** Selects all text within the terminal. */ 59 | void selectAll(); 60 | 61 | /** 62 | * Selects text in the buffer between 2 lines. 63 | * 64 | * @param start The 0-based line index to select from (inclusive). 65 | * @param end The 0-based line index to select to (inclusive). 66 | */ 67 | void selectLines(int start, int end); 68 | 69 | /** 70 | * Scroll the display of the terminal 71 | * 72 | * @param amount The number of lines to scroll down (negative scroll up). 73 | */ 74 | void scrollLines(int amount); 75 | 76 | /** 77 | * Scroll the display of the terminal by a number of pages. 78 | * 79 | * @param pageCount The number of pages to scroll (negative scrolls up). 80 | */ 81 | void scrollPages(int pageCount); 82 | 83 | /** Scrolls the display of the terminal to the top. */ 84 | void scrollToTop(); 85 | 86 | /** Scrolls the display of the terminal to the bottom. */ 87 | void scrollToBottom(); 88 | 89 | /** 90 | * Scrolls to a line within the buffer. 91 | * 92 | * @param line The 0-based line index to scroll to. 93 | */ 94 | void scrollToLine(int line); 95 | 96 | /** Clear the entire buffer, making the prompt line the new first line. */ 97 | void clear(); 98 | 99 | /** 100 | * Write data to the terminal. 101 | * 102 | * @param data The data to write to the terminal. 103 | */ 104 | void write(String data); 105 | 106 | /** 107 | * Writes data to the terminal, followed by a break line character (\n). 108 | * 109 | * @param data The data to write to the terminal. This can either be raw bytes given as Uint8Array 110 | * from the pty or a string. Raw bytes will always be treated as UTF-8 encoded, string data as 111 | * UTF-16. 112 | */ 113 | void writeln(String data); 114 | 115 | /** 116 | * Writes text to the terminal, performing the necessary transformations for pasted text. 117 | * 118 | * @param data The text to write to the terminal. 119 | */ 120 | void paste(String data); 121 | 122 | /** 123 | * Tells the renderer to refresh terminal content between two rows (inclusive) at the next 124 | * opportunity. 125 | * 126 | * @param start The row to start from (between 0 and this.rows - 1). 127 | * @param end The row to end at (between start and this.rows - 1). 128 | */ 129 | void refresh(int start, int end); 130 | 131 | /** Perform a full reset (RIS, aka '\x1bc'). */ 132 | void reset(); 133 | 134 | /** Resizes the terminal. Set the number of columns and rows in the terminal. */ 135 | void resize(int columns, int rows); 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ITerminalClipboard.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.HasElement; 23 | import java.util.Locale; 24 | import java.util.stream.Stream; 25 | 26 | /** 27 | * Add clipboard support to XTerm. This provides handling of Ctrl-C (copy) and Ctrl-V (paste) 28 | * shortcuts, and optional click-to-copy and click-to-paste. The clipboard target is either the 29 | * system clipboard or an internal buffer. 30 | */ 31 | public interface ITerminalClipboard extends HasElement { 32 | 33 | public enum UseSystemClipboard { 34 | /** Copy and paste from the internal buffer */ 35 | FALSE, 36 | /** Copy to system clipboard, paste from internal buffer */ 37 | WRITE, 38 | /** Copy and paste from the system clipboard */ 39 | READWRITE; 40 | } 41 | 42 | /** Configure the clipboard to write (and optionally read from) the system clipboard */ 43 | default void setUseSystemClipboard(UseSystemClipboard value) { 44 | getElement().setProperty("useSystemClipboard", value.toString().toLowerCase(Locale.ENGLISH)); 45 | } 46 | 47 | /** Handle mouse right click as a paste action */ 48 | default void setPasteWithRightClick(boolean value) { 49 | getElement().setProperty("pasteWithRightClick", value); 50 | } 51 | 52 | /** Handle mouse middle click as a paste action */ 53 | default void setPasteWithMiddleClick(boolean value) { 54 | getElement().setProperty("pasteWithMiddleClick", value); 55 | } 56 | 57 | /** Automatically copy to the clipboard the select text. */ 58 | default void setCopySelection(boolean value) { 59 | getElement().setProperty("copySelection", value); 60 | } 61 | 62 | /** Return whether copy and paste actions use the system clipboard */ 63 | default UseSystemClipboard getUseSystemClipboard() { 64 | String value = getElement().getProperty("useSystemClipboard", ""); 65 | return Stream.of(UseSystemClipboard.values()) 66 | .filter(e -> e.toString().toLowerCase(Locale.ENGLISH).equals(value)) 67 | .findAny() 68 | .orElse(UseSystemClipboard.FALSE); 69 | } 70 | 71 | /** Return whether mouse right click is handled as a paste action */ 72 | default boolean isPasteWithRightClick() { 73 | return getElement().getProperty("pasteWithRightClick", false); 74 | } 75 | 76 | /** Return whether mouse middle click is handled as a paste action */ 77 | default boolean isPasteWithMiddleClick() { 78 | return getElement().getProperty("pasteWithMiddleClick", false); 79 | } 80 | 81 | /** Return whether text is copied to the clipboard when selected. */ 82 | default boolean isCopySelection() { 83 | return getElement().getProperty("copySelection", false); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ITerminalConsole.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.Component; 23 | import com.vaadin.flow.component.ComponentEvent; 24 | import com.vaadin.flow.component.ComponentEventListener; 25 | import com.vaadin.flow.component.ComponentUtil; 26 | import com.vaadin.flow.component.DomEvent; 27 | import com.vaadin.flow.component.EventData; 28 | import com.vaadin.flow.component.HasElement; 29 | import com.vaadin.flow.shared.Registration; 30 | import elemental.json.JsonValue; 31 | import java.util.concurrent.CompletableFuture; 32 | import lombok.Getter; 33 | 34 | /** 35 | * Add console support to XTerm. This provides handling of cursor, home/end, insert, delete and 36 | * backspace keys, as well as a {@link #addLineListener(ComponentEventListener) line event}. 37 | */ 38 | @SuppressWarnings("serial") 39 | public interface ITerminalConsole extends HasElement { 40 | 41 | // TODO set cursor properties (blink, style, width) separately for insert and overwrite modes 42 | 43 | @DomEvent("line") 44 | public static class LineEvent extends ComponentEvent { 45 | 46 | private final @Getter String line; 47 | 48 | public LineEvent(XTerm source, boolean fromClient, @EventData("event.detail") String line) { 49 | super(source, fromClient); 50 | this.line = line; 51 | } 52 | } 53 | 54 | /** Adds a line listener. The listener is called when a line feed is intered in the console. */ 55 | default Registration addLineListener(ComponentEventListener listener) { 56 | Component terminal = getElement().getComponent().get(); 57 | return ComponentUtil.addListener(terminal, LineEvent.class, listener); 58 | } 59 | 60 | /** Set the insert mode. */ 61 | default void setInsertMode(boolean insertMode) { 62 | ((XTermBase) this).executeJs("this.insertMode=$0", insertMode); 63 | } 64 | 65 | /** Returns the text in the current line */ 66 | default CompletableFuture getCurrentLine() { 67 | return getElement().executeJs("return this.currentLine").toCompletableFuture() 68 | .thenApply(JsonValue::asString); 69 | } 70 | 71 | /** Sets the command line prompt. */ 72 | default void setPrompt(String prompt) { 73 | getElement().setProperty("prompt", prompt); 74 | } 75 | 76 | /** Returns the command line prompt. */ 77 | default String getPrompt() { 78 | return getElement().getProperty("prompt", ""); 79 | } 80 | 81 | /** Writes the command line prompt to the terminal. */ 82 | default void writePrompt() { 83 | ((XTermBase) this).executeJs("this.writePrompt()"); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ITerminalFit.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.HasElement; 23 | import com.vaadin.flow.component.dependency.NpmPackage; 24 | 25 | @NpmPackage(value = "xterm-addon-fit", version = "0.7.0") 26 | public interface ITerminalFit extends HasElement { 27 | 28 | default void fit() { 29 | getElement().executeJs("this.fit()"); 30 | } 31 | 32 | default void setFitOnResize(boolean value) { 33 | getElement().setProperty("fitOnResize", value); 34 | } 35 | 36 | default boolean isFitOnResize() { 37 | return getElement().getProperty("fitOnResize", false); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ITerminalOptions.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import java.io.Serializable; 23 | 24 | /** Start up options for the terminal. */ 25 | public interface ITerminalOptions extends Serializable { 26 | 27 | /** A data uri of the sound to use for the bell when `bellStyle = 'sound'`. */ 28 | void setBellSound(String value); 29 | 30 | /** The type of the bell notification the terminal will use. */ 31 | void setBellStyle(BellStyle value); 32 | 33 | enum BellStyle { 34 | NONE, 35 | SOUND 36 | } 37 | 38 | /** Whether the cursor blinks. */ 39 | void setCursorBlink(boolean value); 40 | 41 | /** The style of the cursor. */ 42 | void setCursorStyle(CursorStyle value); 43 | 44 | enum CursorStyle { 45 | BLOCK, 46 | UNDERLINE, 47 | BAR 48 | } 49 | 50 | /** The width of the cursor in CSS pixels when `cursorStyle` is set to 'bar'. */ 51 | void setCursorWidth(int value); 52 | 53 | /** Whether to draw bold text in bright colors. The default is true. */ 54 | void setDrawBoldTextInBrightColors(boolean value); 55 | 56 | /** The modifier key hold to multiply scroll speed. */ 57 | void setFastScrollModifier(FastScrollModifier value); 58 | 59 | enum FastScrollModifier { 60 | ALT, 61 | CTRL, 62 | SHIFT, 63 | UNDEFINED 64 | } 65 | 66 | /** The scroll speed multiplier used for fast scrolling. */ 67 | void setFastScrollSensitivity(int number); 68 | 69 | /** The font size used to render text. */ 70 | void setFontSize(int number); 71 | 72 | /** The font family used to render text. */ 73 | void setFontFamily(String fontFamily); 74 | 75 | /** The font weight used to render non-bold text. */ 76 | void setFontWeight(int value); 77 | 78 | /** The font weight used to render bold text. */ 79 | void setFontWeightBold(int value); 80 | 81 | /** The spacing in whole pixels between characters.. */ 82 | void setLetterSpacing(int value); 83 | 84 | /** The line height used to render text. */ 85 | void setLineHeight(int value); 86 | 87 | /** Whether to treat option as the meta key. */ 88 | void setMacOptionIsMeta(boolean value); 89 | 90 | /** 91 | * Whether holding a modifier key will force normal selection behavior, regardless of whether the 92 | * terminal is in mouse events mode. This will also prevent mouse events from being emitted by the 93 | * terminal. For example, this allows you to use xterm.js' regular selection inside tmux with 94 | * mouse mode enabled. 95 | */ 96 | void setMacOptionClickForcesSelection(boolean value); 97 | 98 | /** 99 | * The minimum contrast ratio for text in the terminal, setting this will change the foreground 100 | * color dynamically depending on whether the contrast ratio is met. Example values: 101 | * 102 | *
    103 | *
  • 1: The default, do nothing. 104 | *
  • 4.5: Minimum for WCAG AA compliance. 105 | *
  • 7: Minimum for WCAG AAA compliance. 106 | *
  • 21: White on black or black on white. 107 | *
108 | */ 109 | void setMinimumContrastRatio(int value); 110 | 111 | /** The color theme of the terminal. */ 112 | void setTheme(TerminalTheme theme); 113 | 114 | /** 115 | * The type of renderer to use, this allows using the fallback DOM renderer when canvas is too 116 | * slow for the environment. The following features do not work when the DOM renderer is used: 117 | * 118 | *

- Letter spacing - Cursor blink 119 | */ 120 | void setRendererType(RendererType value); 121 | 122 | enum RendererType { 123 | DOM, 124 | CANVAS 125 | } 126 | 127 | /** 128 | * Whether to select the word under the cursor on right click, this is standard behavior in a lot 129 | * of macOS applications. 130 | */ 131 | void setRightClickSelectsWord(boolean value); 132 | 133 | /** 134 | * Whether screen reader support is enabled. When on this will expose supporting elements in the 135 | * DOM to support NVDA on Windows and VoiceOver on macOS. 136 | */ 137 | void setScreenReaderMode(boolean value); 138 | 139 | /** 140 | * The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained 141 | * when lines are scrolled beyond the initial viewport. 142 | */ 143 | void setScrollback(int value); 144 | 145 | /** The scrolling speed multiplier used for adjusting normal scrolling speed. */ 146 | void setScrollSensitivity(int value); 147 | 148 | /** The size of tab stops in the terminal. */ 149 | void setTabStopWidth(int value); 150 | 151 | /** 152 | * A string containing all characters that are considered word separated by the double click to 153 | * select work logic. 154 | */ 155 | void setWordSeparator(String value); 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/ITerminalSelection.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.HasElement; 23 | 24 | /** 25 | * Add selection support to XTerm using arrow keys. 26 | */ 27 | public interface ITerminalSelection extends HasElement { 28 | 29 | /** Sets the command line prompt. */ 30 | default void setKeyboardSelectionEnabled(boolean enabled) { 31 | getElement().setProperty("keyboardSelectionEnabled", enabled); 32 | } 33 | 34 | /** Returns the command line prompt. */ 35 | default boolean getKeyboardSelectionEnabled() { 36 | // the feature is enabled by default 37 | // getProperty defaults to false in case the mixin isn't applied 38 | return getElement().getProperty("keyboardSelectionEnabled", false); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/PreserveStateAddon.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2025 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.flowingcode.vaadin.addons.xterm.utils.StateMemoizer; 23 | import lombok.experimental.Delegate; 24 | 25 | import java.util.Objects; 26 | import java.util.concurrent.CompletableFuture; 27 | 28 | /** 29 | * Add-on which preserves the client-side state when the component is removed 30 | * from the UI then reattached later on. The problem here is that when the 31 | * {@link XTerm} server-side component is detached from the UI, the xterm.js client-side 32 | * component is destroyed along with its state. When the {@link XTerm} component 33 | * is later re-attached to the UI, a new unconfigured xterm.js is created on the 34 | * client-side. 35 | *

36 | * To use this addon, simply create the addon then make sure to call all {@link ITerminal} 37 | * and {@link ITerminalOptions} methods via this addon: 38 | *
 39 |  * final XTerm xterm = new XTerm();
 40 |  * final PreserveStateAddon addon = new PreserveStateAddon(xterm);
 41 |  * addon.writeln("Hello!");
 42 |  * addon.setPrompt("$ ");
 43 |  * addon.writePrompt();
 44 |  * 
45 | */ 46 | public class PreserveStateAddon extends TerminalAddon 47 | implements ITerminal, ITerminalOptions { 48 | 49 | /** 50 | * The xterm to delegate all calls to. 51 | */ 52 | private final XTerm xterm; 53 | /** 54 | * Remembers everything that was printed into the xterm and what the user typed in. 55 | */ 56 | private final StringBuilder scrollbackBuffer = new StringBuilder(); 57 | /** 58 | * All commands are properly applied before the first attach; they're just 59 | * not preserved after subsequent detach/attach. 60 | */ 61 | private boolean wasDetachedOnce = false; 62 | /** 63 | * Used to re-apply all options to the xterm after it has been reattached back to the UI. 64 | * Otherwise, the options would not be applied to the client-side xterm.js component. 65 | */ 66 | private final StateMemoizer optionsMemoizer; 67 | 68 | /** 69 | * Delegate all option setters through this delegate, which is the {@link #optionsMemoizer} proxy. 70 | * That will allow us to re-apply the settings when the xterm is re-attached. 71 | *

72 | * For example, calling {@link ITerminalOptions#setBellSound(String)} 73 | * on this addon will pass through the call to this delegate, which in turn passes 74 | * the call to {@link #optionsMemoizer} which remembers the call and passes 75 | * it to {@link #xterm}. 76 | *

77 | * After the xterm.js is re-attached, we simply call {@link StateMemoizer#apply()} 78 | * to apply all changed setters again to xterm.js, to make sure xterm.js is 79 | * configured. 80 | */ 81 | @Delegate 82 | private final ITerminalOptions optionsDelegate; 83 | 84 | public PreserveStateAddon(XTerm xterm) { 85 | super(xterm); 86 | this.xterm = Objects.requireNonNull(xterm); 87 | optionsMemoizer = new StateMemoizer(xterm, ITerminalOptions.class); 88 | optionsDelegate = (ITerminalOptions) optionsMemoizer.getProxy(); 89 | xterm.addAttachListener(e -> { 90 | if (wasDetachedOnce) { 91 | optionsMemoizer.apply(); 92 | xterm.write(scrollbackBuffer.toString()); 93 | xterm.writePrompt(); 94 | } 95 | }); 96 | xterm.addDetachListener(e -> wasDetachedOnce = true); 97 | xterm.addLineListener(e -> { 98 | // add the prompt to the scrollback buffer 99 | scrollbackBuffer.append(xterm.getPrompt()); 100 | // also make sure that any user input ends up in the scrollback buffer. 101 | scrollbackBuffer.append(e.getLine()); 102 | scrollbackBuffer.append('\n'); 103 | }); 104 | } 105 | 106 | @Override 107 | public void blur() { 108 | xterm.blur(); 109 | } 110 | 111 | @Override 112 | public void focus() { 113 | xterm.focus(); 114 | } 115 | 116 | @Override 117 | public CompletableFuture hasSelection() { 118 | return xterm.hasSelection(); 119 | } 120 | 121 | @Override 122 | public CompletableFuture getSelection() { 123 | return xterm.getSelection(); 124 | } 125 | 126 | @Override 127 | public void clearSelection() { 128 | xterm.clearSelection(); 129 | } 130 | 131 | @Override 132 | public void select(int column, int row, int length) { 133 | xterm.select(column, row, length); 134 | } 135 | 136 | @Override 137 | public void selectAll() { 138 | xterm.selectAll(); 139 | } 140 | 141 | @Override 142 | public void selectLines(int start, int end) { 143 | xterm.selectLines(start, end); 144 | } 145 | 146 | @Override 147 | public void scrollLines(int amount) { 148 | xterm.scrollLines(amount); 149 | } 150 | 151 | @Override 152 | public void scrollPages(int pageCount) { 153 | xterm.scrollPages(pageCount); 154 | } 155 | 156 | @Override 157 | public void scrollToTop() { 158 | xterm.scrollToTop(); 159 | } 160 | 161 | @Override 162 | public void scrollToBottom() { 163 | xterm.scrollToBottom(); 164 | } 165 | 166 | @Override 167 | public void scrollToLine(int line) { 168 | xterm.scrollToLine(line); 169 | } 170 | 171 | @Override 172 | public void clear() { 173 | xterm.clear(); 174 | scrollbackBuffer.delete(0, scrollbackBuffer.length()); 175 | } 176 | 177 | @Override 178 | public void write(String data) { 179 | xterm.write(data); 180 | scrollbackBuffer.append(data); 181 | } 182 | 183 | @Override 184 | public void writeln(String data) { 185 | xterm.writeln(data); 186 | scrollbackBuffer.append(data); 187 | scrollbackBuffer.append('\n'); 188 | } 189 | 190 | @Override 191 | public void paste(String data) { 192 | xterm.paste(data); 193 | } 194 | 195 | @Override 196 | public void refresh(int start, int end) { 197 | xterm.refresh(start, end); 198 | } 199 | 200 | @Override 201 | public void reset() { 202 | xterm.reset(); 203 | scrollbackBuffer.delete(0, scrollbackBuffer.length()); 204 | } 205 | 206 | @Override 207 | public void resize(int columns, int rows) { 208 | xterm.resize(columns, rows); 209 | } 210 | 211 | /** 212 | * {@link ITerminalConsole#setPrompt(String)} 213 | */ 214 | public void setPrompt(String prompt) { 215 | xterm.setPrompt(prompt); 216 | } 217 | 218 | /** 219 | * {@link ITerminalConsole#getPrompt()} 220 | */ 221 | public String getPrompt() { 222 | return xterm.getPrompt(); 223 | } 224 | 225 | public void writePrompt() { 226 | xterm.writePrompt(); 227 | } 228 | 229 | public String getScrollbackBuffer() { 230 | return scrollbackBuffer.toString(); 231 | } 232 | 233 | public XTerm getXTerm() { 234 | return xterm; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/TerminalAddon.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2025 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import java.io.Serializable; 23 | import java.util.Objects; 24 | 25 | /** 26 | * Represents an abstract base class for server-side add-ons designed to extend or modify the 27 | * functionality of an {@link XTermBase} terminal instance. 28 | *

29 | * Concrete add-on implementations should subclass this class to provide specific features. Each 30 | * add-on is tightly coupled with a specific {@code XTermBase} instance, allowing it to interact 31 | * with and enhance that terminal. 32 | *

33 | * 34 | * @author Javier Godoy / Flowing Code S.A. 35 | */ 36 | @SuppressWarnings("serial") 37 | public abstract class TerminalAddon implements Serializable { 38 | 39 | /** 40 | * Constructs a new {@code TerminalAddon} and associates it with the provided {@link XTermBase} 41 | * instance. 42 | * 43 | * @param xterm the {@code XTermBase} instance to which this add-on will be attached 44 | * @throws NullPointerException if the provided {@code xterm} is {@code null} 45 | */ 46 | protected TerminalAddon(XTermBase xterm) { 47 | Objects.requireNonNull(xterm); 48 | xterm.registerServerSideAddon(this); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/TerminalHistory.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.ComponentUtil; 23 | import com.vaadin.flow.component.Key; 24 | import com.vaadin.flow.shared.Registration; 25 | import java.io.IOException; 26 | import java.io.ObjectInputStream; 27 | import java.io.Serializable; 28 | import java.util.ArrayList; 29 | import java.util.Collections; 30 | import java.util.Iterator; 31 | import java.util.LinkedList; 32 | import java.util.List; 33 | import java.util.ListIterator; 34 | import java.util.Objects; 35 | import java.util.Optional; 36 | import java.util.function.Predicate; 37 | 38 | /** Manages a command history buffer for {@link XTerm}. */ 39 | @SuppressWarnings("serial") 40 | public class TerminalHistory implements Serializable { 41 | 42 | private LinkedList history = new LinkedList<>(); 43 | 44 | private transient ListIterator iterator; 45 | 46 | private final XTermBase terminal; 47 | 48 | private List registrations; 49 | 50 | private String lastRet; 51 | 52 | private String initialLine; 53 | 54 | private Integer maxSize; 55 | 56 | protected TerminalHistory(T terminal) { 57 | if (TerminalHistory.of(terminal) != null) { 58 | throw new IllegalArgumentException("The terminal already has a history"); 59 | } 60 | this.terminal = terminal; 61 | ComponentUtil.setData(terminal, TerminalHistory.class, this); 62 | } 63 | 64 | /** Returns the command history of the terminal. */ 65 | public static TerminalHistory of(T xterm) { 66 | return ComponentUtil.getData(xterm, TerminalHistory.class); 67 | } 68 | 69 | /** Adds support for command history to the given terminal. */ 70 | public static void extend(XTerm xterm) { 71 | TerminalHistory history = new TerminalHistory(xterm); 72 | history.setEnabled(true); 73 | } 74 | 75 | /** 76 | * Set the number of elements to retain. If {@code null} the history is unbounded. 77 | * 78 | * @throws IllegalArgumentException if the argument is negative. 79 | */ 80 | public void setMaxSize(Integer maxSize) { 81 | if (maxSize != null && maxSize < 0) { 82 | throw new IllegalArgumentException(); 83 | } 84 | this.maxSize = maxSize; 85 | iterator = null; 86 | 87 | if (maxSize != null) { 88 | while (history.size() > maxSize) { 89 | history.remove(0); 90 | } 91 | } 92 | } 93 | 94 | /** Sets the enabled state of the history. */ 95 | public void setEnabled(boolean enabled) { 96 | if (!enabled && registrations != null) { 97 | registrations.forEach(Registration::remove); 98 | registrations = null; 99 | } else if (enabled && registrations == null) { 100 | registrations = new ArrayList<>(); 101 | registrations.add(((ITerminalConsole) terminal).addLineListener(ev -> { 102 | add(ev.getLine()); 103 | resetIterator(); 104 | })); 105 | registrations.add(terminal.addCustomKeyListener(ev -> handleArrowUp(), Key.ARROW_UP)); 106 | registrations.add(terminal.addCustomKeyListener(ev -> handleArrowDown(), Key.ARROW_DOWN)); 107 | } 108 | } 109 | 110 | /** Gets the enabled state of the history. */ 111 | public boolean isEnabled() { 112 | return registrations != null; 113 | } 114 | 115 | private void handleArrowUp() { 116 | if (initialLine == null) { 117 | ((ITerminalConsole) terminal).getCurrentLine().thenAccept(currentLine -> { 118 | initialLine = currentLine; 119 | write(previous()); 120 | }); 121 | } else { 122 | write(previous()); 123 | } 124 | } 125 | 126 | private void handleArrowDown() { 127 | write(next()); 128 | } 129 | 130 | 131 | private void write(String line) { 132 | if (line != null) { 133 | // erase logical line, cursor home in logical line, cursor horizontal absolute 134 | String prompt = ((ITerminalConsole) terminal).getPrompt(); 135 | terminal.write("\033[<2K\033[ listIterator() { 141 | if (iterator == null) { 142 | iterator = history.listIterator(history.size()); 143 | } 144 | return iterator; 145 | } 146 | 147 | private Iterator forwardIterator() { 148 | return listIterator(); 149 | } 150 | 151 | private Iterator reverseIterator() { 152 | if (iterator == null) { 153 | iterator = history.listIterator(history.size()); 154 | } 155 | return new Iterator() { 156 | @Override 157 | public boolean hasNext() { 158 | return iterator.hasPrevious(); 159 | } 160 | 161 | @Override 162 | public String next() { 163 | return iterator.previous(); 164 | } 165 | }; 166 | } 167 | 168 | 169 | /** Add a line to the history */ 170 | public void add(String line) { 171 | line = line.trim(); 172 | if (!line.isEmpty()) { 173 | history.add(Objects.requireNonNull(line)); 174 | resetIterator(); 175 | if (maxSize != null && history.size() > maxSize) { 176 | history.removeLast(); 177 | } 178 | } 179 | } 180 | 181 | private void resetIterator() 182 | { 183 | lastRet = null; 184 | iterator = null; 185 | } 186 | 187 | private Optional find(Iterator iterator, Predicate predicate) { 188 | while (iterator.hasNext()) { 189 | String line = iterator.next(); 190 | if (predicate.test(line) && !line.equals(lastRet)) { 191 | return Optional.of(line); 192 | } 193 | } 194 | return Optional.empty(); 195 | } 196 | 197 | private String previous() { 198 | return find(reverseIterator(), line -> true).orElse(null); 199 | } 200 | 201 | private String next() { 202 | return find(forwardIterator(), line -> true).orElseGet(() -> { 203 | String result = initialLine; 204 | initialLine = null; 205 | return result; 206 | }); 207 | } 208 | 209 | /** Clears the history. */ 210 | public void clear() { 211 | history.clear(); 212 | } 213 | 214 | /** Returns the lines in the history. */ 215 | public List getLines() { 216 | return Collections.unmodifiableList(history); 217 | } 218 | 219 | private void writeObject(java.io.ObjectOutputStream out) throws IOException { 220 | int cursor = Optional.ofNullable(iterator).map(ListIterator::nextIndex).orElse(history.size()); 221 | out.defaultWriteObject(); 222 | out.writeInt(cursor); 223 | } 224 | 225 | private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { 226 | in.defaultReadObject(); 227 | int cursor = in.readInt(); 228 | if (cursor != history.size()) { 229 | iterator = history.listIterator(cursor); 230 | } 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/TerminalTheme.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import elemental.json.Json; 23 | import elemental.json.JsonObject; 24 | import elemental.json.JsonValue; 25 | import java.lang.reflect.Field; 26 | import java.lang.reflect.UndeclaredThrowableException; 27 | import java.util.Optional; 28 | import lombok.AccessLevel; 29 | import lombok.AllArgsConstructor; 30 | import lombok.NoArgsConstructor; 31 | import lombok.With; 32 | import lombok.experimental.FieldDefaults; 33 | 34 | /** The color theme of the terminal. */ 35 | @FieldDefaults(level = AccessLevel.PRIVATE) 36 | @With 37 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 38 | @NoArgsConstructor 39 | public final class TerminalTheme { 40 | 41 | /** Set the default foreground color */ 42 | String foreground; 43 | 44 | /** Set the default background color */ 45 | String background; 46 | 47 | /** Set the cursor color */ 48 | String cursor; 49 | 50 | /** Set the accent color of the cursor (fg color for a block cursor) */ 51 | String cursorAccent; 52 | 53 | /** Set the selection background color (can be transparent) */ 54 | String selection; 55 | 56 | /** ANSI black (eg. `\x1b[30m`) */ 57 | String black; 58 | 59 | /** ANSI red (eg. `\x1b[31m`) */ 60 | String red; 61 | 62 | /** ANSI green (eg. `\x1b[32m`) */ 63 | String green; 64 | 65 | /** ANSI yellow (eg. `\x1b[33m`) */ 66 | String yellow; 67 | 68 | /** ANSI blue (eg. `\x1b[34m`) */ 69 | String blue; 70 | 71 | /** ANSI magenta (eg. `\x1b[35m`) */ 72 | String magenta; 73 | 74 | /** ANSI cyan (eg. `\x1b[36m`) */ 75 | String cyan; 76 | 77 | /** ANSI white (eg. `\x1b[37m`) */ 78 | String white; 79 | 80 | /** ANSI bright black (eg. `\x1b[1;30m`) */ 81 | String brightBlack; 82 | 83 | /** ANSI bright red (eg. `\x1b[1;31m`) */ 84 | String brightRed; 85 | 86 | /** ANSI bright green (eg. `\x1b[1;32m`) */ 87 | String brightGreen; 88 | 89 | /** ANSI bright yellow (eg. `\x1b[1;33m`) */ 90 | String brightYellow; 91 | 92 | /** ANSI bright blue (eg. `\x1b[1;34m`) */ 93 | String brightBlue; 94 | 95 | /** ANSI bright magenta (eg. `\x1b[1;35m`) */ 96 | String brightMagenta; 97 | 98 | /** ANSI bright cyan (eg. `\x1b[1;36m`) */ 99 | String brightCyan; 100 | 101 | /** ANSI bright white (eg. `\x1b[1;37m`) */ 102 | String brightWhite; 103 | 104 | JsonObject asJsonObject() { 105 | JsonObject obj = Json.createObject(); 106 | for (Field field : this.getClass().getDeclaredFields()) { 107 | try { 108 | obj.put( 109 | field.getName(), 110 | Optional.ofNullable((String) field.get(this)) 111 | .map(Json::create) 112 | .orElseGet(Json::createNull)); 113 | } catch (Exception e) { 114 | throw new UndeclaredThrowableException(e); 115 | } 116 | } 117 | return obj; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/XTerm.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.Tag; 23 | import com.vaadin.flow.component.dependency.JsModule; 24 | 25 | /** Server-side component for the XTerm component. */ 26 | @SuppressWarnings("serial") 27 | @Tag("fc-xterm") 28 | @JsModule("./fc-xterm/xterm.ts") 29 | public class XTerm extends XTermBase 30 | implements ITerminalFit, ITerminalConsole, ITerminalClipboard { 31 | 32 | public XTerm() { 33 | setInsertMode(true); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/XTermBase.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2025 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.Component; 23 | import com.vaadin.flow.component.HasEnabled; 24 | import com.vaadin.flow.component.HasSize; 25 | import com.vaadin.flow.component.Key; 26 | import com.vaadin.flow.component.KeyLocation; 27 | import com.vaadin.flow.component.KeyModifier; 28 | import com.vaadin.flow.component.dependency.CssImport; 29 | import com.vaadin.flow.component.dependency.JsModule; 30 | import com.vaadin.flow.component.dependency.NpmPackage; 31 | import com.vaadin.flow.dom.DomEventListener; 32 | import com.vaadin.flow.dom.Element; 33 | import com.vaadin.flow.server.Command; 34 | import com.vaadin.flow.shared.Registration; 35 | import elemental.json.Json; 36 | import elemental.json.JsonArray; 37 | import elemental.json.JsonNull; 38 | import elemental.json.JsonObject; 39 | import elemental.json.JsonValue; 40 | import java.io.Serializable; 41 | import java.lang.reflect.InvocationHandler; 42 | import java.lang.reflect.InvocationTargetException; 43 | import java.lang.reflect.Method; 44 | import java.lang.reflect.ParameterizedType; 45 | import java.lang.reflect.Proxy; 46 | import java.util.ArrayList; 47 | import java.util.Arrays; 48 | import java.util.HashSet; 49 | import java.util.LinkedList; 50 | import java.util.List; 51 | import java.util.Locale; 52 | import java.util.Set; 53 | import java.util.concurrent.CompletableFuture; 54 | import java.util.function.Function; 55 | import java.util.stream.Collectors; 56 | import java.util.stream.IntStream; 57 | import lombok.experimental.Delegate; 58 | 59 | /** Server-side component for the XTerm component. */ 60 | @SuppressWarnings("serial") 61 | @NpmPackage(value = "xterm", version = "5.1.0") 62 | @JsModule("./fc-xterm/xterm-element.ts") 63 | @CssImport("xterm/css/xterm.css") 64 | public abstract class XTermBase extends Component 65 | implements ITerminal, ITerminalOptions, HasSize, HasEnabled { 66 | 67 | @Delegate private ITerminalOptions terminalOptionsProxy; 68 | 69 | @Delegate private ITerminal terminalProxy; 70 | 71 | private List deferredCommands; 72 | 73 | private final List addons = new ArrayList<>(); 74 | 75 | private class ProxyInvocationHandler implements InvocationHandler, Serializable { 76 | 77 | @Override 78 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 79 | if (method.getDeclaringClass() == Object.class) { 80 | return method.invoke(this, args); 81 | } 82 | 83 | Function mapping = getResultTypeMapper(method); 84 | 85 | String name = method.getName(); 86 | CompletableFuture result = invoke(mapping!=null, name, args); 87 | 88 | if (mapping != null) { 89 | return result.thenApply(json -> (json instanceof JsonNull) ? null : mapping.apply(json)); 90 | } else { 91 | return null; 92 | } 93 | } 94 | 95 | private Function getResultTypeMapper(Method method) { 96 | if (method.getReturnType() == Void.TYPE) { 97 | return null; 98 | } else if (method.getReturnType() == CompletableFuture.class) { 99 | ParameterizedType type = (ParameterizedType) method.getGenericReturnType(); 100 | Class resultType = (Class) type.getActualTypeArguments()[0]; 101 | 102 | if (resultType == Void.class) { 103 | return x -> null; 104 | } else if (resultType == String.class) { 105 | return JsonValue::asString; 106 | } else if (resultType == Boolean.class) { 107 | return JsonValue::asBoolean; 108 | } else if (resultType == Integer.class) { 109 | return json -> (int) json.asNumber(); 110 | } else { 111 | throw new AbstractMethodError(method.toString()); 112 | } 113 | } else { 114 | throw new AbstractMethodError(method.toString()); 115 | } 116 | } 117 | 118 | private CompletableFuture invoke(boolean hasResult, String name, Object[] args) { 119 | if (name.startsWith("set") && args.length == 1) { 120 | name = name.substring("set".length()); 121 | name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1); 122 | Serializable arg; 123 | if (args[0] instanceof Enum) { 124 | arg = ((Enum) args[0]).name().toLowerCase(Locale.ENGLISH); 125 | } else if (args[0] instanceof TerminalTheme) { 126 | arg = ((TerminalTheme) args[0]).asJsonObject(); 127 | } else { 128 | arg = (Serializable) args[0]; 129 | } 130 | if (name.equals("bellStyle") || name.equals("bellSound")) { 131 | return executeJs(false, "this[$0]=$1", name, arg); 132 | } else { 133 | return executeJs(false, "this.terminal.options[$0]=$1", name, arg); 134 | } 135 | } else if (args == null || args.length == 0) { 136 | return executeJs(hasResult, "return this.terminal[$0]()", name); 137 | } else if (args.length == 1) { 138 | return executeJs(hasResult, "return this.terminal[$0]($1)", name, (Serializable) args[0]); 139 | } else { 140 | Serializable[] sargs = new Serializable[args.length]; 141 | System.arraycopy(args, 0, sargs, 0, args.length); 142 | String expr = 143 | IntStream.rangeClosed(1, args.length) 144 | .mapToObj(i -> "$" + i) 145 | .collect(Collectors.joining(",")); 146 | return executeJs(hasResult, "return this.terminal[$0](" + expr + ")", name, sargs); 147 | } 148 | } 149 | } 150 | 151 | private static final Class optionsProxyClass; 152 | 153 | static { 154 | optionsProxyClass = 155 | Proxy.getProxyClass( 156 | XTermBase.class.getClassLoader(), ITerminal.class, ITerminalOptions.class); 157 | } 158 | 159 | /** Constructs a new instance of {@code XTerm} */ 160 | public XTermBase() { 161 | // initialize delegate proxies 162 | try { 163 | Object proxy = 164 | optionsProxyClass 165 | .getConstructor(InvocationHandler.class) 166 | .newInstance(new ProxyInvocationHandler()); 167 | terminalProxy = (ITerminal) proxy; 168 | terminalOptionsProxy = (ITerminalOptions) proxy; 169 | } catch (InstantiationException 170 | | IllegalAccessException 171 | | InvocationTargetException 172 | | NoSuchMethodException e) { 173 | throw new RuntimeException(e); 174 | } 175 | 176 | // the inner
must be in the light DOM 177 | Element div = new Element("div"); 178 | div.setAttribute("slot", "terminal-container"); 179 | div.getStyle().set("width", "100%"); 180 | div.getStyle().set("height", "100%"); 181 | getElement().appendChild(div); 182 | 183 | deferredCommands = new LinkedList<>(); 184 | 185 | Registration r[] = new Registration[1]; 186 | r[0] = getElement().addEventListener("terminal-initialized", ev -> afterInitialization()); 187 | } 188 | 189 | private void afterInitialization() { 190 | if (deferredCommands != null) { 191 | List deferredCommands = this.deferredCommands; 192 | this.deferredCommands = null; 193 | deferredCommands.forEach(Command::execute); 194 | } 195 | } 196 | 197 | private CompletableFuture executeJs(boolean hasResult, String expression, Serializable... parameters) { 198 | if (!hasResult) { 199 | executeJs(expression, parameters); 200 | return CompletableFuture.completedFuture(null); 201 | } else if (deferredCommands == null) { 202 | return getElement().executeJs(expression, parameters).toCompletableFuture(); 203 | } else { 204 | throw new IllegalStateException("Terminal is not initialized"); 205 | } 206 | } 207 | 208 | protected void executeJs(String expression, Serializable... parameters) { 209 | if (deferredCommands == null) { 210 | getElement().executeJs(expression, parameters); 211 | } else { 212 | deferredCommands.add(() -> getElement().executeJs(expression, parameters)); 213 | } 214 | } 215 | 216 | /** 217 | * Add a server-side key listener. This method is equivalent to calling {@link 218 | * #addCustomKeyListener(DomEventListener, Key, KeyModifier...)} with a {@link KeyLocation} of 219 | * {@code null}. 220 | * 221 | * @return a registration for the listener. 222 | */ 223 | public Registration addCustomKeyListener( 224 | DomEventListener listener, Key key, KeyModifier... modifiers) { 225 | return addCustomKeyListener(listener, key, null, modifiers); 226 | } 227 | 228 | /** 229 | * Add a server-side key listener. 230 | * 231 | * @return a registration for the listener. 232 | */ 233 | public Registration addCustomKeyListener( 234 | DomEventListener listener, Key key, KeyLocation location, KeyModifier... modifiers) { 235 | return addCustomKeyListener( 236 | listener, key.getKeys(), location, new HashSet<>(Arrays.asList(modifiers))); 237 | } 238 | 239 | private Registration addCustomKeyListener( 240 | DomEventListener listener, 241 | List keys, 242 | KeyLocation location, 243 | Set modifiers) { 244 | JsonArray array = Json.createArray(); 245 | for (String key : keys) { 246 | JsonObject json = Json.createObject(); 247 | json.put("code", key); 248 | if (location != null) { 249 | json.put("location", location.getLocation()); 250 | } 251 | json.put("ctrlKey", modifiers.contains(KeyModifier.CONTROL)); 252 | json.put("altKey", modifiers.contains(KeyModifier.ALT)); 253 | json.put("metaKey", modifiers.contains(KeyModifier.META)); 254 | json.put("shiftKey", modifiers.contains(KeyModifier.SHIFT)); 255 | array.set(array.length(), json); 256 | } 257 | 258 | StringBuilder sb = new StringBuilder(""); 259 | sb.append( 260 | IntStream.range(0, array.length()) 261 | .mapToObj(i -> array.getObject(i).getString("code")) 262 | .map(s -> String.format("'%s'", s)) 263 | .collect(Collectors.joining(",", "[", "]"))) 264 | .append(".includes(event.detail.code)"); 265 | 266 | JsonObject json = array.getObject(0); 267 | if (location != null) { 268 | sb.append(" && event.detail.location=").append(json.getNumber("location")); 269 | } 270 | 271 | for (String modifier : Arrays.asList("ctrlKey", "altKey", "metaKey", "shiftKey")) { 272 | sb.append(json.getBoolean(modifier) ? "&& " : "&& !") 273 | .append("event.detail.") 274 | .append(modifier); 275 | } 276 | 277 | String filter = sb.toString(); 278 | Registration r = getElement().addEventListener("CustomKey", listener).setFilter(filter); 279 | executeJs("this.registerCustomKeyListener($0)", json); 280 | return r::remove; 281 | } 282 | 283 | @Override 284 | public void setEnabled(boolean enabled) { 285 | HasEnabled.super.setEnabled(enabled); 286 | } 287 | 288 | /** 289 | * Retrieves a registered server-side add-on instance of a specific type. 290 | *

291 | * Example usage: 292 | *

293 | * 294 | *
{@code
295 |    * MySpecificAddon addon = terminal.getAddon(MySpecificAddon.class);
296 |    * if (addon != null) {
297 |    *   addon.doSomethingSpecific();
298 |    * }
299 |    * }
300 | * 301 | * @param the type of the add-on to retrieve. This is inferred from the {@code clazz} 302 | * parameter. 303 | * @param clazz the {@code Class} object representing the type of the add-on to retrieve. Must not 304 | * be {@code null}. 305 | * @return the registered add-on instance that is of the specified {@code Class}, or 306 | * {@code null} if no such add-on is found 307 | * @throws NullPointerException if {@code clazz} is {@code null} 308 | */ 309 | public T getAddon(Class clazz) { 310 | return addons.stream().filter(clazz::isInstance).map(clazz::cast).findFirst().orElse(null); 311 | } 312 | 313 | /** 314 | * Registers a server-side add-on with this terminal instance. This method is called by the add-on 315 | * itself during its construction. 316 | * 317 | * @param addon the add-on to register. Must not be {@code null}. 318 | * @throws NullPointerException if {@code addon} is {@code null} 319 | * @throws IllegalStateException if an add-on of the same class as the provided {@code addon} is 320 | * already registered with this terminal instance 321 | */ 322 | final void registerServerSideAddon(T addon) { 323 | if (getAddon(addon.getClass()) != null) { 324 | throw new IllegalStateException("Addon already registered: " + addon.getClass().getName()); 325 | } 326 | addons.add(addon); 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/com/flowingcode/vaadin/addons/xterm/utils/StateMemoizer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.utils; 21 | 22 | import java.io.Serializable; 23 | import java.lang.reflect.InvocationTargetException; 24 | import java.lang.reflect.Method; 25 | import java.lang.reflect.Proxy; 26 | import java.util.Arrays; 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | import java.util.Objects; 30 | 31 | /** 32 | * Remembers the values passed to all setters. At any time you can reapply 33 | * those calls by using {@link #apply()}. 34 | */ 35 | public class StateMemoizer implements Serializable { 36 | /** 37 | * Remember all calls to all setters; also remember what args were passed to those setters. 38 | */ 39 | private final Map setterCalls = new HashMap<>(); 40 | /** 41 | * Pass-through the setters here. 42 | */ 43 | private final Object delegate; 44 | /** 45 | * Setters invoked on this proxy will have the args remembered; the methods invocations 46 | * will then be passed on to {@link #delegate}. 47 | */ 48 | private final Object proxy; 49 | 50 | /** 51 | * Creates the memoizer. Remember to invoke interface methods via {@link #getProxy()} in 52 | * order for this to work. 53 | * @param delegate pass-through the setters here. 54 | * @param interfaces used to create the memoizing proxy. 55 | */ 56 | public StateMemoizer(Object delegate, Class... interfaces) { 57 | this.delegate = Objects.requireNonNull(delegate); 58 | proxy = Proxy.newProxyInstance(interfaces[0].getClassLoader(), interfaces, (proxy, method, args) -> { 59 | if (method.getName().startsWith("set") && args.length == 1) { 60 | // remember the state 61 | setterCalls.put(method.getName(), (Serializable) args[0]); 62 | } 63 | return method.invoke(delegate, args); 64 | }); 65 | } 66 | 67 | /** 68 | * Setters invoked on this proxy will have the args remembered; the methods invocations 69 | * will then be passed on to {@link #delegate}. 70 | * @return the proxy, not null. 71 | */ 72 | public Object getProxy() { 73 | return proxy; 74 | } 75 | 76 | /** 77 | * Calls all setters again on {@link #delegate}. 78 | */ 79 | public void apply() { 80 | setterCalls.forEach((k, v) -> { 81 | final Method method = Arrays.stream(delegate.getClass().getMethods()).filter(it -> it.getName().equals(k)).findAny().get(); 82 | try { 83 | method.invoke(delegate, v); 84 | } catch (IllegalAccessException | InvocationTargetException e) { 85 | throw new RuntimeException(e); 86 | } 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/VAADIN/package.properties: -------------------------------------------------------------------------------- 1 | vaadin.allowed-packages=com.flowingcode 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm-clipboard-mixin.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | import { Terminal } from 'xterm' 21 | import { TerminalMixin, TerminalAddon} from './xterm-element'; 22 | 23 | interface IClipboardMixin extends TerminalMixin { 24 | useSystemClipboard: String; 25 | pasteWithRightClick: Boolean; 26 | pasteWithMiddleClick: Boolean; 27 | copySelection: Boolean; 28 | } 29 | 30 | class ClipboardAddon extends TerminalAddon { 31 | 32 | activateCallback(terminal: Terminal): void { 33 | let _internalClipboard : string | undefined = undefined; 34 | 35 | //function (String) : void 36 | let writeText = (s: string) => { 37 | _internalClipboard = s; 38 | if (this.$.useSystemClipboard== 'readwrite' || this.$.useSystemClipboard=='write') { 39 | try { 40 | navigator.clipboard.writeText(s); 41 | } catch(error) { 42 | console.error(error); 43 | } 44 | } 45 | } 46 | 47 | //function () : Promise 48 | let readText = () => { 49 | if (this.$.useSystemClipboard=='readwrite') { 50 | try { 51 | return navigator.clipboard.readText(); 52 | } catch(error) { 53 | console.error(error); 54 | } 55 | } 56 | return Promise.resolve(_internalClipboard); 57 | } 58 | 59 | let onEvent = (event: string, listener:any) => { 60 | terminal.element!.addEventListener(event, listener); 61 | return {dispose : () => terminal.element!.removeEventListener(event, listener)}; 62 | }; 63 | 64 | let paste = (text: string | undefined) => { 65 | if (text!==undefined) { 66 | terminal.paste(text); 67 | } 68 | }; 69 | 70 | let initializer = ()=>{ 71 | //paste with right click 72 | this._disposables.push(onEvent('contextmenu', (ev:any) => { 73 | if (this.$.pasteWithRightClick && !terminal.options.rightClickSelectsWord) { 74 | ev.preventDefault(); 75 | if (_internalClipboard!==undefined) readText().then(paste); 76 | } 77 | })); 78 | 79 | //paste with middle click 80 | this._disposables.push(onEvent('auxclick', (ev:any) => { 81 | if (this.$.pasteWithMiddleClick && ev.button == 1) { 82 | if (_internalClipboard!==undefined) readText().then(paste); 83 | } 84 | })); 85 | }; 86 | 87 | this.$node.addEventListener('terminal-initialized', initializer); 88 | 89 | this._disposables = [ 90 | {dispose: () => this.$node.removeEventListener('terminal-initialized', initializer)}, 91 | 92 | //ctrl-C ctrl-V 93 | this.$node.customKeyEventHandlers.register({key:'c', ctrlKey:true}, undefined), 94 | this.$node.customKeyEventHandlers.register({key:'v', ctrlKey:true}, undefined), 95 | 96 | //copy selection 97 | terminal.onSelectionChange(() => { 98 | if (this.$.copySelection && terminal.hasSelection()) { 99 | writeText(terminal.getSelection()); 100 | } 101 | }), 102 | 103 | 104 | ]; 105 | 106 | } 107 | 108 | } 109 | 110 | type Constructor = new (...args: any[]) => T; 111 | export function XTermClipboardMixin>(Base: TBase) { 112 | return class XTermClipboardMixin extends Base implements IClipboardMixin { 113 | 114 | useSystemClipboard: String = "write"; 115 | pasteWithRightClick: Boolean; 116 | pasteWithMiddleClick: Boolean; 117 | copySelection: Boolean; 118 | 119 | connectedCallback() { 120 | super.connectedCallback(); 121 | 122 | let addon = new ClipboardAddon(); 123 | addon.$=this; 124 | this.node.terminal.loadAddon(addon); 125 | } 126 | 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm-console-mixin.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | import { Terminal } from 'xterm' 21 | import { TerminalMixin, TerminalAddon } from './xterm-element'; 22 | 23 | export interface IConsoleMixin extends TerminalMixin { 24 | escapeEnabled: Boolean; 25 | insertMode: Boolean; 26 | readonly currentLine: string; 27 | prompt: string; 28 | } 29 | 30 | class ConsoleAddon extends TerminalAddon { 31 | 32 | __yPrompt : Number = -1; 33 | 34 | get currentLine() : string { 35 | let inputHandler = ((this.$core) as any)._inputHandler; 36 | let buffer = inputHandler._bufferService.buffer; 37 | let range = buffer.getWrappedRangeForLine(buffer.y + buffer.ybase); 38 | let line = ""; 39 | for (let i=range.first; i<=range.last; i++) { 40 | line += buffer.lines.get(i).translateToString(); 41 | } 42 | line = line.replace(/\s+$/,""); 43 | if (this.$.prompt && this.__yPrompt==range.first) line = line.substring(this.$.prompt.length); 44 | return line; 45 | } 46 | 47 | activateCallback(terminal: Terminal): void { 48 | 49 | var inputHandler = ((this.$core) as any)._inputHandler; 50 | 51 | let promptLength = () => this.$.prompt ? this.$.prompt.length : 0; 52 | 53 | let scanEOL = (function() { 54 | let buffer = this._bufferService.buffer; 55 | let col = this._bufferService.buffer.lines.get(buffer.ybase+buffer.y).getTrimmedLength(); 56 | this._activeBuffer.x = Math.min(col, terminal.cols); 57 | }).bind(inputHandler); 58 | 59 | let cursorForwardWrapped = (function(params: any = [0]) { 60 | let buffer = this._bufferService.buffer; 61 | let x = buffer.x; 62 | let y = buffer.y; 63 | for (let i=(params[0] || 1); i>0; --i) { 64 | let apply=false; 65 | if (buffer.x>=this._bufferService.cols-1) { 66 | let next = buffer.lines.get(buffer.y+buffer.ybase+1); 67 | if (next && next.isWrapped) { 68 | this.cursorNextLine({params:1}); 69 | apply=true; 70 | } else if (i==1) { 71 | buffer.x=this._bufferService.cols; 72 | apply=true; 73 | } 74 | } else if (buffer.x < buffer.lines.get(buffer.y+buffer.ybase).getTrimmedLength()) { 75 | this.cursorForward({params:[1]}); 76 | apply=true; 77 | } 78 | 79 | if (!apply) { 80 | buffer.x=x; 81 | buffer.y=y; 82 | return false; 83 | } 84 | } 85 | return true; 86 | }).bind(inputHandler); 87 | 88 | let cursorBackwardWrapped = (function(params : any = [0]) { 89 | let buffer = this._bufferService.buffer; 90 | let x = buffer.x; 91 | let y = buffer.y; 92 | let apply=false; 93 | 94 | for (let i=0; i< (params[0] || 1); i++) { 95 | let line = buffer.lines.get(buffer.y+buffer.ybase); 96 | if (!line.isWrapped && buffer.x< 1+promptLength()) { 97 | break; 98 | } else if (buffer.x>0) { 99 | apply=true; 100 | this.cursorBackward({params:[1]}); 101 | } else if (line.isWrapped) { 102 | apply=true; 103 | this.cursorPrecedingLine({params:[1]}); 104 | scanEOL(); 105 | --this._activeBuffer.x; 106 | } else { 107 | apply=false; 108 | break; 109 | } 110 | } 111 | if (!apply) { 112 | buffer.x=x; 113 | buffer.y=y; 114 | } 115 | return apply; 116 | }).bind(inputHandler); 117 | 118 | let deleteChar = (function(params: any = [0]) { 119 | let buffer = this._bufferService.buffer; 120 | let x = buffer.x; 121 | let y = buffer.y; 122 | for (let j=0; j< (params[0] || 1); j++) { 123 | this.deleteChars({params:[1]}); 124 | let line = buffer.lines.get(buffer.ybase+buffer.y); 125 | let range = buffer.getWrappedRangeForLine(buffer.y + buffer.ybase) 126 | for (let i=buffer.y+buffer.ybase; i ev.shiftKey || ev.altKey || ev.metaKey || ev.ctrlKey; 207 | 208 | function probe_others(callback: ((params: T[]) => void)): ((params: T[]) => boolean) { 209 | // execute callback and also probe for other handlers 210 | return params => {callback(params); return false;} 211 | }; 212 | 213 | this._disposables = [ 214 | terminal.parser.registerCsiHandler({prefix: '<', final: 'H'}, probe_others(cursorHome)), 215 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='Home' && !hasModifiers(ev), ()=> terminal.write('\x1b[ ev.key=='End' && !hasModifiers(ev), ()=> terminal.write('\x1b[ ev.key=='ArrowLeft' && !hasModifiers(ev), ()=> terminal.write('\x1b[ ev.key=='ArrowRight' && !hasModifiers(ev), ()=> terminal.write('\x1b[ ev.key=='Backspace' && !hasModifiers(ev), ()=> terminal.write('\x1b[ ev.key=='Delete' && !hasModifiers(ev), ()=> terminal.write('\x1b[ ev.key=='Insert' && !hasModifiers(ev), ev=>{ 235 | this.$.insertMode = !this.$.insertMode; 236 | }), 237 | 238 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='Enter' && !hasModifiers(ev), ()=>{ 239 | terminal.write('\x1b[ [ 243 | 'ArrowUp', 244 | 'ArrowDown', 245 | 'F1', 'F2', 'F3', 'F4', 'F7', 'F8', 'F9', 'F10', 'F11', 246 | ].includes(ev.key), ev=>{ev.preventDefault(); return false;}), 247 | 248 | this.$node.customKeyEventHandlers.register(ev=> [ 249 | 'Escape' 250 | ].includes(ev.key), () => this.$.escapeEnabled), 251 | 252 | this.$node.customKeyEventHandlers.register(ev=> [ 253 | 'F5', 254 | 'F6', 255 | 'F12' 256 | ].includes(ev.key), undefined), 257 | 258 | terminal.parser.registerCsiHandler({prefix: '<', final: 'N'}, probe_others(linefeed)) 259 | 260 | ]; 261 | 262 | } 263 | 264 | writePrompt() { 265 | if (!this.$.prompt) return; 266 | 267 | let inputHandler = ((this.$core) as any)._inputHandler; 268 | let buffer = inputHandler._bufferService.buffer; 269 | let range = buffer.getWrappedRangeForLine(buffer.y + buffer.ybase); 270 | 271 | let prepare = ""; 272 | let restore = this.$.insertMode ? "\x1b[4h" : "\x1b[4l" 273 | 274 | if (this.__yPrompt == range.first) { 275 | //prompt has been written in this line 276 | prepare+="\x1b[4l"; //Override mode 277 | } else { 278 | //prompt has not been written in this line 279 | this.__yPrompt = range.first; 280 | prepare+="\x1b[s"; //Save cursor position 281 | prepare+="\x1b[4h"; //Insert mode 282 | restore+="\x1b[u"; //Restore cursor position 283 | restore+="\x1b[<"+this.$.prompt.length+"R"; //cursor forward wrapped 284 | } 285 | 286 | prepare+="\x1b[ = new (...args: any[]) => T; 292 | export function XTermConsoleMixin>(Base: TBase) { 293 | return class XTermConsoleMixin extends Base implements IConsoleMixin { 294 | 295 | _consoleAddon? : ConsoleAddon; 296 | escapeEnabled: Boolean; 297 | prompt: string; 298 | 299 | connectedCallback() { 300 | super.connectedCallback(); 301 | 302 | this._consoleAddon = new ConsoleAddon(); 303 | this._consoleAddon.$=this; 304 | this.node.terminal.loadAddon(this._consoleAddon); 305 | } 306 | 307 | get insertMode(): Boolean { 308 | return this.node.terminal.modes.insertMode; 309 | } 310 | 311 | set insertMode(value: Boolean) { 312 | if (value) { 313 | this.node.terminal.write('\x1b[4h\x1b[3 q'); 314 | } else { 315 | this.node.terminal.write('\x1b[4l\x1b[2 q'); 316 | } 317 | } 318 | 319 | get currentLine() : string { 320 | return this._consoleAddon!.currentLine; 321 | } 322 | 323 | writePrompt() { 324 | //execute writePrompt with blocking semantics 325 | this.node.terminal.write('', ()=>this._consoleAddon!.writePrompt()); 326 | } 327 | 328 | } 329 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm-element.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | import { LitElement, html, TemplateResult } from 'lit-element'; 21 | 22 | import { Terminal, ITerminalAddon, IDisposable } from 'xterm'; 23 | import { ITerminal } from 'xterm/src/browser/Types'; 24 | 25 | type integer = number; 26 | type KeyboardEventHandler = (event: KeyboardEvent) => void; 27 | type KeyboardEventPredicate = (event: KeyboardEvent) => boolean; 28 | 29 | interface CustomKey { 30 | key?: string; 31 | code?: string; 32 | location?: number; 33 | ctrlKey?: boolean; 34 | altKey?: boolean; 35 | metaKey?: boolean; 36 | shiftKey?: boolean; 37 | } 38 | 39 | interface CustomKeyEventHandler { 40 | predicate:KeyboardEventPredicate; 41 | handle?: KeyboardEventHandler; 42 | } 43 | 44 | interface CustomKeyEventHandlerRegistryDisposable extends IDisposable { 45 | id: integer; 46 | unshift: () => void; 47 | } 48 | 49 | //sparse array of CustomKeyEventHandler 50 | class CustomKeyEventHandlerRegistry { 51 | private handlers: CustomKeyEventHandler[] = []; 52 | private indexes: number[] = []; 53 | private next:integer=0; 54 | 55 | register(customKey: CustomKey, handle?: (event: KeyboardEvent) => void): CustomKeyEventHandlerRegistryDisposable; 56 | register(predicate: KeyboardEventPredicate, handle?: KeyboardEventHandler) : CustomKeyEventHandlerRegistryDisposable; 57 | register(arg: CustomKey | KeyboardEventPredicate, handle?: KeyboardEventHandler) : CustomKeyEventHandlerRegistryDisposable { 58 | let predicate : KeyboardEventPredicate; 59 | if ((typeof arg) === 'object') { 60 | let customKey = arg as any; 61 | //(C) T.J. Crowder cc-by-sa 4.0 62 | //https://stackoverflow.com/a/35430956/1297272 63 | let properties = ['key', 'code', 'location', 'ctrlKey', 'altKey', 'metaKey', 'shiftKey']; 64 | predicate = (ev : KeyboardEvent) => properties.every(property => customKey[property] === (ev as any)[property] || customKey[property]===undefined); 65 | } else { 66 | predicate = arg as KeyboardEventPredicate; 67 | } 68 | 69 | const id = this.next++; 70 | if (!handle) handle = () => {}; 71 | this.handlers[id] = {predicate, handle}; 72 | this.indexes.push(id); 73 | return {id, dispose : () => this.remove(id), unshift : () => this.unshift(id)}; 74 | } 75 | 76 | private unshift(id: integer) : void { 77 | const i= this.indexes.indexOf(id); 78 | if (i>=0) { 79 | this.indexes.splice(i, 1); 80 | this.indexes.unshift(id); 81 | } 82 | } 83 | 84 | remove(id: integer) : void { 85 | delete this.handlers[id]; 86 | const i= this.indexes.indexOf(id); 87 | if (i>=0) this.indexes.splice(i, 1); 88 | } 89 | 90 | handle(context: XTermElement, ev: KeyboardEvent) : boolean { 91 | //invoke all the applicable handlers for event 92 | 93 | let listeners : CustomKeyEventHandler[] = []; 94 | 95 | for(var i=0;i { 106 | stopImmediatePropagation(); 107 | immediatePropagationStopped=true; 108 | }; 109 | 110 | let handled = listeners.length>0; 111 | for (var i=0;i implements ITerminalAddon { 134 | public $ : T; 135 | protected $node: XTermElement; 136 | protected $core: ITerminal; 137 | protected _disposables : IDisposable[]; 138 | 139 | public readonly activate = (terminal: Terminal): void => { 140 | this.$node = this.$.node; 141 | this.$core = (terminal as any)._core as ITerminal; 142 | this._disposables=[]; 143 | this.activateCallback(terminal); 144 | }; 145 | 146 | public readonly dispose = (): void => { 147 | this._disposables.forEach(d => d.dispose()); 148 | }; 149 | 150 | protected abstract activateCallback(terminal: Terminal) : void; 151 | } 152 | 153 | 154 | export class XTermElement extends LitElement implements TerminalMixin { 155 | 156 | terminal: Terminal; 157 | disabled: boolean = false; 158 | node: XTermElement; 159 | 160 | bellSound: string; 161 | bellStyle: 'none' | 'sound' 162 | 163 | customKeyEventHandlers: CustomKeyEventHandlerRegistry; 164 | 165 | addons : Object = {}; 166 | 167 | render(): TemplateResult { 168 | return html` 169 | 170 | `; 171 | } 172 | 173 | constructor() { 174 | super(); 175 | this.customKeyEventHandlers = new CustomKeyEventHandlerRegistry(); 176 | this.terminal = new Terminal(); 177 | this.node = this; 178 | 179 | //https://gist.github.com/literallylara/7ece1983fab47365108c47119afb51c7 180 | //(C) Lara Sophie Schütt 2016, CC0 181 | for(var i=44100*0.1,d="";i--;)d+=String.fromCharCode(~~((Math.sin(i/44100*2*Math.PI*800)+1)*128)); 182 | this.bellSound = "data:Audio/WAV;base64,"+btoa("RIFFdataWAVEfmt "+atob("EAAAAAEAAQBErAAARKwAAAEACABkYXRh/////w==")+d); 183 | this.bellStyle = 'none'; 184 | } 185 | 186 | _onData(e:string) : void { 187 | //this._onData(e)); 199 | 200 | term.onBell(() => { 201 | if (this.bellStyle == 'sound') { 202 | new Audio(this.bellSound).play(); 203 | } 204 | }); 205 | 206 | term.attachCustomKeyEventHandler(ev => { 207 | if (ev.type!=='keydown') return false; 208 | return !this.customKeyEventHandlers.handle(this, ev); 209 | }); 210 | } 211 | 212 | disconnectedCallback() { 213 | this.terminal.dispose(); 214 | this.terminal = new Terminal(); 215 | super.disconnectedCallback(); 216 | } 217 | 218 | _slotchange() { 219 | let slot = this.shadowRoot?.querySelector("slot[name='terminal-container']") as HTMLSlotElement; 220 | requestAnimationFrame(()=>{ 221 | this.terminal.open(slot.assignedNodes()[0] as HTMLElement); 222 | this.dispatchEvent(new CustomEvent('terminal-initialized')); 223 | }); 224 | } 225 | 226 | updated(changedProps : any) { 227 | if (changedProps.has('disabled')) { 228 | this._disabledChanged(this.disabled); 229 | } 230 | } 231 | 232 | _disabledChanged(disabled: boolean) { 233 | this.terminal.options.disableStdin=disabled; 234 | this.terminal.write(disabled?"\x1b[?25l":"\x1b[?25h"); 235 | } 236 | 237 | registerCustomKeyListener(customKey: CustomKey) : integer { 238 | let handler : KeyboardEventHandler = (ev: KeyboardEvent) => (ev as any).requestCustomEvent = true; 239 | return this.customKeyEventHandlers.register(customKey, handler).id; 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm-fit-mixin.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | import { Terminal, IDisposable } from 'xterm' 21 | import { TerminalMixin } from './xterm-element'; 22 | import { FitAddon as FitAddonBase } from 'xterm-addon-fit/src/FitAddon'; 23 | 24 | 25 | interface FitMixin extends TerminalMixin { 26 | fitOnResize: boolean; 27 | }; 28 | 29 | class FitAddon extends FitAddonBase { 30 | _disposables : IDisposable[]; 31 | $ : FitMixin; 32 | 33 | constructor() { 34 | super(); 35 | const super_fit = this.fit.bind(this); 36 | this.fit = () => { 37 | super_fit(); 38 | requestAnimationFrame(()=>this.__unsetWidth()); 39 | }; 40 | } 41 | 42 | __unsetWidth() { 43 | let viewport = (this.$.node.terminal as any)._core.viewport; 44 | if (viewport) viewport._viewportElement.style.width='unset'; 45 | } 46 | 47 | activate(terminal: Terminal): void { 48 | super.activate(terminal); 49 | 50 | let _fitOnResize = () => { 51 | if (this.$.fitOnResize) this.fit(); 52 | } 53 | 54 | window.addEventListener('resize', _fitOnResize); 55 | 56 | this._disposables = []; 57 | this._disposables.push({dispose : () => { 58 | window.removeEventListener('resize', _fitOnResize); 59 | }}); 60 | } 61 | 62 | dispose(): void { 63 | this._disposables.forEach(d => d.dispose()); 64 | super.dispose(); 65 | } 66 | } 67 | 68 | type Constructor = new (...args: any[]) => T; 69 | export function XTermFitMixin>(Base: TBase) { 70 | return class XTermFitMixin extends Base { 71 | 72 | _fitAddon? : FitAddon; 73 | fitOnResize: boolean = true; 74 | 75 | connectedCallback() { 76 | super.connectedCallback(); 77 | 78 | let addon = new FitAddon(); 79 | addon.$=this; 80 | this.node.terminal.loadAddon(addon); 81 | 82 | this._fitAddon = addon; 83 | this.fit(); 84 | 85 | this.node.addEventListener("terminal-initialized", () => this.fit()); 86 | } 87 | 88 | fit() { 89 | this._fitAddon!.proposeDimensions(); 90 | window.setTimeout(()=>{ 91 | try { 92 | this._fitAddon!.fit(); 93 | } catch (e) { 94 | console.warn(e); 95 | } 96 | }); 97 | } 98 | 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm-insertfix-mixin.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | 21 | import { Terminal } from 'xterm' 22 | import { TerminalMixin, TerminalAddon } from './xterm-element'; 23 | 24 | type PrintHandler = (data: Uint32Array, start: number, end: number) => void; 25 | 26 | class InsertFixAddon extends TerminalAddon { 27 | 28 | activateCallback(terminal: Terminal): void { 29 | const core = this.$core as any; 30 | const oldPrintHandler = core._inputHandler._parser._printHandler; 31 | const newPrintHandler = (data: Uint32Array, start: number, end: number) => this.printHandler(core._inputHandler, data, start, end, oldPrintHandler); 32 | (this.$core as any)._inputHandler._parser.setPrintHandler(newPrintHandler); 33 | this._disposables.push({dispose : () => {core._inputHandler._parser._printHandler = oldPrintHandler;}}); 34 | } 35 | 36 | printHandler(inputHandler: any, data: Uint32Array, start: number, end: number, printHandler : PrintHandler): void { 37 | const wraparoundMode = inputHandler._coreService.decPrivateModes.wraparound; 38 | const insertMode = inputHandler._coreService.modes.insertMode; 39 | 40 | if (insertMode && wraparoundMode) { 41 | const buffer = inputHandler._bufferService.buffer; 42 | const bufferRow = buffer.lines.get(buffer.y + buffer.ybase); 43 | const printedLength = end-start; 44 | let trimmedLength = bufferRow.getTrimmedLength(); 45 | 46 | //If the inserted characters would overflow the current liner 47 | if (buffer.x!=trimmedLength && trimmedLength+printedLength > bufferRow.length) { 48 | let range = buffer.getWrappedRangeForLine(buffer.y + buffer.ybase) 49 | range.first = buffer.y + buffer.ybase; 50 | 51 | let src; 52 | if (range.first==range.last) { 53 | src = bufferRow; 54 | } else { 55 | src = buffer.lines.get(range.last); 56 | trimmedLength = src.getTrimmedLength(); 57 | } 58 | 59 | //If the inserted characters would overflow the last line in wrapped range 60 | if (trimmedLength+printedLength > src.length) { 61 | //Then wrap the next row 62 | if (range.last == buffer._rows - 1) { 63 | inputHandler._bufferService.scroll(inputHandler._eraseAttrData(), true); 64 | } 65 | const dst = buffer.lines.get(range.last+1); 66 | dst.isWrapped = true; 67 | dst.copyCellsFrom(src, trimmedLength-printedLength, 0, printedLength); 68 | inputHandler._dirtyRowTracker.markDirty(buffer.y+1); 69 | } 70 | 71 | //Allocate space for the characters to be inserted 72 | //Wrap-move characters in page memory to the next line 73 | for (let y=range.last;y>range.first;y--) { 74 | let dst = src; 75 | src= buffer.lines.get(y-1); 76 | dst.insertCells(0, printedLength, buffer.getNullCell(inputHandler._eraseAttrData())); 77 | dst.copyCellsFrom(src, buffer._cols-printedLength, 0, printedLength); 78 | inputHandler._dirtyRowTracker.markDirty(y); 79 | } 80 | } 81 | } 82 | 83 | printHandler(data, start, end); 84 | } 85 | 86 | } 87 | 88 | type Constructor = new (...args: any[]) => T; 89 | export function XTermInsertFixMixin>(Base: TBase) { 90 | return class XTermInsertFixMixin extends Base { 91 | connectedCallback() { 92 | super.connectedCallback(); 93 | let addon = new InsertFixAddon(); 94 | addon.$=this; 95 | this.node.terminal.loadAddon(addon); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm-selection-mixin.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Selection Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | import { Terminal } from 'xterm' 21 | import { TerminalMixin, TerminalAddon } from './xterm-element'; 22 | import { IConsoleMixin } from './xterm-console-mixin'; 23 | 24 | interface ISelectionMixin extends TerminalMixin { 25 | keyboardSelectionEnabled: boolean; 26 | } 27 | 28 | class SelectionAddon extends TerminalAddon { 29 | 30 | __selectionLength: number; 31 | __selectionAnchor?: number; 32 | __selectionRight: boolean = true; 33 | 34 | activateCallback(terminal: Terminal): void { 35 | 36 | var inputHandler = ((this.$core) as any)._inputHandler; 37 | 38 | let resetSelection = () => { 39 | this.__selectionAnchor = undefined; 40 | } 41 | 42 | let clearSelection = () => { 43 | if (!this.$.keyboardSelectionEnabled) return; 44 | resetSelection(); 45 | terminal.clearSelection(); 46 | } 47 | 48 | let ensureSelection = () => { 49 | if (this.__selectionAnchor === undefined) { 50 | let buffer = inputHandler._bufferService.buffer; 51 | this.__selectionAnchor = buffer.y * terminal.cols + buffer.x;; 52 | this.__selectionLength = 0; 53 | } 54 | }; 55 | 56 | let moveSelection = (dx:number, dy:number=0) => { 57 | if (!this.$.keyboardSelectionEnabled) return; 58 | ensureSelection(); 59 | 60 | let newSelectionLength = this.__selectionLength; 61 | if (this.__selectionRight) { 62 | newSelectionLength += dx + dy * terminal.cols; 63 | } else { 64 | newSelectionLength -= dx + dy * terminal.cols; 65 | } 66 | 67 | if (newSelectionLength<0) { 68 | newSelectionLength = -newSelectionLength; 69 | this.__selectionRight = !this.__selectionRight; 70 | } 71 | 72 | let newSelectionStart = this.__selectionAnchor!; 73 | if (!this.__selectionRight) { 74 | newSelectionStart -= newSelectionLength; 75 | } 76 | 77 | if (newSelectionStart<0) return; 78 | if (newSelectionStart+newSelectionLength>terminal.buffer.active.length*terminal.cols) return; 79 | 80 | let row = Math.floor(newSelectionStart / terminal.cols); 81 | let col = newSelectionStart % terminal.cols; 82 | 83 | this.__selectionLength = newSelectionLength; 84 | terminal.select(col,row,newSelectionLength); 85 | }; 86 | 87 | let selectLeft = () => moveSelection(-1); 88 | let selectRight = () => moveSelection(+1); 89 | let selectUp = () => moveSelection(0,-1); 90 | let selectDown = () => moveSelection(0,+1); 91 | 92 | let promptLength = () => (this.$ as unknown as IConsoleMixin).prompt?.length || 0; 93 | 94 | let selectHome = () => { 95 | if (!this.$.keyboardSelectionEnabled) return; 96 | 97 | let buffer = (terminal.buffer.active as any)._buffer; 98 | let range = buffer.getWrappedRangeForLine(buffer.ybase+buffer.y); 99 | 100 | let pos = terminal.getSelectionPosition() || {start: {y: buffer.ybase+buffer.y, x: buffer.x}}; 101 | 102 | resetSelection(); 103 | ensureSelection(); 104 | let dx = range.first * terminal.cols - this.__selectionAnchor!; 105 | if (pos.start.y != range.first || pos.start.x != promptLength()) { 106 | dx+= promptLength(); 107 | } 108 | 109 | moveSelection(dx); 110 | }; 111 | 112 | let selectEnd = () => { 113 | if (!this.$.keyboardSelectionEnabled) return; 114 | 115 | let buffer = (terminal.buffer.active as any)._buffer; 116 | let range = buffer.getWrappedRangeForLine(buffer.ybase+buffer.y); 117 | 118 | resetSelection(); 119 | ensureSelection(); 120 | moveSelection(range.last * terminal.cols + buffer.lines.get(range.last).getTrimmedLength() - this.__selectionAnchor!); 121 | }; 122 | 123 | let deleteSelection = (ev: KeyboardEvent) => { 124 | if (!this.$.keyboardSelectionEnabled) return; 125 | if (this.__selectionAnchor!==undefined) { 126 | let buffer = (terminal.buffer.active as any)._buffer; 127 | let range = buffer.getWrappedRangeForLine(buffer.ybase+buffer.y); 128 | let pos = terminal.getSelectionPosition(); 129 | if (pos && pos.start.y>=range.first && pos.end.y<=range.last) { 130 | if (!this.__selectionRight) { 131 | //cursor backward wrapped 132 | terminal.write("\x1b[<" + this.__selectionLength + "L"); 133 | } 134 | //delete characters wrapped 135 | terminal.write("\x1b[<" + this.__selectionLength + "D"); 136 | ev.stopImmediatePropagation(); 137 | } 138 | } 139 | clearSelection(); 140 | }; 141 | 142 | let hasModifiers = (ev:KeyboardEvent) => ev.shiftKey || ev.altKey || ev.metaKey || ev.ctrlKey; 143 | 144 | this._disposables = [ 145 | (this.$core as any).coreService.onUserInput(() => clearSelection), 146 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowLeft' && ev.shiftKey, selectLeft), 147 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowRight' && ev.shiftKey, selectRight), 148 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowUp' && ev.shiftKey, selectUp), 149 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowDown' && ev.shiftKey, selectDown), 150 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='Home' && ev.shiftKey, selectHome), 151 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='End' && ev.shiftKey, selectEnd), 152 | 153 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowLeft' && !hasModifiers(ev), clearSelection), 154 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowRight' && !hasModifiers(ev), clearSelection), 155 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowUp' && !hasModifiers(ev), clearSelection), 156 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='ArrowDown' && !hasModifiers(ev), clearSelection), 157 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='Home' && !hasModifiers(ev), clearSelection), 158 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='End' && !hasModifiers(ev), clearSelection), 159 | 160 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='Delete' && !hasModifiers(ev), deleteSelection), 161 | this.$node.customKeyEventHandlers.register(ev=> ev.key=='Backspace' && !hasModifiers(ev), deleteSelection), 162 | ]; 163 | 164 | } 165 | 166 | } 167 | 168 | type Constructor = new (...args: any[]) => T; 169 | export function XTermSelectionMixin>(Base: TBase) { 170 | return class XTermSelectionMixin extends Base implements ISelectionMixin { 171 | 172 | keyboardSelectionEnabled: boolean = true; 173 | 174 | connectedCallback() { 175 | super.connectedCallback(); 176 | let addon = new SelectionAddon(); 177 | addon.$=this; 178 | this.node.terminal.loadAddon(addon); 179 | } 180 | 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/frontend/fc-xterm/xterm.ts: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | import {customElement} from 'lit/decorators/custom-element.js'; 21 | 22 | import { XTermElement } from './xterm-element'; 23 | import { XTermClipboardMixin } from './xterm-clipboard-mixin'; 24 | import { XTermConsoleMixin } from './xterm-console-mixin'; 25 | import { XTermFitMixin } from './xterm-fit-mixin'; 26 | import { XTermInsertFixMixin } from './xterm-insertfix-mixin'; 27 | import { XTermSelectionMixin } from './xterm-selection-mixin'; 28 | 29 | @customElement('fc-xterm') 30 | export class XTermComponent extends XTermInsertFixMixin(XTermClipboardMixin(XTermConsoleMixin(XTermSelectionMixin(XTermFitMixin(XTermElement))))) { 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/com.flowingcode.addons/xterm-console/proxy-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "interfaces":["com.flowingcode.vaadin.addons.xterm.ITerminal","com.flowingcode.vaadin.addons.xterm.ITerminalOptions"] 4 | } 5 | ] 6 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/com.flowingcode.addons/xterm-console/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"com.flowingcode.vaadin.addons.xterm.ITerminal", 4 | "queryAllDeclaredMethods":true, 5 | "allPublicMethods": true 6 | }, 7 | { 8 | "name":"com.flowingcode.vaadin.addons.xterm.ITerminalOptions", 9 | "queryAllDeclaredMethods":true, 10 | "allPublicMethods": true 11 | }, 12 | { 13 | "name":"com.flowingcode.vaadin.addons.xterm.TerminalTheme", 14 | "allDeclaredFields": true 15 | } 16 | ] -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons; 21 | 22 | import com.vaadin.flow.component.html.Div; 23 | import com.vaadin.flow.router.RouterLayout; 24 | 25 | @SuppressWarnings("serial") 26 | public class DemoLayout extends Div implements RouterLayout { 27 | 28 | public DemoLayout() { 29 | setSizeFull(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/DemoView.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | 21 | package com.flowingcode.vaadin.addons.xterm; 22 | 23 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 24 | import com.vaadin.flow.router.BeforeEnterEvent; 25 | import com.vaadin.flow.router.BeforeEnterObserver; 26 | import com.vaadin.flow.router.Route; 27 | 28 | @SuppressWarnings("serial") 29 | @Route("") 30 | public class DemoView extends VerticalLayout implements BeforeEnterObserver { 31 | 32 | @Override 33 | public void beforeEnter(BeforeEnterEvent event) { 34 | event.forwardTo(XtermDemoView.class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/PreserveStateAddonTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.vaadin.flow.component.Component; 23 | import com.vaadin.flow.component.ComponentUtil; 24 | import org.junit.Test; 25 | 26 | import static org.junit.Assert.assertEquals; 27 | 28 | public class PreserveStateAddonTest { 29 | @Test 30 | public void smoke() { 31 | new PreserveStateAddon(new XTerm()); 32 | } 33 | 34 | @Test 35 | public void writeGoesToScrollbackBuffer() { 36 | final PreserveStateAddon addon = new PreserveStateAddon(new XTerm()); 37 | addon.write("foo"); 38 | addon.writeln("bar"); 39 | addon.write("baz"); 40 | assertEquals("foobar\nbaz", addon.getScrollbackBuffer()); 41 | } 42 | 43 | @Test 44 | public void promptGoesToScrollbackBufferAfterSubmit() { 45 | XTerm xterm = new XTerm(); 46 | final PreserveStateAddon addon = new PreserveStateAddon(xterm); 47 | Component component = xterm.getElement().getComponent().get(); 48 | 49 | addon.setPrompt("a> "); 50 | addon.writePrompt(); 51 | ComponentUtil.fireEvent(component, new ITerminalConsole.LineEvent(xterm, true, "bar")); 52 | 53 | addon.setPrompt("b> "); 54 | addon.writePrompt(); 55 | ComponentUtil.fireEvent(component, new ITerminalConsole.LineEvent(xterm, true, "baz")); 56 | 57 | addon.setPrompt("c> "); 58 | addon.writePrompt(); 59 | // Does not get added to buffer 60 | 61 | assertEquals("a> bar\nb> baz\n", addon.getScrollbackBuffer()); 62 | } 63 | 64 | @Test 65 | public void clearClearsScrollbackBuffer() { 66 | final PreserveStateAddon addon = new PreserveStateAddon(new XTerm()); 67 | addon.writeln("bar"); 68 | addon.clear(); 69 | assertEquals("", addon.getScrollbackBuffer()); 70 | } 71 | 72 | @Test 73 | public void resetClearsScrollbackBuffer() { 74 | final PreserveStateAddon addon = new PreserveStateAddon(new XTerm()); 75 | addon.writeln("bar"); 76 | addon.reset(); 77 | assertEquals("", addon.getScrollbackBuffer()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/XtermDemoView.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm; 21 | 22 | import com.flowingcode.vaadin.addons.DemoLayout; 23 | import com.flowingcode.vaadin.addons.GithubLink; 24 | import com.flowingcode.vaadin.addons.xterm.ITerminalClipboard.UseSystemClipboard; 25 | import com.flowingcode.vaadin.addons.xterm.ITerminalOptions.BellStyle; 26 | import com.flowingcode.vaadin.addons.xterm.ITerminalOptions.CursorStyle; 27 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 28 | import com.vaadin.flow.router.Route; 29 | import java.time.LocalDate; 30 | import java.time.LocalTime; 31 | import java.time.format.DateTimeFormatter; 32 | import java.time.temporal.ChronoUnit; 33 | 34 | @SuppressWarnings("serial") 35 | @Route(value = "xterm", layout = DemoLayout.class) 36 | @GithubLink("https://github.com/FlowingCode/XTermConsoleAddon") 37 | public class XtermDemoView extends VerticalLayout { 38 | 39 | private XTerm xterm; 40 | 41 | public XtermDemoView() { 42 | setSizeFull(); 43 | setPadding(false); 44 | getElement().getStyle().set("background", "black"); 45 | 46 | xterm = new XTerm(); 47 | xterm.setPrompt("[user@xterm ~]$ "); 48 | 49 | xterm.writeln("xterm add-on by Flowing Code S.A.\n\n"); 50 | xterm.writeln("Commands: time, date, beep, color, history, prompt\n"); 51 | xterm.writePrompt(); 52 | 53 | xterm.setCursorBlink(true); 54 | xterm.setCursorStyle(CursorStyle.UNDERLINE); 55 | xterm.setBellStyle(BellStyle.SOUND); 56 | 57 | xterm.setSizeFull(); 58 | 59 | xterm.setCopySelection(true); 60 | xterm.setUseSystemClipboard(UseSystemClipboard.READWRITE); 61 | xterm.setPasteWithMiddleClick(true); 62 | xterm.setPasteWithRightClick(true); 63 | 64 | TerminalHistory.extend(xterm); 65 | 66 | xterm.addLineListener( 67 | ev -> { 68 | String[] line = ev.getLine().toLowerCase().split("\\s+",2); 69 | switch (line[0]) { 70 | case "time": 71 | xterm.writeln( 72 | LocalTime.now() 73 | .truncatedTo(ChronoUnit.SECONDS) 74 | .format(DateTimeFormatter.ISO_TIME)); 75 | break; 76 | case "date": 77 | xterm.writeln(LocalDate.now().toString()); 78 | break; 79 | case "beep": 80 | xterm.write("\u0007"); 81 | break; 82 | case "color": 83 | if (line.length>1) { 84 | if (line[1].equals("on")) { 85 | xterm.setTheme( 86 | new TerminalTheme() 87 | .withBackground("rgb(103,195,228)") 88 | .withForeground("rgb(0,0,0)")); 89 | break; 90 | } else if (line[1].equals("off")) { 91 | xterm.setTheme(new TerminalTheme()); 92 | break; 93 | } 94 | } 95 | xterm.writeln("color on: use TI-99/4A palette"); 96 | xterm.writeln("color off: use default palette"); 97 | break; 98 | case "history": 99 | showHistory(); 100 | break; 101 | case "prompt": 102 | if (line.length == 1) { 103 | xterm.writeln("Write prompt off for disabling the prompt"); 104 | xterm.writeln("Write prompt for setting the prompt"); 105 | } else if (line[1].equals("off")) { 106 | xterm.setPrompt(null); 107 | } else { 108 | xterm.setPrompt(line[1].trim() + " "); 109 | } 110 | break; 111 | default: 112 | if (!ev.getLine().trim().isEmpty()) { 113 | xterm.writeln("Unknown command: " + line[0]); 114 | } 115 | } 116 | 117 | xterm.writePrompt(); 118 | }); 119 | 120 | xterm.focus(); 121 | xterm.fit(); 122 | add(xterm); 123 | } 124 | 125 | private void showHistory() { 126 | int index = 1; 127 | StringBuilder sb = new StringBuilder(); 128 | for (String line : TerminalHistory.of(xterm).getLines()) { 129 | sb.append(String.format("%5s %s\n", index++, line)); 130 | } 131 | xterm.write(sb.toString()); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/AbstractViewTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import com.vaadin.testbench.ScreenshotOnFailureRule; 23 | import com.vaadin.testbench.TestBench; 24 | import com.vaadin.testbench.parallel.ParallelTest; 25 | import io.github.bonigarcia.wdm.WebDriverManager; 26 | import org.junit.Before; 27 | import org.junit.BeforeClass; 28 | import org.junit.Rule; 29 | import org.openqa.selenium.chrome.ChromeDriver; 30 | 31 | /** 32 | * Base class for ITs 33 | * 34 | *

The tests use Chrome driver (see pom.xml for integration-tests profile) to run integration 35 | * tests on a headless Chrome. If a property {@code test.use .hub} is set to true, {@code 36 | * AbstractViewTest} will assume that the TestBench test is running in a CI environment. In order to 37 | * keep the this class light, it makes certain assumptions about the CI environment (such as 38 | * available environment variables). It is not advisable to use this class as a base class for you 39 | * own TestBench tests. 40 | * 41 | *

To learn more about TestBench, visit Vaadin TestBench. 43 | */ 44 | public abstract class AbstractViewTest extends ParallelTest { 45 | private static final int SERVER_PORT = 8080; 46 | 47 | private final String route; 48 | 49 | @Rule public ScreenshotOnFailureRule rule = new ScreenshotOnFailureRule(this, true); 50 | 51 | public AbstractViewTest() { 52 | this(""); 53 | } 54 | 55 | protected AbstractViewTest(String route) { 56 | this.route = route; 57 | } 58 | 59 | @BeforeClass 60 | public static void setupClass() { 61 | WebDriverManager.chromedriver().setup(); 62 | } 63 | 64 | @Override 65 | @Before 66 | public void setup() throws Exception { 67 | if (isUsingHub()) { 68 | super.setup(); 69 | } else { 70 | setDriver(TestBench.createDriver(new ChromeDriver())); 71 | } 72 | getDriver().get(getURL(route)); 73 | } 74 | 75 | /** 76 | * Returns deployment host name concatenated with route. 77 | * 78 | * @return URL to route 79 | */ 80 | private static String getURL(String route) { 81 | return String.format("http://%s:%d/%s", getDeploymentHostname(), SERVER_PORT, route); 82 | } 83 | 84 | /** Property set to true when running on a test hub. */ 85 | private static final String USE_HUB_PROPERTY = "test.use.hub"; 86 | 87 | /** 88 | * Returns whether we are using a test hub. This means that the starter is running tests in 89 | * Vaadin's CI environment, and uses TestBench to connect to the testing hub. 90 | * 91 | * @return whether we are using a test hub 92 | */ 93 | private static boolean isUsingHub() { 94 | return Boolean.TRUE.toString().equals(System.getProperty(USE_HUB_PROPERTY)); 95 | } 96 | 97 | /** 98 | * If running on CI, get the host name from environment variable HOSTNAME 99 | * 100 | * @return the host name 101 | */ 102 | private static String getDeploymentHostname() { 103 | return isUsingHub() ? System.getenv("HOSTNAME") : "localhost"; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/ClipboardFeatureIT.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import static org.hamcrest.Matchers.is; 23 | import static org.junit.Assert.assertThat; 24 | import java.util.List; 25 | import org.junit.Test; 26 | import org.openqa.selenium.interactions.Actions; 27 | 28 | public class ClipboardFeatureIT extends AbstractViewTest { 29 | 30 | private static int[] intArray(Object obj) { 31 | return ((List) obj).stream().mapToInt(i -> ((Long) i).intValue()).toArray(); 32 | } 33 | 34 | @Test 35 | public void testFeature() { 36 | XTermElement term = $(XTermElement.class).first(); 37 | 38 | term.setPrompt(null); 39 | term.write("\\x1bcTEXT"); 40 | 41 | int[] size = 42 | intArray( 43 | term 44 | .executeScript( 45 | "return [this.clientWidth, this.clientHeight]")); 46 | 47 | term.setUseSystemClipboard(false); 48 | 49 | new Actions(driver) 50 | .moveToElement(term, -size[0] / 2, -size[1] / 2 + 10) 51 | .clickAndHold() 52 | .moveByOffset(100, 0) 53 | .release() 54 | .perform(); 55 | 56 | new Actions(driver).contextClick().perform(); 57 | assertThat(term.currentLine(), is("TEXTTEXT")); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/ConsoleFeatureIT.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import static com.flowingcode.vaadin.addons.xterm.integration.XTermTestUtils.makeFullLine; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.isEmptyString; 25 | import static org.junit.Assert.assertThat; 26 | import org.junit.Test; 27 | import org.openqa.selenium.Keys; 28 | 29 | public class ConsoleFeatureIT extends AbstractViewTest { 30 | 31 | @Test 32 | public void testWriteWrappedLine() throws InterruptedException { 33 | XTermElement term = $(XTermElement.class).first(); 34 | Position home = term.cursorPosition(); 35 | String text = makeFullLine(term, true) + "Z"; 36 | term.sendKeys(text); 37 | assertThat(term.currentLine(), is(text)); 38 | assertThat(term.cursorPosition(), is(new Position(1, home.y + 1))); 39 | assertThat(term.lineAtOffset(0), is("Z")); 40 | } 41 | 42 | @Test 43 | public void testFeature() throws InterruptedException { 44 | // navigation with keyboard 45 | XTermElement term = $(XTermElement.class).first(); 46 | 47 | Position pos = term.cursorPosition(); 48 | 49 | term.sendKeys("HELLO"); 50 | assertThat(term.currentLine(), is("HELLO")); 51 | assertThat(term.cursorPosition(), is(pos.plus(5, 0))); 52 | 53 | term.sendKeys(Keys.ARROW_LEFT); 54 | assertThat(term.cursorPosition(), is(pos.plus(4, 0))); 55 | 56 | term.sendKeys(Keys.ARROW_RIGHT); 57 | assertThat(term.cursorPosition(), is(pos.plus(5, 0))); 58 | 59 | term.sendKeys(Keys.HOME); 60 | assertThat(term.cursorPosition(), is(pos.plus(0, 0))); 61 | 62 | term.sendKeys(Keys.END); 63 | assertThat(term.cursorPosition(), is(pos.plus(5, 0))); 64 | 65 | term.sendKeys(Keys.BACK_SPACE); 66 | assertThat(term.currentLine(), is("HELL")); 67 | assertThat(term.cursorPosition(), is(pos.plus(4, 0))); 68 | 69 | term.sendKeys(Keys.HOME, Keys.DELETE); 70 | assertThat(term.currentLine(), is("ELL")); 71 | assertThat(term.cursorPosition(), is(pos.plus(0, 0))); 72 | 73 | term.sendKeys("A"); 74 | assertThat(term.currentLine(), is("AELL")); 75 | 76 | term.sendKeys(Keys.INSERT, "B"); 77 | assertThat(term.currentLine(), is("ABLL")); 78 | 79 | term.sendKeys(Keys.INSERT, "C"); 80 | assertThat(term.currentLine(), is("ABCLL")); 81 | 82 | 83 | // long line 84 | term.sendKeys(Keys.ENTER); 85 | assertThat(term.currentLine(), is("")); 86 | assertThat(term.cursorPosition(), is(pos.advance(0, 1))); 87 | 88 | String prompt = term.lineAtOffset(0); 89 | String text = makeFullLine(term, true); 90 | int cols = text.length(); 91 | 92 | term.sendKeys(text); 93 | term.sendKeys(Keys.HOME); 94 | assertThat(term.cursorPosition(), is(pos)); 95 | assertThat(term.currentLine(), is(text)); 96 | 97 | term.sendKeys("A"); 98 | assertThat(term.lineAtOffset(0), is(prompt + "A" + text.substring(0, cols - 1))); 99 | assertThat(term.lineAtOffset(1), is(text.substring(cols - 1))); 100 | 101 | term.sendKeys("B"); 102 | assertThat(term.lineAtOffset(0), is(prompt + "AB" + text.substring(0, cols - 2))); 103 | assertThat(term.lineAtOffset(1), is(text.substring(cols - 2))); 104 | 105 | term.sendKeys(Keys.END); 106 | assertThat(term.cursorPosition(), is(new Position(2, pos.y + 1))); 107 | 108 | term.sendKeys(Keys.HOME); 109 | assertThat(term.cursorPosition(), is(pos)); 110 | 111 | term.sendKeys(Keys.DELETE); 112 | assertThat(term.lineAtOffset(0), is(prompt + "B" + text.substring(0, cols - 1))); 113 | assertThat(term.lineAtOffset(1), is(text.substring(cols - 1))); 114 | 115 | term.sendKeys(Keys.DELETE); 116 | assertThat(term.lineAtOffset(0), is(prompt + text)); 117 | assertThat(term.lineAtOffset(1), isEmptyString()); 118 | } 119 | 120 | @Test 121 | public void testCsiSequences() throws InterruptedException { 122 | // CSI sequences that implement navigation with keyboard 123 | XTermElement term = $(XTermElement.class).first(); 124 | Position pos = term.cursorPosition(); 125 | 126 | term.sendKeys("HELLO"); 127 | assertThat(term.currentLine(), is("HELLO")); 128 | 129 | // Cursor Home Logical Line 130 | term.write("\u001b[ 0; i--) { 203 | assertThat(term.cursorPosition(), is(home.plus(i - 1, 0).adjust(cols))); 204 | term.write("\u001b[ { 40 | int w = term.getColumnWidth(); 41 | return w != colsBefore ? w : null; 42 | }); 43 | 44 | assertThat(colsAfter * 2, is(lessThanOrEqualTo(colsBefore))); 45 | 46 | getDriver().manage().window().setSize(dimension); 47 | 48 | int colsRestored = waitUntil(driver -> { 49 | int w = term.getColumnWidth(); 50 | return w != colsAfter ? w : null; 51 | }); 52 | 53 | assertThat(colsRestored, is(colsBefore)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/Position.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import lombok.AllArgsConstructor; 23 | import lombok.EqualsAndHashCode; 24 | import lombok.ToString; 25 | 26 | @AllArgsConstructor 27 | @ToString 28 | @EqualsAndHashCode 29 | class Position { 30 | int x, y; 31 | 32 | /**Increments the position by dx,dy.*/ 33 | public Position advance(int dx, int dy) { 34 | this.x+=dx; 35 | this.y+=dy; 36 | return this; 37 | } 38 | 39 | /**Return a new position that is equal to this position plus dx,dy.*/ 40 | public Position plus(int dx, int dy) { 41 | return new Position(this.x+dx, this.y+dy); 42 | } 43 | 44 | /**Return a new position that is equal to this position adjusted by columnWidth.*/ 45 | public Position adjust(int columnWidth) { 46 | int pos = x + y*columnWidth; 47 | return new Position(pos%columnWidth, pos/columnWidth); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/SelectionFeatureIT.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import static com.flowingcode.vaadin.addons.xterm.integration.XTermTestUtils.makeFullLine; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.isEmptyString; 25 | import static org.junit.Assert.assertThat; 26 | import org.junit.Test; 27 | import org.openqa.selenium.Keys; 28 | 29 | public class SelectionFeatureIT extends AbstractViewTest { 30 | 31 | @Test 32 | public void testSelectionFeature() throws InterruptedException { 33 | XTermElement term = $(XTermElement.class).first(); 34 | Position pos; 35 | 36 | term.write("abcd"); 37 | pos = term.cursorPosition(); 38 | 39 | // select left, backspace 40 | term.sendKeys(Keys.SHIFT, Keys.ARROW_LEFT); 41 | assertThat(term.getSelection(), is("d")); 42 | assertThat(term.cursorPosition(), is(pos)); 43 | term.sendKeys(Keys.BACK_SPACE); 44 | assertThat(term.currentLine(), is("abc")); 45 | assertThat(term.cursorPosition(), is(pos.advance(-1, 0))); 46 | 47 | // select left, delete 48 | term.sendKeys(Keys.SHIFT, Keys.ARROW_LEFT); 49 | assertThat(term.getSelection(), is("c")); 50 | assertThat(term.cursorPosition(), is(pos)); 51 | term.sendKeys(Keys.DELETE); 52 | assertThat(term.currentLine(), is("ab")); 53 | assertThat(term.cursorPosition(), is(pos.advance(-1, 0))); 54 | 55 | // select right, delete 56 | term.sendKeys(Keys.HOME); 57 | pos = term.cursorPosition(); 58 | term.sendKeys(Keys.SHIFT, Keys.ARROW_RIGHT); 59 | assertThat(term.getSelection(), is("a")); 60 | assertThat(term.cursorPosition(), is(pos)); 61 | term.sendKeys(Keys.DELETE); 62 | assertThat(term.currentLine(), is("b")); 63 | assertThat(term.cursorPosition(), is(pos)); 64 | 65 | // select right, backspace 66 | term.sendKeys(Keys.SHIFT, Keys.ARROW_RIGHT); 67 | assertThat(term.getSelection(), is("b")); 68 | assertThat(term.cursorPosition(), is(pos)); 69 | term.sendKeys(Keys.BACK_SPACE); 70 | assertThat(term.currentLine(), isEmptyString()); 71 | assertThat(term.cursorPosition(), is(pos)); 72 | 73 | term.write("abcd"); 74 | pos = term.cursorPosition(); 75 | 76 | // select to home, delete 77 | term.sendKeys(Keys.SHIFT, Keys.HOME); 78 | assertThat(term.getSelection(), is("abcd")); 79 | assertThat(term.cursorPosition(), is(pos)); 80 | term.sendKeys(Keys.DELETE); 81 | assertThat(term.currentLine(), isEmptyString()); 82 | assertThat(term.cursorPosition(), is(pos.advance(-4, 0))); 83 | 84 | // select to end, delete 85 | term.write("abcd"); 86 | term.sendKeys(Keys.HOME); 87 | pos = term.cursorPosition(); 88 | term.sendKeys(Keys.SHIFT, Keys.END); 89 | assertThat(term.getSelection(), is("abcd")); 90 | assertThat(term.cursorPosition(), is(pos)); 91 | term.sendKeys(Keys.DELETE); 92 | assertThat(term.currentLine(), isEmptyString()); 93 | assertThat(term.cursorPosition(), is(pos)); 94 | 95 | String text = makeFullLine(term, true) + makeFullLine(term, false) + makeFullLine(term, false); 96 | 97 | // select to home, delete (wrapping) 98 | term.write(text); 99 | assertThat(term.currentLine(), is(text)); 100 | term.sendKeys(Keys.SHIFT, Keys.HOME); 101 | assertThat(term.getSelection(), is(text)); 102 | term.sendKeys(Keys.DELETE); 103 | assertThat(term.currentLine(), isEmptyString()); 104 | 105 | // select to end, delete (wrapping) 106 | term.write(text); 107 | assertThat(term.currentLine(), is(text)); 108 | term.sendKeys(Keys.HOME); 109 | term.sendKeys(Keys.SHIFT, Keys.END); 110 | assertThat(term.getSelection(), is(text)); 111 | term.sendKeys(Keys.DELETE); 112 | assertThat(term.currentLine(), isEmptyString()); 113 | 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/TerminalHistoryIT.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import static org.hamcrest.Matchers.is; 23 | import static org.hamcrest.Matchers.isEmptyString; 24 | import static org.junit.Assert.assertThat; 25 | import org.junit.Test; 26 | import org.openqa.selenium.Keys; 27 | 28 | public class TerminalHistoryIT extends AbstractViewTest { 29 | 30 | @Test 31 | public void testArrowKeys() { 32 | XTermElement term = $(XTermElement.class).first(); 33 | 34 | Position pos = term.cursorPosition(); 35 | term.sendKeys("foo1\nfoo2\n"); 36 | 37 | assertThat(term.cursorPosition(), is(pos.advance(0, 2))); 38 | assertThat(term.currentLine(), isEmptyString()); 39 | 40 | term.sendKeys(Keys.ARROW_UP); 41 | assertThat(term.currentLine(), is("foo2")); 42 | 43 | term.sendKeys(Keys.ARROW_UP); 44 | assertThat(term.currentLine(), is("foo1")); 45 | 46 | term.sendKeys(Keys.ARROW_UP); 47 | assertThat(term.currentLine(), is("foo1")); 48 | 49 | term.sendKeys(Keys.ARROW_DOWN); 50 | assertThat(term.currentLine(), is("foo2")); 51 | 52 | term.sendKeys(Keys.ARROW_DOWN); 53 | assertThat(term.currentLine(), isEmptyString()); 54 | } 55 | 56 | @Test 57 | public void testArrowKeysAndRestore() { 58 | XTermElement term = $(XTermElement.class).first(); 59 | 60 | Position pos = term.cursorPosition(); 61 | term.sendKeys("foo1\nfoo2\n"); 62 | 63 | assertThat(term.cursorPosition(), is(pos.advance(0, 2))); 64 | assertThat(term.currentLine(), isEmptyString()); 65 | 66 | term.sendKeys("bar"); 67 | term.sendKeys(Keys.ARROW_UP); 68 | assertThat(term.currentLine(), is("foo2")); 69 | 70 | term.sendKeys(Keys.ARROW_DOWN); 71 | assertThat(term.currentLine(), is("bar")); 72 | } 73 | 74 | @Test 75 | public void testArrowUpAfterRunningLastCommandFromHistory() { 76 | XTermElement term = $(XTermElement.class).first(); 77 | 78 | term.sendKeys("foo1\n"); 79 | term.sendKeys("foo2\n"); 80 | 81 | assertThat(term.currentLine(), isEmptyString()); 82 | 83 | term.sendKeys(Keys.ARROW_UP); 84 | assertThat(term.currentLine(), is("foo2")); 85 | term.sendKeys("\n"); 86 | 87 | term.sendKeys(Keys.ARROW_UP); 88 | assertThat(term.currentLine(), is("foo2")); 89 | 90 | term.sendKeys(Keys.ARROW_UP); 91 | assertThat(term.currentLine(), is("foo1")); 92 | } 93 | 94 | @Test 95 | public void testArrowUpAfterRunningEmptyCommand() { 96 | XTermElement term = $(XTermElement.class).first(); 97 | 98 | term.sendKeys("foo1\n"); 99 | term.sendKeys("foo2\n"); 100 | 101 | assertThat(term.currentLine(), isEmptyString()); 102 | 103 | term.sendKeys(Keys.ARROW_UP); 104 | assertThat(term.currentLine(), is("foo2")); 105 | term.sendKeys("\u0008\u0008\u0008\u0008"); // 4 backspaces 106 | assertThat(term.currentLine(), isEmptyString()); 107 | term.sendKeys("\n"); 108 | 109 | term.sendKeys(Keys.ARROW_UP); 110 | // The position in the history should be back at the end 111 | assertThat(term.currentLine(), is("foo2")); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/XTermElement.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import com.vaadin.testbench.TestBenchElement; 23 | import com.vaadin.testbench.commands.TestBenchCommandExecutor; 24 | import com.vaadin.testbench.elementsbase.Element; 25 | import java.util.Arrays; 26 | import java.util.List; 27 | import org.openqa.selenium.WebElement; 28 | 29 | /** 30 | * A TestBench element representing a <fc-xterm> element. 31 | */ 32 | @Element("fc-xterm") 33 | public class XTermElement extends TestBenchElement { 34 | 35 | private WebElement input; 36 | 37 | @Override 38 | protected void init(WebElement element, TestBenchCommandExecutor commandExecutor) { 39 | super.init(element, commandExecutor); 40 | input = (WebElement) waitUntil( 41 | driver -> executeScript("return this.terminal.textarea")); 42 | } 43 | public void write(String text) { 44 | executeScript(String.format("this.terminal.write('%s')", text)); 45 | } 46 | 47 | public int getColumnWidth() { 48 | return ((Long) executeScript("return this.terminal.cols")).intValue(); 49 | } 50 | 51 | final String currentLine() { 52 | return getPropertyString("currentLine"); 53 | } 54 | 55 | public String getSelection() { 56 | return (String) executeScript("return this.terminal.getSelection()"); 57 | } 58 | 59 | public String lineAtOffset(int offset) { 60 | return ((String) executeScript( 61 | "buffer=this.terminal._core._inputHandler._bufferService.buffer;" 62 | + "line=buffer.lines.get(buffer.ybase+buffer.y+(arguments[0]));" 63 | + "return line.translateToString().substr(0,line.getTrimmedLength());", 64 | offset)); 65 | } 66 | 67 | public Position cursorPosition() { 68 | int[] pos = intArray(executeScript( 69 | "buffer=this.terminal.buffer.active; return [buffer.cursorX, buffer.cursorY]", 70 | this)); 71 | return new Position(pos[0], pos[1]); 72 | } 73 | 74 | private static int[] intArray(Object obj) { 75 | return ((List) obj).stream().mapToInt(i -> ((Long) i).intValue()).toArray(); 76 | } 77 | 78 | public void setUseSystemClipboard(boolean value) { 79 | setProperty("useSystemClipboard", value); 80 | } 81 | 82 | public void setPrompt(String value) { 83 | setProperty("prompt", value); 84 | } 85 | 86 | @Override 87 | public void sendKeys(CharSequence... keysToSend) { 88 | input.sendKeys(keysToSend); 89 | } 90 | 91 | @Override 92 | public Object executeScript(String script, Object... arguments) { 93 | script = String.format( 94 | "return function(arguments){arguments.pop(); %s}.bind(arguments[arguments.length-1])([].slice.call(arguments))", 95 | script); 96 | arguments = Arrays.copyOf(arguments, arguments.length + 1); 97 | arguments[arguments.length - 1] = this; 98 | return getCommandExecutor().executeScript(script, arguments); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/XTermIT.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import static org.hamcrest.Matchers.is; 23 | import static org.hamcrest.Matchers.not; 24 | import static org.hamcrest.Matchers.notNullValue; 25 | import static org.junit.Assert.assertThat; 26 | import com.vaadin.testbench.TestBenchElement; 27 | import org.hamcrest.Description; 28 | import org.hamcrest.Matcher; 29 | import org.hamcrest.TypeSafeDiagnosingMatcher; 30 | import org.junit.Test; 31 | import org.openqa.selenium.By; 32 | 33 | public class XTermIT extends AbstractViewTest { 34 | 35 | private Matcher hasBeenUpgradedToCustomElement = 36 | new TypeSafeDiagnosingMatcher() { 37 | 38 | @Override 39 | public void describeTo(Description description) { 40 | description.appendText("a custom element"); 41 | } 42 | 43 | @Override 44 | protected boolean matchesSafely(TestBenchElement item, Description mismatchDescription) { 45 | String script = "let s=arguments[0].shadowRoot; return !!(s&&s.childElementCount)"; 46 | if (!item.getTagName().contains("-")) { 47 | return true; 48 | } 49 | if ((Boolean) item.getCommandExecutor().executeScript(script, item)) { 50 | return true; 51 | } else { 52 | mismatchDescription.appendText(item.getTagName() + " "); 53 | mismatchDescription.appendDescriptionOf(is(not(this))); 54 | return false; 55 | } 56 | } 57 | }; 58 | 59 | 60 | @Test 61 | public void componentWorks() { 62 | TestBenchElement term = $("fc-xterm").first(); 63 | assertThat(term, hasBeenUpgradedToCustomElement); 64 | term.findElement(By.xpath("./*[@slot='terminal-container']")); 65 | } 66 | 67 | @Test 68 | public void writeText() throws InterruptedException { 69 | XTermElement term = $(XTermElement.class).first(); 70 | 71 | Position pos = term.cursorPosition(); 72 | 73 | term.sendKeys("HELLO"); 74 | assertThat(term.currentLine(), is("HELLO")); 75 | assertThat(term.cursorPosition(), is(pos.plus(5, 0))); 76 | 77 | term.sendKeys("HELLO"); 78 | assertThat(term.currentLine(), is("HELLOHELLO")); 79 | assertThat(term.cursorPosition(), is(pos.plus(10, 0))); 80 | 81 | term.sendKeys("\n"); 82 | assertThat(term.currentLine(), is("")); 83 | assertThat(term.cursorPosition(), is(pos.advance(0, 1))); 84 | 85 | term.sendKeys("HELLO\nWORLD"); 86 | assertThat(term.currentLine(), is("WORLD")); 87 | assertThat(term.cursorPosition(), is(pos.advance(0, 1).plus(5, 0))); 88 | } 89 | 90 | private Integer getKeyCount() { 91 | return ((Long)getCommandExecutor().executeScript("return keyCount")).intValue(); 92 | } 93 | 94 | @Test 95 | public void customKeyHandlerLowLevel() throws InterruptedException { 96 | XTermElement term = $(XTermElement.class).first(); 97 | term.executeScript("window.keyCount=0"); 98 | 99 | // register an event handler 100 | term.executeScript( 101 | "r1=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>++keyCount)"); 102 | assertThat(getKeyCount(), is(0)); 103 | 104 | // fire it 105 | term.sendKeys("E"); 106 | assertThat(term.currentLine(), is("E")); 107 | assertThat(getKeyCount(), is(1)); 108 | 109 | // register another event handler for the same key 110 | term.executeScript( 111 | "r2=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>++keyCount)"); 112 | 113 | // fire it: increment is performed twice (by each handler) 114 | term.sendKeys("E"); 115 | assertThat(term.currentLine(), is("EE")); 116 | assertThat(getKeyCount(), is(3)); 117 | 118 | // deregister 119 | term.executeScript("r1.dispose()"); 120 | term.sendKeys("E"); 121 | assertThat(getKeyCount(), is(4)); 122 | 123 | term.executeScript("r2.dispose()"); 124 | term.sendKeys("E"); 125 | assertThat(getKeyCount(), is(4)); 126 | } 127 | 128 | @Test 129 | public void customKeyHandlerRegistrationOrder() throws InterruptedException { 130 | // assert that custom key handlers are processed in registration order 131 | XTermElement term = $(XTermElement.class).first(); 132 | term.executeScript("window.keyCount=0"); 133 | 134 | term.executeScript( 135 | "r1=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>keyCount=keyCount*10+1)"); 136 | term.executeScript( 137 | "r2=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>keyCount=keyCount*10+2)"); 138 | term.sendKeys("E"); 139 | assertThat(getKeyCount(), is(12)); 140 | term.executeScript("r1.dispose()"); 141 | term.executeScript("r2.dispose()"); 142 | } 143 | 144 | @Test 145 | public void customKeyHandlerStopImmediatePropagation() throws InterruptedException { 146 | // since custom key handlers are processed in registration order 147 | // then r2 will prevent the processing of r3 148 | XTermElement term = $(XTermElement.class).first(); 149 | term.executeScript("window.keyCount=0"); 150 | 151 | term.executeScript( 152 | "r1=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>keyCount=keyCount*10+1)"); 153 | term.executeScript( 154 | "r2=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>ev.stopImmediatePropagation())"); 155 | term.executeScript( 156 | "r3=this.customKeyEventHandlers.register(ev=> ev.key=='E', ev=>keyCount=keyCount*10+3)"); 157 | term.sendKeys("E"); 158 | assertThat(getKeyCount(), is(1)); 159 | term.executeScript("r1.dispose()"); 160 | term.executeScript("r2.dispose()"); 161 | term.executeScript("r3.dispose()"); 162 | } 163 | 164 | @Test 165 | public void customKeyHandlerHighLevel() throws InterruptedException { 166 | XTermElement term = $(XTermElement.class).first(); 167 | term.executeScript("window.keyCount=0"); 168 | 169 | // register interest on CustomKey event 170 | Long id1 = (Long) term.executeScript("return this.registerCustomKeyListener({key:'E'})"); 171 | term.executeScript("this.addEventListener('CustomKey', ()=>++keyCount)"); 172 | assertThat(id1, is(notNullValue())); 173 | term.sendKeys("E"); 174 | assertThat(getKeyCount(), is(1)); 175 | 176 | // register interest again on CustomKey event for the same key 177 | // increment is performed by the CustomKey listener, which is called once 178 | Long id2 = (Long) term.executeScript("return this.registerCustomKeyListener({key:'E'})"); 179 | assertThat(id2, is(notNullValue())); 180 | assertThat(id2, is(not(id1))); 181 | term.sendKeys("E"); 182 | assertThat(getKeyCount(), is(2)); 183 | 184 | // deregister 185 | term.executeScript("this.customKeyEventHandlers.remove(arguments[0])", id1); 186 | term.sendKeys("E"); 187 | assertThat(getKeyCount(), is(3)); 188 | 189 | term.executeScript("this.customKeyEventHandlers.remove(arguments[0])", id2); 190 | term.sendKeys("E"); 191 | assertThat(getKeyCount(), is(3)); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/integration/XTermTestUtils.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.integration; 21 | 22 | import org.apache.commons.lang3.StringUtils; 23 | 24 | public class XTermTestUtils { 25 | 26 | /** Returns a line that runs up to the right margin */ 27 | public static String makeFullLine(XTermElement term, boolean hasPrompt) { 28 | int cols = term.getColumnWidth(); 29 | int x = hasPrompt ? term.cursorPosition().x : 0; 30 | return StringUtils.repeat("0123456789", cols / 10 + 1).substring(0, cols - x); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/test/SerializationTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.test; 21 | 22 | import static org.hamcrest.Matchers.is; 23 | import static org.hamcrest.Matchers.notNullValue; 24 | import static org.junit.Assert.assertThat; 25 | import com.flowingcode.vaadin.addons.xterm.TerminalHistory; 26 | import com.flowingcode.vaadin.addons.xterm.XTerm; 27 | import java.io.ByteArrayInputStream; 28 | import java.io.ByteArrayOutputStream; 29 | import java.io.IOException; 30 | import java.io.ObjectInputStream; 31 | import java.io.ObjectOutputStream; 32 | import java.lang.reflect.Method; 33 | import java.util.ListIterator; 34 | import java.util.stream.IntStream; 35 | import org.junit.Test; 36 | 37 | public class SerializationTest { 38 | 39 | private T shake(T obj) throws IOException, ClassNotFoundException { 40 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 41 | 42 | try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { 43 | oos.writeObject(obj); 44 | } 45 | 46 | try (ObjectInputStream in = 47 | new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { 48 | obj.getClass().cast(in.readObject()); 49 | } 50 | 51 | return obj; 52 | } 53 | 54 | @Test 55 | public void testSerialization() throws ClassNotFoundException, IOException { 56 | shake(new XTerm()); 57 | } 58 | 59 | @SuppressWarnings("serial") 60 | private final static class TestTerminalHistory extends TerminalHistory { 61 | public TestTerminalHistory(XTerm terminal) { 62 | super(terminal); 63 | } 64 | 65 | @SuppressWarnings("unchecked") 66 | public ListIterator listIterator() { 67 | try { 68 | Method method = TerminalHistory.class.getDeclaredMethod("listIterator"); 69 | method.setAccessible(true); 70 | return (ListIterator) method.invoke(this); 71 | } catch (Exception e) { 72 | throw new RuntimeException(e); 73 | } 74 | } 75 | } 76 | 77 | @Test 78 | public void testTerminalHistorySerialization() throws ClassNotFoundException, IOException { 79 | // prepare 80 | TestTerminalHistory history = new TestTerminalHistory(new XTerm()); 81 | 82 | assertThat(history.listIterator(), is(notNullValue())); 83 | assertThat(history.listIterator().nextIndex(), is(0)); 84 | 85 | shake(history); 86 | assertThat(history.listIterator(), is(notNullValue())); 87 | assertThat(history.listIterator().previousIndex(), is(-1)); 88 | 89 | int n = 5; 90 | IntStream.range(0, n).mapToObj(Integer::toString).forEach(history::add); 91 | assertThat(history.listIterator().nextIndex(), is(n)); 92 | 93 | history.listIterator().previous(); 94 | history.listIterator().previous(); 95 | assertThat(history.listIterator().nextIndex(), is(n - 2)); 96 | 97 | // assert 98 | shake(history); 99 | assertThat(history.listIterator().nextIndex(), is(n - 2)); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/com/flowingcode/vaadin/addons/xterm/utils/StateMemoizerTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * XTerm Console Addon 4 | * %% 5 | * Copyright (C) 2020 - 2023 Flowing Code 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package com.flowingcode.vaadin.addons.xterm.utils; 21 | 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | 25 | import static org.junit.Assert.assertEquals; 26 | import static org.junit.Assert.assertNull; 27 | 28 | public class StateMemoizerTest { 29 | 30 | private StateMemoizer memoizer; 31 | private BunchOfSetters proxy; 32 | private MyBean bean; 33 | 34 | public static interface BunchOfSetters { 35 | void setFoo(String value); 36 | void setBar(int value); 37 | } 38 | 39 | public static class MyBean implements BunchOfSetters { 40 | private String foo; 41 | private int bar; 42 | 43 | public String getFoo() { 44 | return foo; 45 | } 46 | 47 | public void setFoo(String foo) { 48 | this.foo = foo; 49 | } 50 | 51 | public int getBar() { 52 | return bar; 53 | } 54 | 55 | public void setBar(int bar) { 56 | this.bar = bar; 57 | } 58 | } 59 | 60 | @Before 61 | public void setupTestValues() { 62 | bean = new MyBean(); 63 | memoizer = new StateMemoizer(bean, BunchOfSetters.class); 64 | proxy = (BunchOfSetters) memoizer.getProxy(); 65 | } 66 | 67 | @Test 68 | public void emptyMemoizerApplySucceedsButDoesNothing() { 69 | memoizer.apply(); 70 | assertNull(bean.getFoo()); 71 | assertEquals(0, bean.getBar()); 72 | } 73 | 74 | @Test 75 | public void proxyModificationPassesValuesThrough() { 76 | proxy.setFoo("foo"); 77 | assertEquals("foo", bean.getFoo()); 78 | proxy.setBar(25); 79 | assertEquals(25, bean.getBar()); 80 | } 81 | 82 | @Test 83 | public void applyAppliesInvokedSettersOnly() { 84 | proxy.setFoo("foo"); 85 | bean.setFoo("bar"); 86 | bean.setBar(25); 87 | memoizer.apply(); 88 | assertEquals("foo", bean.getFoo()); 89 | assertEquals(25, bean.getBar()); 90 | } 91 | 92 | @Test 93 | public void applyBasicTest() { 94 | proxy.setFoo("foo"); 95 | proxy.setBar(25); 96 | bean.setFoo("FOO"); 97 | bean.setBar(26); 98 | memoizer.apply(); 99 | assertEquals("foo", bean.getFoo()); 100 | assertEquals(25, bean.getBar()); 101 | } 102 | 103 | @Test 104 | public void consequentSetterCallsAppliedProperly() { 105 | proxy.setFoo("foo"); 106 | assertEquals("foo", bean.getFoo()); 107 | proxy.setFoo("bar"); 108 | assertEquals("bar", bean.getFoo()); 109 | proxy.setBar(25); 110 | bean.setFoo("FOO"); 111 | bean.setBar(26); 112 | memoizer.apply(); 113 | assertEquals("bar", bean.getFoo()); 114 | assertEquals(25, bean.getBar()); 115 | } 116 | } 117 | --------------------------------------------------------------------------------