pax_global_header00006660000000000000000000000064152047522720014520gustar00rootroot0000000000000052 comment=102f67c450090b7c8fdf846932961e2b0674b8ae golang-github-gdamore-tcell.v3-3.4.0+dfsg/000077500000000000000000000000001520475227200202365ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/.codecov.yml000066400000000000000000000000271520475227200224600ustar00rootroot00000000000000ignore: - "vt/tests" golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/000077500000000000000000000000001520475227200215765ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/CODEOWNERS000066400000000000000000000000131520475227200231630ustar00rootroot00000000000000* @gdamore golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/FUNDING.yml000066400000000000000000000001101520475227200234030ustar00rootroot00000000000000patreon: gedamore github: gdamore tidelift: go/github.com/gdamore/tcell golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/dependabot.yml000066400000000000000000000002631520475227200244270ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: weekly - package-ecosystem: github-actions directory: / schedule: interval: weekly golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/workflows/000077500000000000000000000000001520475227200236335ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/workflows/linux.yml000066400000000000000000000016101520475227200255130ustar00rootroot00000000000000name: linux on: pull_request: push: branches: - main jobs: build: name: build runs-on: [ubuntu-latest] strategy: fail-fast: false matrix: go: ["stable", "oldstable"] steps: - name: Check out code into the Go module directory uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} id: go - name: Go version run: go version - name: Get dependencies run: go get -v -t -d ./... - name: Build run: go build -v . - name: Test run: script -q -e -c "go test -coverpkg=github.com/gdamore/tcell/v3/... -covermode=count -coverprofile=coverage.txt ./..." - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/workflows/macos.yml000066400000000000000000000016151520475227200254630ustar00rootroot00000000000000name: macos on: pull_request: push: branches: - main jobs: build: name: build runs-on: [macos-latest] strategy: fail-fast: false matrix: go: ["stable", "oldstable"] steps: - name: Check out code into the Go module directory uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} id: go - name: Go version run: go version - name: Get dependencies run: go get -v -t -d ./... - name: Build run: go build -v . - name: Test run: script -q /dev/null go test -coverpkg="github.com/gdamore/tcell/v3/..." -covermode=count -coverprofile="coverage.txt" ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/workflows/webasm.yml000066400000000000000000000011611520475227200256330ustar00rootroot00000000000000name: webasm on: pull_request: push: branches: - main jobs: build: name: build runs-on: [ubuntu-latest] strategy: fail-fast: false matrix: go: ["stable", "oldstable"] steps: - name: Check out code into the Go module directory uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} id: go - name: Get dependencies run: go get -v -t -d ./... - name: Build the web assembly mouse demo run: env GOOS=js GOARCH=wasm go build _demos/mouse.go golang-github-gdamore-tcell.v3-3.4.0+dfsg/.github/workflows/windows.yml000066400000000000000000000015111520475227200260460ustar00rootroot00000000000000name: windows on: pull_request: push: branches: - main jobs: build: name: build runs-on: [windows-latest] strategy: fail-fast: false matrix: go: ["stable", "oldstable"] steps: - name: Check out code into the Go module directory uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} id: go - name: Get dependencies run: go get -v -t -d ./... - name: Build run: go build -v . - name: Test run: go test -coverpkg="github.com/gdamore/tcell/v3/..." -covermode=count -coverprofile="coverage.txt" ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} golang-github-gdamore-tcell.v3-3.4.0+dfsg/.gitignore000066400000000000000000000000301520475227200222170ustar00rootroot00000000000000coverage.txt .zed .idea golang-github-gdamore-tcell.v3-3.4.0+dfsg/.superset/000077500000000000000000000000001520475227200221665ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/.superset/config.json000066400000000000000000000001111520475227200243170ustar00rootroot00000000000000{ "setup": [ "go mod download" ], "teardown": [], "run": [] }golang-github-gdamore-tcell.v3-3.4.0+dfsg/AGENTS.md000066400000000000000000000001661520475227200215440ustar00rootroot00000000000000Use the `gopls` MCP server by default when working on Go code in this repository, unless I explicitly ask you not to. golang-github-gdamore-tcell.v3-3.4.0+dfsg/AUTHORS000066400000000000000000000002331520475227200213040ustar00rootroot00000000000000Garrett D'Amore Zachary Yedidia Junegunn Choi Staysail Systems, Inc. golang-github-gdamore-tcell.v3-3.4.0+dfsg/CHANGESv2.md000066400000000000000000000065401520475227200221050ustar00rootroot00000000000000## Breaking Changes in _Tcell_ v2 A number of changes were made to _Tcell_ for version two, and some of these are breaking. ### Import Path The import path for tcell has changed to `github.com/gdamore/tcell/v2` to reflect a new major version. ### Style Is Not Numeric The type `Style` has changed to a structure, to allow us to add additional data such as flags for color setting, more attribute bits, and so forth. Applications that relied on this being a number will need to be updated to use the accessor methods. ### Mouse Event Changes The middle mouse button was reported as button 2 on Linux, but as button 3 on Windows, and the right mouse button was reported the reverse way. _Tcell_ now always reports the right mouse button as button 2, and the middle button as button 3. To help make this clearer, new symbols `ButtonPrimary`, `ButtonSecondary`, and `ButtonMiddle` are provided. (Note that which button is right vs. left may be impacted by user preferences. Usually the left button will be considered the Primary, and the right will be the Secondary.) Applications may need to adjust their handling of mouse buttons 2 and 3 accordingly. ### Terminals Removed A number of terminals have been removed. These are mostly ancient definitions unlikely to be used by anyone, such as `adm3a`. ### High Number Function Keys Historically terminfo reported function keys with modifiers set as a different function key altogether. For example, Shift-F1 was reported as F13 on XTerm. _Tcell_ now prefers to report these using the base key (such as F1) with modifiers added. This works on XTerm and VTE based emulators, but some emulators may not support this. The new behavior more closely aligns with behavior on Windows platforms. ## New Features in _Tcell_ v2 These features are not breaking, but are introduced in version 2. ### Improved Modifier Support For terminals that appear to behave like the venerable XTerm, _tcell_ automatically adds modifier reporting for ALT, CTRL, SHIFT, and META keys when the terminal reports them. ### Better Support for Palettes (Themes) When using a color by its name or palette entry, _Tcell_ now tries to use that palette entry as is; this should avoid some inconsistency and respect terminal themes correctly. When true fidelity to RGB values is needed, the new `TrueColor()` API can be used to create a direct color, which bypasses the palette altogether. ### Automatic TrueColor Detection For some terminals, if the `Tc` or `RGB` properties are present in terminfo, _Tcell_ will automatically assume the terminal supports 24-bit color. ### ColorReset A new color value, `ColorReset` can be used on the foreground or background to reset the color the default used by the terminal. ### tmux Support _Tcell_ now has improved support for tmux, when the `$TERM` variable is set to "tmux". ### Strikethrough Support _Tcell_ has support for strikethrough when the terminal supports it, using the new `StrikeThrough()` API. ### Bracketed Paste Support _Tcell_ provides the long requested capability to discriminate paste event by using the bracketed-paste capability present in some terminals. This is automatically available on terminals that support XTerm style mouse handling, but applications must opt-in to this by using the new `EnablePaste()` function. A new `EventPaste` type of event will be delivered when starting and finishing a paste operation. golang-github-gdamore-tcell.v3-3.4.0+dfsg/CHANGESv3.md000066400000000000000000000121351520475227200221030ustar00rootroot00000000000000## Breaking Changes in _Tcell_ v3 There are a number of changes in _Tcell_ v3, mostly aimed at simplifying things for applications, but some also intended to reduce the burden for support. Every application will need at least some changes, but it is expected that those changes will be small, possibly even mechanical in nature. ### Cell and Contents APIs In order to improve support for multi-rune grapheme clusters, and to provide an experience that reduces friction when using it, some APIs have been removed, and newer APIs exist in their place. - `SetCell` and `SetContents` are removed. Use `Put` instead. - `GetContents` is removed. Use `Get` instead. ### Events (PostEvent, PollEvent, ChannelEvents) The event channel is now directly exposed via `EventQ`, and events may be read from or written directly to the channel in the standard Go fashion. This should help applications that want to integrate into `select` statements (e.g. for timed key presses). The `ChannelEvents`, `PollEvent`, `PostEvent`, and `PostEventWait` functions are removed, as applications can now just access the event channel directly. ### Key Event Changes `EventKey` now carries a string for `KeyRune` instead of a single rune. As a result the old `Rune` method for `EventKey` is replaced by `Str`. The main difference for most users will be that `Str` returns a string, and most of the time that string will consist of only a single rune. However, it is possible now to inject synthetic key strokes consisting of multi-rune grapheme clusters. Additionally the following special keys are removed, as they are delivered instead as `KeyRune` with the relevant rune, and the `ModCtrl` modifier: `KeyCtrlSpace`, `KeyCtrlLeftSq`, `KeyCtrlRightSq`, `KeyCtrlBackslash`, and `KeyCtrlUnderscore`. Note that `KeyRune` will never have a `ModShift` applied unless it is applied also with other modifiers. The `KeyCtrlA` through `KeyCtrlZ` keys are delivered, but will also carry the associated lower case rune (e.g. "a", "b", etc.) and `ModCtrl`. The `KeyBackspace2` key is no longer delivered, but is converted to `KeyBackspace`. (This resolves some inconsistency around e.g. CTRL-H vs DELETE.) When advanced key reporting is enabled, Shift-Tab is reported as `KeyTab` with `ModShift`, not as `KeyBacktab`. Legacy key reporting still reports Shift-Tab as `KeyBacktab`. ### Termbox Compatibility Removed The `termbox` compatibility package is removed. Few applications were using it, and the compatibility was imperfect. Also the package had limited support for many newer features. Further, _Termbox_ itself is no longer being maintained. Applications that still need this should keep using _Tcell_ v2. ### Terminfo Removed The Terminfo subsystem has been removed entirely. Essentially the old terminfo based design has long proved to be inferior for modern terminal applications, and has not kept up with newer terminal features such as 24-bit color, different mouse reporting modes, bracketed paste, advanced text styling, and so forth. As part of this, we're removing the parsed terminfo logic entirely. It turns out that pretty much all of the terminal logic can be consolidated to just a few classes of terminals with substantial overlap. A consequence of this is that support for some legacy terminals that are either functionally extinct (such as _hpterm_) or unlikely to be found outside of a museum (such as VT52, Wyse50, or anything produced more than 40 years ago.) Note that VT100 and later will work in emulation, and VT220 and later physical terminals should still work. VT100 physical terminals may not work, as the padding delays that existed for them are removed. Those delays hurt emulations that do not need them, and existed only to accommodate limitations found on the physical hardware from the 1970s. Note that we still examine `$TERM` when appropriate, but if the value is not one we recognize, then we will assume something reasonably capable and compatible at some level with _xterm_ or at least ECMA-48. ### Color, Attributes, Etc. Bit Sizes The `Color` type is now only 32-bits, which should save some memory on large terminal windows. The `AttrMask` type is now only 16-bits, and the `UnderlineStyle` is now 8 bits. All these lead to further savings in the memory per cell. ### Underline `AttrUnderline` is gone. It was not sufficient to describe styled and colored underlines. ### Removed Capability Queries Deprecated APIs `HasKey`, `HasMouse`, and `CanDisplay` are removed. These functions weren't reliable and served no useful purpose. ### Windows Console API `NewConsoleScreen` is removed as is support for Windows console mode. Instead this uses the more modern Windows VT modes. As a consequence, this means that _Tcell_ on Windows requires at least Winows 10 build 1703 (the Creators Update). If you are using a version of Windows 10 older than that, you should really upgrade for _many_ reasons, not just because _Tcell_ doesn't support it anymore. ### InputProcessor is no longer Public This structure, and the associated `NewInputProcessor` function, were made public incorrectly. They are not part of our public API going forward, and are now private symbols. golang-github-gdamore-tcell.v3-3.4.0+dfsg/LICENSE000066400000000000000000000261361520475227200212530ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-gdamore-tcell.v3-3.4.0+dfsg/README-plan9.md000066400000000000000000000016061520475227200225410ustar00rootroot00000000000000# _Tcell_ on Plan 9 > [!NOTE] > Plan 9 is supported on a best-effort basis, as the main _Tcell_ development team does not have a Plan 9 environment. The Plan 9 backend opens `/dev/cons` for I/O, enables raw mode by writing `rawon`/`rawoff` to `/dev/consctl`. It watches `/dev/wctl` for resize notifications. The default mode for `vt((1)` is VT100, which will only provide basic monochrome text, and few additional features. In this case, it is expected that `TERM=vt100` is set. It may be possible to emulate more modern terminals using `-2` (VT220), `-a` (ANSI), or `-x` (XTerm) flags to `vt`. While this has not been tested, the use of `-x` to get xterm like features, combinerd with a `TERM=xterm` may yield superior results, including possibly color and mouse support. Note that if _Tcell_ does not find a suitable value for `TERM` in the environment, it will assume XTerm like functionality. golang-github-gdamore-tcell.v3-3.4.0+dfsg/README.md000066400000000000000000000215021520475227200215150ustar00rootroot00000000000000 # Tcell _Tcell_ is a _Go_ package that provides a cell based view for text terminals, like _XTerm_. It was inspired by _termbox_, but includes many additional improvements. [![Stand With Ukraine](logos/ukraine.svg)](https://stand-with-ukraine.pp.ua) [![Docs](https://img.shields.io/badge/godoc-reference-blue.svg?label=&logo=go)](https://pkg.go.dev/github.com/gdamore/tcell/v3) [![Linux](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/linux.yml?branch=main&logoColor=grey&logo=linux&label=)](https://github.com/gdamore/tcell/actions/workflows/linux.yml) [![macOS](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/macos.yml?branch=main&logoColor=grey&logo=apple&label=)](https://github.com/gdamore/tcell/actions/workflows/macos.yml) [![Windows](https://custom-icon-badges.demolab.com/github/actions/workflow/status/gdamore/tcell/windows.yml?branch=main&logoColor=grey&logo=windows10&label=)](https://github.com/gdamore/tcell/actions/workflows/windows.yml) [![Web Assembly](https://img.shields.io/github/actions/workflow/status/gdamore/tcell/webasm.yml?branch=main&logoColor=grey&logo=webassembly&label=)](https://github.com/gdamore/tcell/actions/workflows/webasm.yml) [![Coverage](https://img.shields.io/codecov/c/github/gdamore/tcell?logoColor=grey&logo=codecov&label=)](https://codecov.io/gh/gdamore/tcell) [![Go Report Card](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=flat&label=&logo=go&logoColor=grey)](https://goreportcard.com/report/github.com/gdamore/tcell/v3) [![Discord](https://img.shields.io/discord/639503822733180969?label=&logo=discord)](https://discord.gg/urTTxDN) [![Latest Release](https://img.shields.io/github/v/release/gdamore/tcell.svg?logo=github&label=)](https://github.com/gdamore/tcell/releases) > [!NOTE] > This is version 3 of _Tcell_. > There are breaking changes relative to versions 1 and 2. > [Version 2](https://github.com/gdamore/tcell/tree/v2) remains available using the import `github.com/gdamore/tcell/v2`. > [Version 1](https://github.com/gdamore/tcell/tree/v1) Version 1.x remains available using the import `github.com/gdamore/tcell`, but is > unmaintained and should not be used. ## Tutorial A brief, and still somewhat rough, [tutorial](TUTORIAL.md) is available. ## Examples A number of example are posted up on our [Gallery](https://github.com/gdamore/tcell/wikis/Gallery/). That's a wiki, and please do submit updates if you have something you want to showcase. There are also demonstration programs in the `./demos` directory, as well as some in `./_demos`. ## More Portable _Tcell_ is portable to a wide variety of systems, and is pure Go, without any need for CGO. _Tcell_ works with mainstream systems officially supported by golang. Following the Go support policy, _Tcell_ officially only supports the current ("stable") version of go, and the version immediately prior ("oldstable"). This policy is necessary to make sure that we can update dependencies to pick up security fixes and new features, and it allows us to adopt changes (such as library and language features) that are only supported in newer versions of Go. ## Rich Unicode & non-Unicode support _Tcell_ includes enhanced support for Unicode, including wide characters and grapheme clusters, provided your terminal can support them. It will also convert to and from Unicode locales, so that the program can work with UTF-8 internally, and get reasonable output in other locales. _Tcell_ tries hard to convert to native characters on both input and output. On output _Tcell_ even makes use of the alternate character set to facilitate drawing certain characters. ## Better Keyboard Support _Tcell_ also has richer support for a larger number of special keys that some terminals can send. On modern terminal emulators we can also support a rich set of modifiers, and can discriminate between e.g. CTRL-I and TAB. (This does require the terminal emulator to support one of the modern keyboard protocols.) ## Better Mouse Support _Tcell_ supports enhanced mouse tracking mode, so your application can receive regular mouse motion events, click-drag, and wheel events, if your terminal supports it. ## Working With Unicode Internally _Tcell_ uses UTF-8, just like Go. However, _Tcell_ understands how to convert to and from other character sets, using the capabilities of the `golang.org/x/text/encoding` packages. Your application must supply them, as the full set of the most common ones bloats the program by about 2 MB. If you're lazy, and want them all anyway, see the `encoding` sub-directory. ## Wide & Combining Characters The `Put()` API takes a string, which should be legal UTF-8, and displays the first grapheme cluster (which may composed of multiple runes). It returns the actual width displayed, which can be used to advance the column positiion for the next display grapheme. Alternatively, `PutStr()` or `PutStrStyled()` can be used to display a single line of text (which will be clipped at the edge of the screen). If a second character is displayed immediately in the cell adjacent to a wide character (offset by one instead of by two), then the results are undefined. ## Colors _Tcell_ assumes the ANSI/XTerm color palette for up to 256 colors, although terminals such as legacy ANSI terminals may only support 8 colors. ## 24-bit Color _Tcell_ _supports 24-bit color!_ (That is, if your terminal can support it.) There are a few ways you can enable (or disable) 24-bit color. - You can force this one by setting the `COLORTERM` environment variable to `truecolor`. This environment variable is frequently set by terminal emulators that support 24-bit color. - On Windows, 24-bit color support is assumed. (All modern Windows terminal emulators support it.) - If you set your `TERM` environment variable to a value with the suffix `-truecolor` or `-direct`, then 24-bit color compatible with XTerm and ECMA-48 will be assumed. - You can disable 24-bit color by setting `TCELL_TRUECOLOR=disable` in your environment. When using 24-bit color, programs will display the colors that the programmer intended, overriding any "`themes`" the user may have set in their terminal emulator. (For some cases, accurate color fidelity is more important than respecting themes. For other cases, such as typical text apps that only use a few colors, its more desirable to respect the themes that the user has established.) ## Terminal Overrides _Tcell_ normally negotiates terminal capabilities automatically, but some terminal emulators answer those queries incorrectly. These environment variables provide user escape hatches when the automatic path is not reliable: - `TCELL_KEYBOARD_PROTOCOL=auto|legacy|kitty|win32|xterm` forces the keyboard reporting protocol. - `TCELL_NEGOTIATE=auto|disable` disables startup capability negotiation when terminal responses themselves are problematic. - `TCELL_MOUSE=auto|disable` prevents applications from enabling terminal mouse reporting. Applications can also choose a keyboard protocol with `OptKeyboardProtocol` or disable startup negotiation with `OptNegotiation`. Environment variables take precedence so users can recover from bad terminal behavior without modifying an application. ## Performance Reasonable attempts have been made to minimize sending data to terminals, avoiding repeated sequences or drawing the same cell on refresh updates. ## Mouse Support Mouse tracking, buttons, and even wheel mice are supported on most terminal emulators, as well as Windows. ## Bracketed Paste Terminals that support support it, can use bracketed paste. See `EnablePaste()` for details. ## Breaking Changes in v3 There are a number of changes in _Tcell_ version 3, which break compatibility with version 2 and version 1. Your application will almost certainly need some minor updates to work with version 3. Please see the [CHANGESv3](CHANGESv3.md) document for a list. ## Platforms ### POSIX (Linux, FreeBSD, macOS, Solaris, etc.) Everything works using pure Go on mainstream platforms. Esoteric platforms (e.g. zOS or AIX) are supported on a best-effort only basis. Pull requests to fix any issues found are welcome! ### Windows Modern Windows is supported. Please see the [README-windows](README-windows.md) document for much more detailed information. ### WASM WASM is supported, but needs additional setup detailed in [README-wasm](README-wasm.md). ### Plan 9 Plan 9 is supported on a best-effort basis. Please see the [README-plan9](README-plan9.md) document for more information. ### Commercial Support _Tcell_ is absolutely free, but if you want to obtain commercial, professional support, there are options. - [TideLift](https://tidelift.com/) subscriptions include support for _Tcell_, as well as many other open source packages. - [Staysail Systems Inc.](mailto:info@staysail.tech) offers direct support, and custom development around _Tcell_ on an hourly basis. golang-github-gdamore-tcell.v3-3.4.0+dfsg/SECURITY.md000066400000000000000000000007621520475227200220340ustar00rootroot00000000000000# SECURITY It's somewhat unlikely that tcell is in a security sensitive path, but we do take security seriously. ## Vulnerability Response If you report a vulnerability, we will respond within 2 business days. ## Report a Vulnerability If you wish to report a vulnerability found in tcell, simply send a message to garrett@damore.org. You may also reach us on our discord channel - https://discord.gg/urTTxDN - a private message to `gdamore` on that channel may be submitted instead of mail. golang-github-gdamore-tcell.v3-3.4.0+dfsg/TUTORIAL.md000066400000000000000000000202141520475227200220220ustar00rootroot00000000000000# _Tcell_ Tutorial _Tcell_ provides a low-level, portable API for building terminal-based programs. A [terminal emulator](https://en.wikipedia.org/wiki/Terminal_emulator) (or a real terminal such as a DEC VT-220) is used to interact with such a program. _Tcell_'s interface is fairly low-level. While it provides a reasonably portable way of dealing with all the usual terminal features, it may be easier to utilize a higher level framework. A number of such frameworks are listed on the _Tcell_ main [README](README.md). This tutorial provides the details of _Tcell_, and is appropriate for developers wishing to create their own application frameworks or needing more direct access to the terminal capabilities. ## Resize events Applications receive an event of type `EventResize` when they are first initialized and each time the terminal is resized. The new size is available as `Size`. ```go switch ev := ev.(type) { case *tcell.EventResize: w, h := ev.Size() logMessage(fmt.Sprintf("Resized to %dx%d", w, h)) } ``` ## Key events When a key is pressed, applications receive an event of type `EventKey`. This event describes the modifier keys pressed (if any) and the pressed key or rune. When a rune key is pressed, an event with its `Key` set to `KeyRune` is dispatched. When a non-rune key is pressed, it is available as the `Key` of the event. ```go switch ev := ev.(type) { case *tcell.EventKey: mod, key, ch := ev.Mod(), ev.Key(), ev.Str() logMessage(fmt.Sprintf("EventKey Modifiers: %d Key: %d Str: %q", mod, key, ch)) } ``` ### Key event restrictions Terminal-based programs have less visibility into keyboard activity than graphical applications. When a key is pressed and held, additional key press events are sent by the terminal emulator. The rate of these repeated events depends on the emulator's configuration. Key release events are not available. It is not possible to distinguish runes typed while holding shift and runes typed using caps lock. Capital letters are reported without the Shift modifier. ## Mouse events Applications receive an event of type `EventMouse` when the mouse moves, or a mouse button is pressed or released. Mouse events are only delivered if `EnableMouse` has been called. The mouse buttons being pressed (if any) are available as `Buttons`, and the position of the mouse is available as `Position`. ```go switch ev := ev.(type) { case *tcell.EventMouse: mod := ev.Modifiers() btns := ev.Buttons() x, y := ev.Position() logMessage(fmt.Sprintf("EventMouse Modifiers: %d Buttons: %d Position: %d,%d", mod, btns, x, y)) } ``` ### Mouse buttons Identifier | Alias | Description -----------|-----------------|----------- Button1 | ButtonPrimary | Left button Button2 | ButtonSecondary | Right button Button3 | ButtonMiddle | Middle button Button4 | | Side button (thumb/next) Button5 | | Side button (thumb/prev) WheelUp | | Scroll wheel up WheelDown | | Scroll wheel down WheelLeft | | Horizontal wheel left WheelRight | | Horizontal wheel right ## Usage To create a _Tcell_ application, first initialize a screen to hold it. ```go s, err := tcell.NewScreen() if err != nil { log.Fatalf("%+v", err) } if err := s.Init(); err != nil { log.Fatalf("%+v", err) } // Set default text style defStyle := tcell.StyleDefault.Background(color.Reset).Foreground(color.Reset) s.SetStyle(defStyle) // Clear screen s.Clear() ``` Text may be drawn on the screen using `Put`, `PutStr`, or `PutStrStyled`. ```go s.Put(0, 0, "H", defStyle) s.Put(1, 0, "i", defStyle) s.Put(2, 0, "!", defStyle) ``` which is equivalent to ```go s.PutStrStyled(0, 0, "Hi!", defStyle) ```` To draw text more easily with wrapping, define a render function. ```go func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { row := y1 col := x1 var width int for text != "" { text, width = s.Put(col, row, text, style) col += width if col >= x2 { row++ col = x1 } if row > y2 { break } if width == 0 { // incomplete grapheme at end of string break } } } ``` Lastly, define an event loop to handle user input and update application state. ```go quit := func() { s.Fini() os.Exit(0) } for { // Update screen s.Show() // Poll event (can be used in select statement as well) ev := <-s.EventQ() // Process event switch ev := ev.(type) { case *tcell.EventResize: s.Sync() case *tcell.EventKey: if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC { quit() } } } ``` ## Demo application The following demonstrates how to initialize a screen, draw text/graphics and handle user input. ```go package main import ( "fmt" "log" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { row := y1 col := x1 var width int for text != "" { text, width = s.Put(col, row, text, style) col += width if col >= x2 { row++ col = x1 } if row > y2 { break } if width == 0 { // incomplete grapheme at end of string break } } } func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { if y2 < y1 { y1, y2 = y2, y1 } if x2 < x1 { x1, x2 = x2, x1 } // Fill background for row := y1; row <= y2; row++ { for col := x1; col <= x2; col++ { s.Put(col, row, " ", style) } } // Draw borders for col := x1; col <= x2; col++ { s.Put(col, y1, string(tcell.RuneHLine), style) s.Put(col, y2, string(tcell.RuneHLine), style) } for row := y1 + 1; row < y2; row++ { s.Put(x1, row, string(tcell.RuneVLine), style) s.Put(x2, row, string(tcell.RuneVLine), style) } // Only draw corners if necessary if y1 != y2 && x1 != x2 { s.Put(x1, y1, string(tcell.RuneULCorner), style) s.Put(x2, y1, string(tcell.RuneURCorner), style) s.Put(x1, y2, string(tcell.RuneLLCorner), style) s.Put(x2, y2, string(tcell.RuneLRCorner), style) } drawText(s, x1+1, y1+1, x2-1, y2-1, style, text) } func main() { defStyle := tcell.StyleDefault.Background(color.Reset).Foreground(color.Reset) boxStyle := tcell.StyleDefault.Foreground(color.White).Background(color.Purple) // Initialize screen s, err := tcell.NewScreen() if err != nil { log.Fatalf("%+v", err) } if err := s.Init(); err != nil { log.Fatalf("%+v", err) } s.SetStyle(defStyle) s.EnableMouse() s.EnablePaste() s.Clear() // Draw initial boxes drawBox(s, 1, 1, 42, 7, boxStyle, "Click and drag to draw a box") drawBox(s, 5, 9, 32, 14, boxStyle, "Press C to reset") quit := func() { // You have to catch panics in a defer, clean up, and // re-raise them - otherwise your application can // die without leaving any diagnostic trace. maybePanic := recover() s.Fini() if maybePanic != nil { panic(maybePanic) } } defer quit() // Here's how to get the screen size when you need it. // xmax, ymax := s.Size() // Here's an example of how to inject a keystroke where it will // be picked up by a future read of the event queue. Note that // care should be used to avoid blocking writes to the queue if // this is done from the same thread that is responsible for reading // the queue, or else a single-party deadlock might occur. // s.EventQ() <- tcell.NewEventKey(tcell.KeyRune, rune('a'), 0) // Event loop ox, oy := -1, -1 for { // Update screen s.Show() // Poll event (this can be in a select statement as well) ev := <-s.EventQ() // Process event switch ev := ev.(type) { case *tcell.EventResize: s.Sync() case *tcell.EventKey: if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC { return } else if ev.Key() == tcell.KeyCtrlL { s.Sync() } else if ev.Str() == "C" || ev.Str() == "c" { s.Clear() } case *tcell.EventMouse: x, y := ev.Position() switch ev.Buttons() { case tcell.Button1, tcell.Button2: if ox < 0 { ox, oy = x, y // record location when click started } case tcell.ButtonNone: if ox >= 0 { label := fmt.Sprintf("%d,%d to %d,%d", ox, oy, x, y) drawBox(s, ox, oy, x, y, boxStyle, label) ox, oy = -1, -1 } } } } } ``` golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/000077500000000000000000000000001520475227200215045ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/mouse.go000066400000000000000000000211311520475227200231610ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // mouse displays a text box and tests mouse interaction. As you click // and drag, boxes are displayed on screen. Other events are reported in // the box. Press ESC twice to exit the program. package main import ( "fmt" "os" "os/exec" "runtime" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) var defStyle tcell.Style func isCtrlRune(ev *tcell.EventKey, r string) bool { return ev.Key() == tcell.KeyRune && ev.Str() == r && ev.Modifiers()&tcell.ModCtrl != 0 } func keyboardProtocolName(p tcell.KeyProtocol) string { switch p { case tcell.LegacyKeyboard: return "Legacy" case tcell.KittyKeyboard: return "Kitty" case tcell.Win32Keyboard: return "Win32" case tcell.XTermKeyboard: return "XTerm" default: return "Unknown" } } func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, r rune) { rs := string(r) if y2 < y1 { y1, y2 = y2, y1 } if x2 < x1 { x1, x2 = x2, x1 } for col := x1; col <= x2; col++ { s.Put(col, y1, string(tcell.RuneHLine), style) s.Put(col, y2, string(tcell.RuneHLine), style) } for row := y1 + 1; row < y2; row++ { s.Put(x1, row, string(tcell.RuneVLine), style) s.Put(x2, row, string(tcell.RuneVLine), style) } if y1 != y2 && x1 != x2 { // Only add corners if we need to s.Put(x1, y1, string(tcell.RuneULCorner), style) s.Put(x2, y1, string(tcell.RuneURCorner), style) s.Put(x1, y2, string(tcell.RuneLLCorner), style) s.Put(x2, y2, string(tcell.RuneLRCorner), style) } for row := y1 + 1; row < y2; row++ { for col := x1 + 1; col < x2; col++ { s.Put(col, row, rs, style) } } } func drawSelect(s tcell.Screen, x1, y1, x2, y2 int, sel bool) { if y2 < y1 { y1, y2 = y2, y1 } if x2 < x1 { x1, x2 = x2, x1 } for row := y1; row <= y2; row++ { for col := x1; col <= x2; col++ { str, style, width := s.Get(col, row) if style == tcell.StyleDefault { style = defStyle } style = style.Reverse(sel) s.Put(col, row, str, style) col += width - 1 // add an extra column if 2 cells } } } // This program just shows simple mouse and keyboard events. Press ESC twice to // exit. func main() { shell := os.Getenv("SHELL") if shell == "" { if runtime.GOOS == "windows" { shell = "CMD.EXE" } else { shell = "/bin/sh" } } s, e := tcell.NewScreen(tcell.OptAdvancedKeys(true)) if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e := s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } s.SetTitle("Tcell Mouse Demonstration") defStyle = tcell.StyleDefault. Background(color.Reset). Foreground(color.Reset) s.SetStyle(defStyle) s.EnableMouse() s.EnablePaste() s.EnableFocus() s.Clear() posfmt := "Mouse: %d, %d " btnfmt := "Buttons: %s" keyfmt := "Keys: %s" pastefmt := "Paste: [%d] %s" focusfmt := "Focus: %s" termFmt := "Term: %s (%s)" kbdFmt := "Keyboard: %s" style := tcell.StyleDefault. Foreground(color.MidnightBlue).Background(color.LightCoral) keyDownStyle := style.Foreground(color.Green) keyUpStyle := style mx, my := -1, -1 ox, oy := -1, -1 bx, by := -1, -1 w, h := s.Size() lchar := '*' bstr := "" lks := "" lkey := "" kcnt := 0 pstr := "" ecnt := 0 pasting := false focus := true // assume we are focused when we start keyStyle := keyUpStyle for { drawBox(s, 1, 1, 42, 10, style, ' ') s.PutStrStyled(2, 2, "Press Ctrl-Q to Quit, C to clear.", style) s.PutStrStyled(2, 3, fmt.Sprintf(posfmt, mx, my), style) s.PutStrStyled(2, 4, fmt.Sprintf(btnfmt, bstr), style) s.PutStrStyled(2, 5, fmt.Sprintf(keyfmt, ""), style) s.PutStrStyled(8, 5, lks, keyStyle) ps := pstr if len(ps) > 26 { ps = "..." + ps[len(ps)-24:] } s.PutStrStyled(2, 6, fmt.Sprintf(pastefmt, len(pstr), ps), style) fstr := "false" if focus { fstr = "true" } s.PutStrStyled(2, 7, fmt.Sprintf(focusfmt, fstr), style) n, v := s.Terminal() s.PutStrStyled(2, 8, fmt.Sprintf(termFmt, n, v), style) s.PutStrStyled(2, 9, fmt.Sprintf(kbdFmt, keyboardProtocolName(s.KeyboardProtocol())), style) s.Show() bstr = "" ev := <-s.EventQ() st := tcell.StyleDefault.Background(color.Red) up := tcell.StyleDefault. Background(color.Blue). Foreground(color.Black) w, h = s.Size() // always clear any old selection box if ox >= 0 && oy >= 0 && bx >= 0 { drawSelect(s, ox, oy, bx, by, false) } switch ev := ev.(type) { case *tcell.EventResize: s.Sync() s.Put(w-1, h-1, "R", st) case *tcell.EventKey: s.Put(w-2, h-2, ev.Str(), st) if !ev.Pressed() { pstr = "" lks = ev.Name() keyStyle = keyUpStyle for x := range w - 1 { s.Put(x, h-1, " ", tcell.StyleDefault) } s.Put(w-1, h-1, "U", st) continue } keyStyle = keyDownStyle if ev.Name() == lkey { kcnt++ } else { lkey = ev.Name() kcnt = 1 } if pasting { s.Put(w-1, h-1, "P", st) if ev.Key() == tcell.KeyRune { pstr = pstr + ev.Str() } else { pstr = pstr + "\ufffd" // replacement for now } lks = "" continue } pstr = "" s.Put(w-1, h-1, "K", st) if ev.Key() == tcell.KeyEscape { ecnt++ if ecnt > 1 { s.Fini() os.Exit(0) } } else if ev.Key() == tcell.KeyCtrlL || isCtrlRune(ev, "l") { s.Sync() } else if ev.Key() == tcell.KeyCtrlQ || isCtrlRune(ev, "q") { s.Fini() os.Exit(0) } else if ev.Key() == tcell.KeyCtrlZ || isCtrlRune(ev, "z") { // CtrlZ doesn't really suspend the process, but we use it to execute a subshell. if err := s.Suspend(); err == nil { fmt.Printf("Executing shell (%s -l)...\n", shell) fmt.Printf("Exit the shell to return to the demo.\n") c := exec.Command(shell, "-l") // NB: -l works for cmd.exe too (ignored) c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr c.Run() if err := s.Resume(); err != nil { panic("failed to resume: " + err.Error()) } } } else { ecnt = 0 if ev.Str() == "C" || ev.Str() == "c" { s.Clear() } } if ks := fmt.Sprintf("K%d", kcnt); w >= len(ks) { s.PutStrStyled(w-len(ks), h-1, ks, st) } else { s.PutStrStyled(0, h-1, ks, st) } lks = ev.Name() case *tcell.EventPaste: pasting = ev.Start() if pasting { pstr = "" } case *tcell.EventMouse: x, y := ev.Position() button := ev.Buttons() mods := ev.Modifiers() if mods&tcell.ModShift != 0 { bstr += " Shift" } if mods&tcell.ModCtrl != 0 { bstr += " Ctrl" } if mods&tcell.ModAlt != 0 { bstr += " Alt" } if mods&tcell.ModMeta != 0 { bstr += " Meta" } for i := uint(0); i < 8; i++ { if int(button)&(1<= 0 { bg := theme[lchar%8] fg := color.Black drawBox(s, ox, oy, x, y, up.Background(bg).Foreground(fg), lchar) ox, oy = -1, -1 bx, by = -1, -1 } case tcell.Button1: ch = '1' case tcell.Button2: ch = '2' case tcell.Button3: ch = '3' case tcell.Button4: ch = '4' case tcell.Button5: ch = '5' case tcell.Button6: ch = '6' case tcell.Button7: ch = '7' case tcell.Button8: ch = '8' default: ch = '*' } if button != tcell.ButtonNone { bx, by = x, y } lchar = ch s.Put(w-1, h-1, "M", st) mx, my = x, y case *tcell.EventFocus: focus = ev.Focused default: s.Put(w-1, h-1, "X", st) } if ox >= 0 && bx >= 0 { drawSelect(s, ox, oy, bx, by, true) } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/notify.go000066400000000000000000000043361520475227200233510ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) func displayHelloWorld(s tcell.Screen, secs int) { w, h := s.Size() s.Clear() style := tcell.StyleDefault.Foreground(color.CadetBlue.TrueColor()).Background(color.White) msg := "Notification Demo" s.PutStrStyled((w-len(msg))/2, h/2-1, msg, style) msg = "(Minimize This Window)" s.PutStrStyled((w-len(msg))/2, h/2+1, msg, style) if secs > 0 { msg = fmt.Sprintf("Incoming in %d Seconds", secs) } else { msg = "Notification Sent!" } s.PutStr((w-len(msg))/2, h/2+3, msg) msg = "Press ESC to exit, ENTER to restart." s.PutStr((w-len(msg))/2, h/2+5, msg) s.Show() } // This program just prints "Hello, World!". Press ESC to exit. func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e := s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defStyle := tcell.StyleDefault. Background(color.Black). Foreground(color.White) s.SetStyle(defStyle) when := 10 displayHelloWorld(s, when) ticker := time.NewTicker(time.Second) var ev tcell.Event for { select { case ev = <-s.EventQ(): case <-ticker.C: if when > 0 { when-- if when == 0 { s.ShowNotification("Ding Dong!", "The wicked witch is dead.") } } displayHelloWorld(s, when) continue } switch ev := ev.(type) { case *tcell.EventResize: s.Sync() case *tcell.EventKey: switch ev.Key() { case tcell.KeyEnter: when = 10 case tcell.KeyEscape: s.Fini() os.Exit(0) } } displayHelloWorld(s, when) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/setsize.go000066400000000000000000000037611520475227200235300ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/encoding" ) func displayDemo(s tcell.Screen) { w, h := s.Size() s.Clear() style := tcell.StyleDefault.Foreground(color.CadetBlue.TrueColor()).Background(color.White) sizeStr := fmt.Sprintf("%d x %d", w, h) helpStr := "Use cursors to resize, ESC to exit." s.PutStrStyled((w-len(sizeStr))/2, h/2, sizeStr, style) s.PutStr((w-len(helpStr))/2, h/2+1, helpStr) s.Show() } // This program just prints "Hello, World!". Press ESC to exit. func main() { encoding.Register() s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e := s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defStyle := tcell.StyleDefault. Background(color.Black). Foreground(color.White) s.SetStyle(defStyle) displayDemo(s) for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventResize: s.Sync() displayDemo(s) case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape: s.Fini() os.Exit(0) case tcell.KeyRight: w, h := s.Size() s.SetSize(w+1, h) case tcell.KeyLeft: w, h := s.Size() s.SetSize(w-1, h) case tcell.KeyUp: w, h := s.Size() s.SetSize(w, h-1) case tcell.KeyDown: w, h := s.Size() s.SetSize(w, h+1) } } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/showcolor.go000066400000000000000000000071231520475227200240550ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // colors just displays a single centered rectangle that should pulse // through available colors. It uses the RGB color cube, bumping at // predefined larger intervals (values of about 8) in order that the // changes happen quickly enough to be appreciable. // // Press ESC to exit the program. package main import ( "fmt" "math/rand" "os" "strconv" "strings" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) var red = int32(rand.Int() % 256) var grn = int32(rand.Int() % 256) var blu = int32(rand.Int() % 256) var inc = int32(8) // rate of color change var redi = int32(inc) var grni = int32(inc) var blui = int32(inc) func makebox(s tcell.Screen, name string, fill color.Color) { w, h := s.Size() if w == 0 || h == 0 { return } lh := h - 3 lw := w lx := 0 ly := 0 st := tcell.StyleDefault s.Fill(' ', st) bg := st.Background(fill) for row := 0; row < lh; row++ { for col := 0; col < lw; col++ { s.Put(lx+col, ly+row, " ", bg) } } cn := fill.Name() if cn == "" { cn = "rgb" } msg := fmt.Sprintf("This is %s (#%06x, %s). Terminal supports %d colors.", name, fill.Hex(), cn, s.Colors()) if len(msg) < w { lx = (w - len(msg)) / 2 } else { lx = 0 } s.PutStrStyled(lx, lh+1, msg, st) s.Show() } func fatal(v string, args ...any) { fmt.Fprintf(os.Stderr, v+"\n", args...) os.Exit(1) } func main() { if len(os.Args) != 2 { fatal("usage: %s color", os.Args[0]) } var color_ color.Color if strings.HasPrefix(os.Args[1], "#") { name := os.Args[1][1:] if len(name) == 3 { hex, err := strconv.ParseUint(name, 16, 12) if err != nil { fatal("invalid color specification") } // expand 12 bit color to 24 bit r := int32((hex & 0xf00) >> 8) g := int32((hex & 0x0f0) >> 4) b := int32(hex & 0x00f) r = r<<4 + r g = g<<4 + g b = b<<4 + b color_ = color.NewRGBColor(r, g, b) } else if len(name) == 6 { hex, err := strconv.ParseUint(name, 16, 32) if err != nil { fatal("invalid color specification") } color_ = color.NewHexColor(int32(hex)) } else { fatal("invalid color specification") } } else if val, err := strconv.Atoi(os.Args[1]); err == nil { color_ = color.Black + color.Color(val) } else if val, ok := color.Names[strings.ToLower(os.Args[1])]; ok { color_ = val } else { fatal("invalid color specification") } tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) s, e := tcell.NewScreen() if e != nil { fatal("new screen: %v", e.Error()) } if e = s.Init(); e != nil { fatal("new screen: %v", e.Error()) } s.SetStyle(tcell.StyleDefault. Foreground(color.Black). Background(color.White)) s.Clear() quit := make(chan struct{}) go func() { for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape, tcell.KeyEnter: close(quit) return case tcell.KeyCtrlL: s.Sync() } case *tcell.EventResize: s.Sync() } } }() makebox(s, os.Args[1], color_) <-quit s.Fini() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/sixel.go000066400000000000000000000075321520475227200231660ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // sixel displays a sixel and demonstrates how to use direct drawing package main import ( "bytes" "fmt" "image" "image/png" "io" "log" "math" "os" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/encoding" "github.com/mattn/go-sixel" ) type imageData struct { width int // width in pixels height int // height in pixels data *bytes.Buffer } func displayHelloWorld(s tcell.Screen) { w, h := s.Size() s.Clear() style := tcell.StyleDefault.Foreground(color.CadetBlue.TrueColor()).Background(color.White) s.PutStrStyled(w/2-7, h/2, "Hello, World!", style) s.PutStr(w/2-9, h/2+1, "Press ESC to exit.") s.PutStr(w/2-18, h/2+2, "Press Enter to toggle sixel lock.") s.Show() } func displaySixel(s tcell.Screen, img *imageData, lock bool) { tty, ok := s.Tty() if !ok { s.Fini() log.Fatal("not a terminal") } ws, err := tty.WindowSize() if err != nil { s.Fini() log.Fatal(err) } // Get the dimensions of a single cell cw, ch := ws.CellDimensions() if cw == 0 || ch == 0 { s.Fini() log.Fatal("terminal does not support sixel graphics") return } // Calculate the image dimensions in cells. We round up to prevent // drawing on a partially filled cell sixelWidth := int(math.Ceil(float64(img.width) / float64(cw))) sixelHeight := int(math.Ceil(float64(img.height) / float64(ch))) sixelX := ws.Width/2 - (sixelWidth / 2) // Center the image horizontally sixelY := ws.Height/2 - sixelHeight - 2 if sixelY < 0 { sixelY = 0 } // Lock the region where we will draw the sixel, this prevents tcell // from drawing over this area s.LockRegion(sixelX, sixelY, sixelWidth, sixelHeight, lock) s.PutStr(sixelX, sixelY, "This text is behind") s.PutStr(sixelX, sixelY+1, " the sixel") setCursorPosition = "\x1b[%[1]d;%[2]dH" // Move the cursor to our draw position io.WriteString(tty, fmt.Sprintf(setCursorPosition, sixelY+1, sixelX+1)) // Draw the sixel data io.WriteString(tty, img.data.String()) s.Show() } func loadImage(path string) (image.Image, error) { f, err := os.Open(path) if err != nil { return nil, err } return png.Decode(f) } func main() { encoding.Register() s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e := s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defStyle := tcell.StyleDefault. Background(color.Black). Foreground(color.White) s.SetStyle(defStyle) raw, err := loadImage("./logos/tcell.png") if err != nil { s.Fini() log.Println("couldn't load image. try running from the root directory") log.Fatalf(" go run ./_demos/sixel.go") } img := &imageData{ width: raw.Bounds().Dx(), height: raw.Bounds().Dy(), data: bytes.NewBuffer(nil), } enc := sixel.NewEncoder(img.data) if err := enc.Encode(raw); err != nil { s.Fini() log.Fatal(err) } lock := true displayHelloWorld(s) displaySixel(s, img, lock) for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventResize: s.Sync() displayHelloWorld(s) displaySixel(s, img, lock) case *tcell.EventKey: if ev.Key() == tcell.KeyEscape { s.Fini() os.Exit(0) } if ev.Key() == tcell.KeyEnter { lock = !lock displaySixel(s, img, lock) } } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/_demos/stress.go000066400000000000000000000052411520475227200233600ustar00rootroot00000000000000//go:build ignore // +build ignore // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // stress will fill the whole screen with random characters, colors and // formatting. The frames are pre-generated to draw as fast as possible. // ESC and Ctrl-C will end the program. Note that resizing isn't supported. package main import ( "fmt" "math/rand" "time" "github.com/gdamore/tcell/v3" ) func main() { screen, err := tcell.NewScreen() if err != nil { panic(err) } if err := screen.Init(); err != nil { panic(err) } var frames int type cell struct { c string style tcell.Style } width, height := screen.Size() glyphs := []string{"@", "#", "&", "*", "=", "%", "Z", "A"} attrs := []tcell.AttrMask{tcell.AttrBold, tcell.AttrReverse, tcell.AttrItalic, tcell.AttrNone} // Pre-Generate 100 different frame patterns, so we stress the terminal as // much as possible :D var patterns [][][]*cell for i := 0; i < 100; i++ { pattern := make([][]*cell, height) for h := 0; h < height; h++ { row := make([]*cell, width) for w := 0; w < width; w++ { rF := int32(rand.Int() % 256) gF := int32(rand.Int() % 256) bF := int32(rand.Int() % 256) rB := int32(rand.Int() % 256) gB := int32(rand.Int() % 256) bB := int32(rand.Int() % 256) row[w] = &cell{ c: glyphs[rand.Int()%len(glyphs)], style: tcell.StyleDefault. Attributes(attrs[rand.Int()%len(attrs)]). Background(tcell.NewRGBColor(rB, gB, bB)). Foreground(tcell.NewRGBColor(rF, gF, bF)), } } pattern[h] = row } patterns = append(patterns, pattern) } startTime := time.Now() loop: for { select { case event := <-screen.EventQ(): if event, ok := event.(*tcell.EventKey); ok { if event.Key() == tcell.KeyCtrlC || event.Key() == tcell.KeyESC { break loop } } default: } pattern := patterns[frames%len(patterns)] for h := 0; h < height; h++ { for w := 0; w < width; w++ { c := pattern[h][w] screen.Put(w, h, c.c, c.style) } } screen.Show() frames++ } duration := time.Since(startTime) screen.Fini() fps := int(float64(frames) / duration.Seconds()) fmt.Println("FPS:", fps) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/attr.go000066400000000000000000000025731520475227200215460ustar00rootroot00000000000000// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // AttrMask represents a mask of text attributes, apart from color. // Note that support for attributes may vary widely across terminals. // Deprecated: This should not be used directly by applications. // Instead use the accessor functions on the style. (In particular the // details of the encoding are subject to change.) type AttrMask uint16 // Attributes are not colors, but affect the display of text. They can // be combined, in some cases, but not others. (E.g. you can have Dim Italic, // but only CurlyUnderline cannot be mixed with DottedUnderline.) const ( AttrBold AttrMask = 1 << iota AttrBlink AttrReverse AttrDim AttrItalic AttrStrikeThrough AttrInvalid AttrMask = 1 << 15 // Mark the style or attributes invalid AttrNone AttrMask = 0 // Just normal text. ) golang-github-gdamore-tcell.v3-3.4.0+dfsg/cell.go000066400000000000000000000152151520475227200215100ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell type cell struct { currStr string lastStr string currStyle Style lastStyle Style width int lock bool } func (c *cell) setDirty(dirty bool) { if dirty { // Empty cells use currStr == "" until they are first drawn, at which // point SetDirty(false) normalizes them to a space. Using "" as the // dirty marker for an untouched empty cell would therefore leave // lastStr == currStr and fail to force a redraw. if c.currStr == "" { c.lastStr = " " } else { c.lastStr = "" } } else { if c.currStr == "" { c.currStr = " " } c.lastStr = c.currStr c.lastStyle = c.currStyle } } // CellBuffer represents a two-dimensional array of character cells. // This is primarily intended for use by Screen implementers; it // contains much of the common code they need. To create one, just // declare a variable of its type; no explicit initialization is necessary. // // CellBuffer is not thread safe. type CellBuffer struct { w int h int cells []cell sanitizeContent bool } // Put a single styled grapheme using the given string and style // at the same location. Note that only the first grapheme in the string // will be displayed, using only the 1 or 2 (depending on width) cells // located at x, y. It returns the rest of the string, and the width used. func (cb *CellBuffer) Put(x int, y int, str string, style Style) (string, int) { if cb.sanitizeContent { str = stripOSCControlsIfNeeded(str) } return cb.put(x, y, str, style) } func (cb *CellBuffer) put(x int, y int, str string, style Style) (string, int) { var width int = 0 if x >= 0 && y >= 0 && x < cb.w && y < cb.h { var cl string c := &cb.cells[(y*cb.w)+x] g := textWidthOptions.StringGraphemes(str) for width == 0 && g.Next() { cluster := g.Value() cl += cluster width = g.Width() str = str[len(cluster):] } // Wide characters: we want to mark the "wide" cells // dirty as well as the base cell, to make sure we consider // both cells as dirty together. We only need to do this // if we're changing content if width > 1 && cl != c.currStr { // Prevent unnecessary bounds checks for first cell, since we already // received that one. c.setDirty(true) for i := 1; i < width; i++ { cb.SetDirty(x+i, y, true) } } c.currStr = cl c.width = width if style.fg == ColorNone { style.fg = c.currStyle.fg } if style.bg == ColorNone { style.bg = c.currStyle.bg } c.currStyle = style } return str, width } // Get the contents of a character cell (or two adjacent cells), including the // the style and the display width in cells. (The width can be either 1, normally, // or 2 for East Asian full-width characters. If the width is 0, then the cell is // is empty.) func (cb *CellBuffer) Get(x, y int) (string, Style, int) { var style Style var width int var str string if x >= 0 && y >= 0 && x < cb.w && y < cb.h { c := &cb.cells[(y*cb.w)+x] str, style = c.currStr, c.currStyle if width = c.width; width == 0 || str == "" { width = 1 str = " " } } return str, style, width } // Size returns the (width, height) in cells of the buffer. func (cb *CellBuffer) Size() (int, int) { return cb.w, cb.h } // Invalidate marks all characters within the buffer as dirty. func (cb *CellBuffer) Invalidate() { for i := range cb.cells { cb.cells[i].setDirty(true) } } // Dirty checks if a character at the given location needs to be // refreshed on the physical display. This returns true if the cell // content is different since the last time it was marked clean. func (cb *CellBuffer) Dirty(x, y int) bool { if x >= 0 && y >= 0 && x < cb.w && y < cb.h { c := &cb.cells[(y*cb.w)+x] if c.lock { return false } if c.lastStyle != c.currStyle { return true } if c.lastStr != c.currStr { return true } } return false } // SetDirty is normally used to indicate that a cell has // been displayed (in which case dirty is false), or to manually // force a cell to be marked dirty. func (cb *CellBuffer) SetDirty(x, y int, dirty bool) { if x >= 0 && y >= 0 && x < cb.w && y < cb.h { c := &cb.cells[(y*cb.w)+x] c.setDirty(dirty) } } // LockCell locks a cell from being drawn, effectively marking it "clean" until // the lock is removed. This can be used to prevent tcell from drawing a given // cell, even if the underlying content has changed. For example, when drawing a // sixel graphic directly to a TTY screen an implementer must lock the region // underneath the graphic to prevent tcell from drawing on top of the graphic. func (cb *CellBuffer) LockCell(x, y int) { if x < 0 || y < 0 { return } if x >= cb.w || y >= cb.h { return } c := &cb.cells[(y*cb.w)+x] c.lock = true } // UnlockCell removes a lock from the cell and marks it as dirty func (cb *CellBuffer) UnlockCell(x, y int) { if x < 0 || y < 0 { return } if x >= cb.w || y >= cb.h { return } c := &cb.cells[(y*cb.w)+x] c.lock = false cb.SetDirty(x, y, true) } // Resize is used to resize the cells array, with different dimensions, // while preserving the original contents. The cells will be invalidated // so that they can be redrawn. func (cb *CellBuffer) Resize(w, h int) { if cb.h == h && cb.w == w { return } newc := make([]cell, w*h) for y := 0; y < h && y < cb.h; y++ { for x := 0; x < w && x < cb.w; x++ { oc := &cb.cells[(y*cb.w)+x] nc := &newc[(y*w)+x] nc.currStr = oc.currStr nc.currStyle = oc.currStyle nc.width = oc.width nc.lastStr = "" } } cb.cells = newc cb.h = h cb.w = w } // Fill fills the entire cell buffer array with the specified character // and style. Normally choose ' ' to clear the screen. This API doesn't // support combining characters, or characters with a width larger than one. // If either the foreground or background are ColorNone, then the respective // color is unchanged. func (cb *CellBuffer) Fill(r rune, style Style) { for i := range cb.cells { c := &cb.cells[i] c.currStr = string(r) cs := style if cs.fg == ColorNone { cs.fg = c.currStyle.fg } if cs.bg == ColorNone { cs.bg = c.currStyle.bg } c.currStyle = cs c.width = 1 } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/cell_bench_test.go000066400000000000000000000032621520475227200237050ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "testing" ) func BenchmarkCellBufferPutCurrent(b *testing.B) { benchCellBufferPut(b, "current", false, func(cb *CellBuffer, x, y int, str string, style Style) (string, int) { return cb.Put(x, y, str, style) }) } func BenchmarkCellBufferPutSanitizedCurrent(b *testing.B) { benchCellBufferPut(b, "sanitized", true, func(cb *CellBuffer, x, y int, str string, style Style) (string, int) { return cb.Put(x, y, str, style) }) } func benchCellBufferPut(b *testing.B, name string, sanitize bool, put func(*CellBuffer, int, int, string, Style) (string, int)) { cases := []struct { name string str string }{ {name: "ascii", str: "Hello, terminal"}, {name: "combining", str: "e\u0301"}, {name: "wide", str: "宽"}, {name: "emoji", str: "👩‍🚀"}, } for _, tc := range cases { b.Run(name+"/"+tc.name, func(b *testing.B) { cb := &CellBuffer{w: 8, h: 1, cells: make([]cell, 8), sanitizeContent: sanitize} style := Style{} b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { cb.cells[0] = cell{} _, _ = put(cb, 0, 0, tc.str, style) } }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/cell_test.go000066400000000000000000000131421520475227200225440ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import "testing" func TestCellBufferPutAndSanitize(t *testing.T) { t.Run("unsanitized", func(t *testing.T) { cb := &CellBuffer{w: 4, h: 1, cells: make([]cell, 4)} rest, width := cb.Put(0, 0, "A\x1bB", StyleDefault) if rest != "\x1bB" { t.Fatalf("unexpected remainder: %q", rest) } if width != 1 { t.Fatalf("unexpected width: %d", width) } if got, _, gotWidth := cb.Get(0, 0); got != "A" || gotWidth != 1 { t.Fatalf("unexpected cell content: %q width=%d", got, gotWidth) } }) t.Run("sanitized", func(t *testing.T) { cb := &CellBuffer{w: 4, h: 1, cells: make([]cell, 4), sanitizeContent: true} rest, width := cb.Put(0, 0, "A\x1bB", StyleDefault) if rest != "B" { t.Fatalf("unexpected sanitized remainder: %q", rest) } if width != 1 { t.Fatalf("unexpected width: %d", width) } if got, _, gotWidth := cb.Get(0, 0); got != "A" || gotWidth != 1 { t.Fatalf("unexpected sanitized cell content: %q width=%d", got, gotWidth) } }) } func TestCellBufferWideAndColorNone(t *testing.T) { cb := &CellBuffer{w: 3, h: 1, cells: make([]cell, 3)} base := StyleDefault.Foreground(ColorRed).Background(ColorBlue) cb.Put(0, 0, "a", base) cb.SetDirty(0, 0, false) cb.Put(1, 0, "z", base) cb.SetDirty(1, 0, false) reused := Style{fg: ColorNone, bg: ColorNone} rest, width := cb.Put(0, 0, "你", reused) if rest != "" { t.Fatalf("unexpected remainder for wide rune: %q", rest) } if width != 2 { t.Fatalf("unexpected width for wide rune: %d", width) } if !cb.Dirty(0, 0) { t.Fatalf("wide rune should dirty the base cell") } if !cb.Dirty(1, 0) { t.Fatalf("wide rune should dirty the second cell") } got, gotStyle, gotWidth := cb.Get(0, 0) if got != "你" || gotWidth != 2 { t.Fatalf("unexpected wide cell content: %q width=%d", got, gotWidth) } if gotStyle.GetForeground() != ColorRed || gotStyle.GetBackground() != ColorBlue { t.Fatalf("ColorNone should preserve existing colors, got fg=%v bg=%v", gotStyle.GetForeground(), gotStyle.GetBackground()) } } func TestCellBufferDirtyState(t *testing.T) { cb := &CellBuffer{w: 2, h: 1, cells: make([]cell, 2)} cb.cells[0].setDirty(false) if got := cb.cells[0].lastStr; got != " " { t.Fatalf("setDirty(false) should normalize empty content to space, got %q", got) } if got := cb.cells[0].lastStyle; got != cb.cells[0].currStyle { t.Fatalf("setDirty(false) should copy style") } cb.cells[0].setDirty(true) if got := cb.cells[0].lastStr; got != "" { t.Fatalf("setDirty(true) should clear lastStr, got %q", got) } cb.Put(0, 0, "x", StyleDefault) cb.SetDirty(0, 0, false) if cb.Dirty(0, 0) { t.Fatalf("clean cell should not be dirty") } cb.cells[0].currStyle = cb.cells[0].currStyle.Bold(true) if !cb.Dirty(0, 0) { t.Fatalf("style change should make cell dirty") } cb.SetDirty(0, 0, false) cb.cells[0].currStyle = cb.cells[0].lastStyle cb.cells[0].currStr = "y" if !cb.Dirty(0, 0) { t.Fatalf("content change should make cell dirty") } } func TestCellBufferLockResizeFill(t *testing.T) { cb := &CellBuffer{w: 2, h: 2, cells: make([]cell, 4)} cb.Fill('x', StyleDefault) if got, _, _ := cb.Get(1, 1); got != "x" { t.Fatalf("unexpected fill content: %q", got) } cb.LockCell(1, 1) if cb.Dirty(1, 1) { t.Fatalf("locked cell should not be dirty") } cb.UnlockCell(1, 1) if !cb.Dirty(1, 1) { t.Fatalf("unlocked cell should be dirty") } cb.Put(0, 0, "ab", StyleDefault) cb.Resize(3, 1) if got, _, _ := cb.Get(0, 0); got != "a" { t.Fatalf("resize should preserve content, got %q", got) } if _, _, width := cb.Get(0, 0); width != 1 { t.Fatalf("unexpected width after resize: %d", width) } cb.Invalidate() if !cb.Dirty(0, 0) { t.Fatalf("invalidate should mark content dirty") } } func TestCellBufferUnlockUntouchedCellMarksDirty(t *testing.T) { cb := &CellBuffer{w: 1, h: 1, cells: make([]cell, 1)} cb.LockCell(0, 0) cb.UnlockCell(0, 0) if !cb.Dirty(0, 0) { t.Fatalf("unlocking an untouched blank cell should mark it dirty") } } func TestCellBufferOutOfRangeAndResizeNoop(t *testing.T) { cb := &CellBuffer{w: 2, h: 1, cells: make([]cell, 2)} if rest, width := cb.Put(-1, 0, "x", StyleDefault); rest != "x" || width != 0 { t.Fatalf("out-of-range put should be a no-op, got rest=%q width=%d", rest, width) } cb.Put(0, 0, "x", StyleDefault.Foreground(ColorRed).Background(ColorBlue)) cb.SetDirty(0, 0, false) if rest, width := cb.Put(0, 0, "", StyleDefault.Foreground(ColorGreen)); rest != "" || width != 0 { t.Fatalf("empty put should be a no-op, got rest=%q width=%d", rest, width) } afterStr, afterStyle, afterWidth := cb.Get(0, 0) if afterStr != " " || afterStyle.GetForeground() != ColorGreen || afterWidth != 1 { t.Fatalf("empty put should clear the cell and apply the new style: got=%q/%v/%d", afterStr, afterStyle, afterWidth) } if got := cb.Dirty(0, 0); !got { t.Fatalf("empty put should mark the cell dirty") } cb.LockCell(-1, 0) cb.LockCell(2, 0) cb.UnlockCell(-1, 0) cb.UnlockCell(2, 0) cb.Resize(2, 1) if w, h := cb.Size(); w != 2 || h != 1 { t.Fatalf("resize no-op changed size to %dx%d", w, h) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/charset_plan9.go000066400000000000000000000013701520475227200233220ustar00rootroot00000000000000//go:build plan9 // +build plan9 // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // Plan 9 uses UTF-8 system-wide, so we return "UTF-8" unconditionally. func getCharset() string { return "UTF-8" } golang-github-gdamore-tcell.v3-3.4.0+dfsg/charset_unix.go000066400000000000000000000025511520475227200232640ustar00rootroot00000000000000//go:build unix // +build unix // Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "os" "strings" ) func getCharset() string { // Determine the character set. This can help us later. // Per POSIX, we search for LC_ALL first, then LC_CTYPE, and // finally LANG. First one set wins. locale := "" if locale = os.Getenv("LC_ALL"); locale == "" { if locale = os.Getenv("LC_CTYPE"); locale == "" { locale = os.Getenv("LANG") } } if locale == "POSIX" || locale == "C" { return "US-ASCII" } if i := strings.IndexRune(locale, '@'); i >= 0 { locale = locale[:i] } if i := strings.IndexRune(locale, '.'); i >= 0 { locale = locale[i+1:] } else { // Default assumption, and on Linux we can see LC_ALL // without a character set, which we assume implies UTF-8. return "UTF-8" } return locale } golang-github-gdamore-tcell.v3-3.4.0+dfsg/charset_unix_test.go000066400000000000000000000024671520475227200243310ustar00rootroot00000000000000//go:build unix // +build unix package tcell import "testing" // Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. func TestGetCharset(t *testing.T) { cases := []struct { name string locale string charset string }{ {"POSIX", "POSIX", "US-ASCII"}, {"C", "C", "US-ASCII"}, {"C Unicode", "C.UTF-8", "UTF-8"}, {"Old UK euro", "UK.ISO-8859-1@euro", "ISO-8859-1"}, {"Unset", "", "UTF-8"}, } for _, tc := range cases { t.Setenv("LANG", "") t.Setenv("LC_ALL", "") t.Setenv("LC_CTYPE", "") for _, env := range []string{"LANG", "LC_ALL", "LC_CTYPE"} { t.Run(tc.name+" "+env, func(t *testing.T) { t.Setenv(env, tc.locale) if actual := getCharset(); actual != tc.charset { t.Errorf("charset %q did not match %q", actual, tc.charset) } }) } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/charset_windows.go000066400000000000000000000012641520475227200237730ustar00rootroot00000000000000//go:build windows // +build windows // Copyright 2015 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell func getCharset() string { return "UTF-8" } golang-github-gdamore-tcell.v3-3.4.0+dfsg/color.go000066400000000000000000000516331520475227200217130ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( ic "image/color" "github.com/gdamore/tcell/v3/color" ) // Note that the entire contents of this file should be considered deprecated // in favor of the color subpackage. (This also means others can use the color // package without importing the entirety of tcell into their binaries.) // Color represents a color. The low numeric values are the same as used // by ECMA-48, and beyond that XTerm. // // Note that on various terminals colors may be approximated however, or // not supported at all. If no suitable representation for a color is known, // the library will simply not set any color, deferring to whatever default // attributes the terminal uses. type Color = color.Color const ( // ColorDefault is used to leave the Color unchanged from whatever // system or terminal default may exist. It's also the zero value. ColorDefault = color.Default // ColorValid is used to indicate the color value is actually // valid (initialized). // Deprecated: Use color.IsValid instead. ColorValid = color.IsValid // ColorIsRGB is used to indicate that the numeric value is not // a known color constant, but rather an RGB value. // Deprecated: Use color.IsRGB instead. ColorIsRGB = color.IsRGB // ColorSpecial is a flag used to indicate that the values have // special meaning, and live outside of the color space(s). // Deprecated. ColorSpecial = color.IsSpecial ) // Note that the order of these options is important -- it follows the // definitions used by ECMA and XTerm. Hence any further named colors // must begin at a value not less than 256. // // Deprecated: Use color.XXX symbols instead. const ( ColorBlack = color.XTerm0 ColorMaroon = color.XTerm1 ColorGreen = color.XTerm2 ColorOlive = color.XTerm3 ColorNavy = color.XTerm4 ColorPurple = color.XTerm5 ColorTeal = color.XTerm6 ColorSilver = color.XTerm7 ColorGray = color.XTerm8 ColorRed = color.XTerm9 ColorLime = color.XTerm10 ColorYellow = color.XTerm11 ColorBlue = color.XTerm12 ColorFuchsia = color.XTerm13 ColorAqua = color.XTerm14 ColorWhite = color.XTerm15 Color16 = color.XTerm16 Color17 = color.XTerm17 Color18 = color.XTerm18 Color19 = color.XTerm19 Color20 = color.XTerm20 Color21 = color.XTerm21 Color22 = color.XTerm22 Color23 = color.XTerm23 Color24 = color.XTerm24 Color25 = color.XTerm25 Color26 = color.XTerm26 Color27 = color.XTerm27 Color28 = color.XTerm28 Color29 = color.XTerm29 Color30 = color.XTerm30 Color31 = color.XTerm31 Color32 = color.XTerm32 Color33 = color.XTerm33 Color34 = color.XTerm34 Color35 = color.XTerm35 Color36 = color.XTerm36 Color37 = color.XTerm37 Color38 = color.XTerm38 Color39 = color.XTerm39 Color40 = color.XTerm40 Color41 = color.XTerm41 Color42 = color.XTerm42 Color43 = color.XTerm43 Color44 = color.XTerm44 Color45 = color.XTerm45 Color46 = color.XTerm46 Color47 = color.XTerm47 Color48 = color.XTerm48 Color49 = color.XTerm49 Color50 = color.XTerm50 Color51 = color.XTerm51 Color52 = color.XTerm52 Color53 = color.XTerm53 Color54 = color.XTerm54 Color55 = color.XTerm55 Color56 = color.XTerm56 Color57 = color.XTerm57 Color58 = color.XTerm58 Color59 = color.XTerm59 Color60 = color.XTerm60 Color61 = color.XTerm61 Color62 = color.XTerm62 Color63 = color.XTerm63 Color64 = color.XTerm64 Color65 = color.XTerm65 Color66 = color.XTerm66 Color67 = color.XTerm67 Color68 = color.XTerm68 Color69 = color.XTerm69 Color70 = color.XTerm70 Color71 = color.XTerm71 Color72 = color.XTerm72 Color73 = color.XTerm73 Color74 = color.XTerm74 Color75 = color.XTerm75 Color76 = color.XTerm76 Color77 = color.XTerm77 Color78 = color.XTerm78 Color79 = color.XTerm79 Color80 = color.XTerm80 Color81 = color.XTerm81 Color82 = color.XTerm82 Color83 = color.XTerm83 Color84 = color.XTerm84 Color85 = color.XTerm85 Color86 = color.XTerm86 Color87 = color.XTerm87 Color88 = color.XTerm88 Color89 = color.XTerm89 Color90 = color.XTerm90 Color91 = color.XTerm91 Color92 = color.XTerm92 Color93 = color.XTerm93 Color94 = color.XTerm94 Color95 = color.XTerm95 Color96 = color.XTerm96 Color97 = color.XTerm97 Color98 = color.XTerm98 Color99 = color.XTerm99 Color100 = color.XTerm100 Color101 = color.XTerm101 Color102 = color.XTerm102 Color103 = color.XTerm103 Color104 = color.XTerm104 Color105 = color.XTerm105 Color106 = color.XTerm106 Color107 = color.XTerm107 Color108 = color.XTerm108 Color109 = color.XTerm109 Color110 = color.XTerm110 Color111 = color.XTerm111 Color112 = color.XTerm112 Color113 = color.XTerm113 Color114 = color.XTerm114 Color115 = color.XTerm115 Color116 = color.XTerm116 Color117 = color.XTerm117 Color118 = color.XTerm118 Color119 = color.XTerm119 Color120 = color.XTerm120 Color121 = color.XTerm121 Color122 = color.XTerm122 Color123 = color.XTerm123 Color124 = color.XTerm124 Color125 = color.XTerm125 Color126 = color.XTerm126 Color127 = color.XTerm127 Color128 = color.XTerm128 Color129 = color.XTerm129 Color130 = color.XTerm130 Color131 = color.XTerm131 Color132 = color.XTerm132 Color133 = color.XTerm133 Color134 = color.XTerm134 Color135 = color.XTerm135 Color136 = color.XTerm136 Color137 = color.XTerm137 Color138 = color.XTerm138 Color139 = color.XTerm139 Color140 = color.XTerm140 Color141 = color.XTerm141 Color142 = color.XTerm142 Color143 = color.XTerm143 Color144 = color.XTerm144 Color145 = color.XTerm145 Color146 = color.XTerm146 Color147 = color.XTerm147 Color148 = color.XTerm148 Color149 = color.XTerm149 Color150 = color.XTerm150 Color151 = color.XTerm151 Color152 = color.XTerm152 Color153 = color.XTerm153 Color154 = color.XTerm154 Color155 = color.XTerm155 Color156 = color.XTerm156 Color157 = color.XTerm157 Color158 = color.XTerm158 Color159 = color.XTerm159 Color160 = color.XTerm160 Color161 = color.XTerm161 Color162 = color.XTerm162 Color163 = color.XTerm163 Color164 = color.XTerm164 Color165 = color.XTerm165 Color166 = color.XTerm166 Color167 = color.XTerm167 Color168 = color.XTerm168 Color169 = color.XTerm169 Color170 = color.XTerm170 Color171 = color.XTerm171 Color172 = color.XTerm172 Color173 = color.XTerm173 Color174 = color.XTerm174 Color175 = color.XTerm175 Color176 = color.XTerm176 Color177 = color.XTerm177 Color178 = color.XTerm178 Color179 = color.XTerm179 Color180 = color.XTerm180 Color181 = color.XTerm181 Color182 = color.XTerm182 Color183 = color.XTerm183 Color184 = color.XTerm184 Color185 = color.XTerm185 Color186 = color.XTerm186 Color187 = color.XTerm187 Color188 = color.XTerm188 Color189 = color.XTerm189 Color190 = color.XTerm190 Color191 = color.XTerm191 Color192 = color.XTerm192 Color193 = color.XTerm193 Color194 = color.XTerm194 Color195 = color.XTerm195 Color196 = color.XTerm196 Color197 = color.XTerm197 Color198 = color.XTerm198 Color199 = color.XTerm199 Color200 = color.XTerm200 Color201 = color.XTerm201 Color202 = color.XTerm202 Color203 = color.XTerm203 Color204 = color.XTerm204 Color205 = color.XTerm205 Color206 = color.XTerm206 Color207 = color.XTerm207 Color208 = color.XTerm208 Color209 = color.XTerm209 Color210 = color.XTerm210 Color211 = color.XTerm211 Color212 = color.XTerm212 Color213 = color.XTerm213 Color214 = color.XTerm214 Color215 = color.XTerm215 Color216 = color.XTerm216 Color217 = color.XTerm217 Color218 = color.XTerm218 Color219 = color.XTerm219 Color220 = color.XTerm220 Color221 = color.XTerm221 Color222 = color.XTerm222 Color223 = color.XTerm223 Color224 = color.XTerm224 Color225 = color.XTerm225 Color226 = color.XTerm226 Color227 = color.XTerm227 Color228 = color.XTerm228 Color229 = color.XTerm229 Color230 = color.XTerm230 Color231 = color.XTerm231 Color232 = color.XTerm232 Color233 = color.XTerm233 Color234 = color.XTerm234 Color235 = color.XTerm235 Color236 = color.XTerm236 Color237 = color.XTerm237 Color238 = color.XTerm238 Color239 = color.XTerm239 Color240 = color.XTerm240 Color241 = color.XTerm241 Color242 = color.XTerm242 Color243 = color.XTerm243 Color244 = color.XTerm244 Color245 = color.XTerm245 Color246 = color.XTerm246 Color247 = color.XTerm247 Color248 = color.XTerm248 Color249 = color.XTerm249 Color250 = color.XTerm250 Color251 = color.XTerm251 Color252 = color.XTerm252 Color253 = color.XTerm253 Color254 = color.XTerm254 Color255 = color.XTerm255 ColorAliceBlue = color.AliceBlue ColorAntiqueWhite = color.AntiqueWhite ColorAquaMarine = color.AquaMarine ColorAzure = color.Azure ColorBeige = color.Beige ColorBisque = color.Bisque ColorBlanchedAlmond = color.BlanchedAlmond ColorBlueViolet = color.BlueViolet ColorBrown = color.Brown ColorBurlyWood = color.BurlyWood ColorCadetBlue = color.CadetBlue ColorChartreuse = color.Chartreuse ColorChocolate = color.Chocolate ColorCoral = color.Coral ColorCornflowerBlue = color.CornflowerBlue ColorCornsilk = color.Cornsilk ColorCrimson = color.Crimson ColorDarkBlue = color.DarkBlue ColorDarkCyan = color.DarkCyan ColorDarkGoldenrod = color.DarkGoldenrod ColorDarkGray = color.DarkGray ColorDarkGreen = color.DarkGreen ColorDarkKhaki = color.DarkKhaki ColorDarkMagenta = color.DarkMagenta ColorDarkOliveGreen = color.DarkOliveGreen ColorDarkOrange = color.DarkOrange ColorDarkOrchid = color.DarkOrchid ColorDarkRed = color.DarkRed ColorDarkSalmon = color.DarkSalmon ColorDarkSeaGreen = color.DarkSeaGreen ColorDarkSlateBlue = color.DarkSlateBlue ColorDarkSlateGray = color.DarkSlateGray ColorDarkTurquoise = color.DarkTurquoise ColorDarkViolet = color.DarkViolet ColorDeepPink = color.DeepPink ColorDeepSkyBlue = color.DeepSkyBlue ColorDimGray = color.DimGray ColorDodgerBlue = color.DodgerBlue ColorFireBrick = color.FireBrick ColorFloralWhite = color.FloralWhite ColorForestGreen = color.ForestGreen ColorGainsboro = color.Gainsboro ColorGhostWhite = color.GhostWhite ColorGold = color.Gold ColorGoldenrod = color.Goldenrod ColorGreenYellow = color.GreenYellow ColorHoneydew = color.Honeydew ColorHotPink = color.HotPink ColorIndianRed = color.IndianRed ColorIndigo = color.Indigo ColorIvory = color.Ivory ColorKhaki = color.Khaki ColorLavender = color.Lavender ColorLavenderBlush = color.LavenderBlush ColorLawnGreen = color.LawnGreen ColorLemonChiffon = color.LemonChiffon ColorLightBlue = color.LightBlue ColorLightCoral = color.LightCoral ColorLightCyan = color.LightCyan ColorLightGoldenrodYellow = color.LightGoldenrodYellow ColorLightGray = color.LightGray ColorLightGreen = color.LightGreen ColorLightPink = color.LightPink ColorLightSalmon = color.LightSalmon ColorLightSeaGreen = color.LightSeaGreen ColorLightSkyBlue = color.LightSkyBlue ColorLightSlateGray = color.LightSlateGray ColorLightSteelBlue = color.LightSteelBlue ColorLightYellow = color.LightYellow ColorLimeGreen = color.LimeGreen ColorLinen = color.Linen ColorMediumAquamarine = color.MediumAquamarine ColorMediumBlue = color.MediumBlue ColorMediumOrchid = color.MediumOrchid ColorMediumPurple = color.MediumPurple ColorMediumSeaGreen = color.MediumSeaGreen ColorMediumSlateBlue = color.MediumSlateBlue ColorMediumSpringGreen = color.MediumSpringGreen ColorMediumTurquoise = color.MediumTurquoise ColorMediumVioletRed = color.MediumVioletRed ColorMidnightBlue = color.MidnightBlue ColorMintCream = color.MintCream ColorMistyRose = color.MistyRose ColorMoccasin = color.Moccasin ColorNavajoWhite = color.NavajoWhite ColorOldLace = color.OldLace ColorOliveDrab = color.OliveDrab ColorOrange = color.Orange ColorOrangeRed = color.OrangeRed ColorOrchid = color.Orchid ColorPaleGoldenrod = color.PaleGoldenrod ColorPaleGreen = color.PaleGreen ColorPaleTurquoise = color.PaleTurquoise ColorPaleVioletRed = color.PaleVioletRed ColorPapayaWhip = color.PapayaWhip ColorPeachPuff = color.PeachPuff ColorPeru = color.Peru ColorPink = color.Pink ColorPlum = color.Plum ColorPowderBlue = color.PowderBlue ColorRebeccaPurple = color.RebeccaPurple ColorRosyBrown = color.RosyBrown ColorRoyalBlue = color.RoyalBlue ColorSaddleBrown = color.SaddleBrown ColorSalmon = color.Salmon ColorSandyBrown = color.SandyBrown ColorSeaGreen = color.SeaGreen ColorSeashell = color.Seashell ColorSienna = color.Sienna ColorSkyblue = color.Skyblue ColorSlateBlue = color.SlateBlue ColorSlateGray = color.SlateGray ColorSnow = color.Snow ColorSpringGreen = color.SpringGreen ColorSteelBlue = color.SteelBlue ColorTan = color.Tan ColorThistle = color.Thistle ColorTomato = color.Tomato ColorTurquoise = color.Turquoise ColorViolet = color.Violet ColorWheat = color.Wheat ColorWhiteSmoke = color.WhiteSmoke ColorYellowGreen = color.YellowGreen ) // These are aliases for the color gray, because some of us spell // it as grey. Deprecated: Use color values. const ( ColorGrey = color.Gray ColorDimGrey = color.DimGray ColorDarkGrey = color.DarkGray ColorDarkSlateGrey = color.DarkSlateGray ColorLightGrey = color.LightGray ColorLightSlateGrey = color.LightSlateGray ColorSlateGrey = color.SlateGray ) // ColorValues maps color constants to their RGB values. var ColorValues = color.ColorValues // Special colors. const ( // ColorReset is used to indicate that the color should use the // vanilla terminal colors. (Basically go back to the defaults.) // Deprecated: Use color.Reset. ColorReset = color.Reset // ColorNone indicates that we should not change the color from // whatever is already displayed. This can only be used in limited // circumstances. // Deprecated: Use color.None. ColorNone = color.None ) // ColorNames holds the written names of colors. Useful to present a list of // recognized named colors. Deprecated: Use color.Names. var ColorNames = color.Names // NewRGBColor returns a new color with the given red, green, and blue values. // Each value must be represented in the range 0-255. // Deprecated: Use color.NewRGBColor. func NewRGBColor(r, g, b int32) Color { return color.NewRGBColor(r, g, b) } // NewHexColor returns a color using the given 24-bit RGB value. // Deprecated: Use color.NewHexColor. func NewHexColor(v int32) Color { return color.NewHexColor(v) } // GetColor creates a Color from a color name (W3C name). A hex value may // be supplied as a string in the format "#ffffff". // Deprecated: Use color.GetColor. func GetColor(name string) Color { return color.GetColor(name) } // PaletteColor creates a color based on the palette index. // Deprecated: Use color.PaletteColor. func PaletteColor(index int) Color { return color.PaletteColor(index) } // FromImageColor converts an image/color.Color into Color. // Deprecated: Use color.FromImageColor. func FromImageColor(imageColor ic.Color) Color { return color.FromImageColor(imageColor) } // FindColor attempts to find a given color, or the best match possible for it, // from the palette given. This is an expensive operation, so results should // be cached by the caller. // Deprecated: Use color.Find. func FindColor(c Color, palette []Color) Color { return color.Find(c, palette) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/color/000077500000000000000000000000001520475227200213545ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/color/color.go000066400000000000000000001024541520475227200230270ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package color import ( "fmt" ic "image/color" "strconv" ) // Color represents a color. The low numeric values are the same as used // by ECMA-48, and beyond that XTerm. // // For Color names we use the W3C approved color names. // // Note that on various terminals colors may be approximated however, or // not supported at all. If no suitable representation for a color is known, // the library will simply not set any color, deferring to whatever default // attributes the terminal uses. type Color uint32 const ( // Default is used to leave the Color unchanged from whatever // system or terminal default may exist. It's also the zero value. Default Color = 0 // IsValid is used to indicate the color value is actually // valid (initialized). This is useful to permit the zero value // to be treated as the default. This should not be used // directly by applications. Use the IsValid method on Color instead. IsValid Color = 1 << 31 // IsRGB is used to indicate that the numeric value is not // a known color constant, but rather an RGB value. The lower // order 3 bytes are RGB. This should not be used directly, // instead use the Color.IsRGB method. IsRGB Color = 1 << 30 // IsSpecial is a flag used to indicate that the values have // special meaning, and live outside of the color space(s). // This should not be used directly by applications. IsSpecial Color = 1 << 29 ) // Note that the order of these options is important -- it follows the // definitions used by ECMA and XTerm. Hence any further named colors // must begin at a value not less than 256. const ( Black = XTerm0 Maroon = XTerm1 Green = XTerm2 Olive = XTerm3 Navy = XTerm4 Purple = XTerm5 Teal = XTerm6 Silver = XTerm7 Gray = XTerm8 Red = XTerm9 Lime = XTerm10 Yellow = XTerm11 Blue = XTerm12 Fuchsia = XTerm13 Aqua = XTerm14 White = XTerm15 ) const ( XTerm0 = IsValid + iota XTerm1 XTerm2 XTerm3 XTerm4 XTerm5 XTerm6 XTerm7 XTerm8 XTerm9 XTerm10 XTerm11 XTerm12 XTerm13 XTerm14 XTerm15 XTerm16 XTerm17 XTerm18 XTerm19 XTerm20 XTerm21 XTerm22 XTerm23 XTerm24 XTerm25 XTerm26 XTerm27 XTerm28 XTerm29 XTerm30 XTerm31 XTerm32 XTerm33 XTerm34 XTerm35 XTerm36 XTerm37 XTerm38 XTerm39 XTerm40 XTerm41 XTerm42 XTerm43 XTerm44 XTerm45 XTerm46 XTerm47 XTerm48 XTerm49 XTerm50 XTerm51 XTerm52 XTerm53 XTerm54 XTerm55 XTerm56 XTerm57 XTerm58 XTerm59 XTerm60 XTerm61 XTerm62 XTerm63 XTerm64 XTerm65 XTerm66 XTerm67 XTerm68 XTerm69 XTerm70 XTerm71 XTerm72 XTerm73 XTerm74 XTerm75 XTerm76 XTerm77 XTerm78 XTerm79 XTerm80 XTerm81 XTerm82 XTerm83 XTerm84 XTerm85 XTerm86 XTerm87 XTerm88 XTerm89 XTerm90 XTerm91 XTerm92 XTerm93 XTerm94 XTerm95 XTerm96 XTerm97 XTerm98 XTerm99 XTerm100 XTerm101 XTerm102 XTerm103 XTerm104 XTerm105 XTerm106 XTerm107 XTerm108 XTerm109 XTerm110 XTerm111 XTerm112 XTerm113 XTerm114 XTerm115 XTerm116 XTerm117 XTerm118 XTerm119 XTerm120 XTerm121 XTerm122 XTerm123 XTerm124 XTerm125 XTerm126 XTerm127 XTerm128 XTerm129 XTerm130 XTerm131 XTerm132 XTerm133 XTerm134 XTerm135 XTerm136 XTerm137 XTerm138 XTerm139 XTerm140 XTerm141 XTerm142 XTerm143 XTerm144 XTerm145 XTerm146 XTerm147 XTerm148 XTerm149 XTerm150 XTerm151 XTerm152 XTerm153 XTerm154 XTerm155 XTerm156 XTerm157 XTerm158 XTerm159 XTerm160 XTerm161 XTerm162 XTerm163 XTerm164 XTerm165 XTerm166 XTerm167 XTerm168 XTerm169 XTerm170 XTerm171 XTerm172 XTerm173 XTerm174 XTerm175 XTerm176 XTerm177 XTerm178 XTerm179 XTerm180 XTerm181 XTerm182 XTerm183 XTerm184 XTerm185 XTerm186 XTerm187 XTerm188 XTerm189 XTerm190 XTerm191 XTerm192 XTerm193 XTerm194 XTerm195 XTerm196 XTerm197 XTerm198 XTerm199 XTerm200 XTerm201 XTerm202 XTerm203 XTerm204 XTerm205 XTerm206 XTerm207 XTerm208 XTerm209 XTerm210 XTerm211 XTerm212 XTerm213 XTerm214 XTerm215 XTerm216 XTerm217 XTerm218 XTerm219 XTerm220 XTerm221 XTerm222 XTerm223 XTerm224 XTerm225 XTerm226 XTerm227 XTerm228 XTerm229 XTerm230 XTerm231 XTerm232 XTerm233 XTerm234 XTerm235 XTerm236 XTerm237 XTerm238 XTerm239 XTerm240 XTerm241 XTerm242 XTerm243 XTerm244 XTerm245 XTerm246 XTerm247 XTerm248 XTerm249 XTerm250 XTerm251 XTerm252 XTerm253 XTerm254 XTerm255 AliceBlue = IsRGB | IsValid | 0xF0F8FF AntiqueWhite = IsRGB | IsValid | 0xFAEBD7 AquaMarine = IsRGB | IsValid | 0x7FFFD4 Azure = IsRGB | IsValid | 0xF0FFFF Beige = IsRGB | IsValid | 0xF5F5DC Bisque = IsRGB | IsValid | 0xFFE4C4 BlanchedAlmond = IsRGB | IsValid | 0xFFEBCD BlueViolet = IsRGB | IsValid | 0x8A2BE2 Brown = IsRGB | IsValid | 0xA52A2A BurlyWood = IsRGB | IsValid | 0xDEB887 CadetBlue = IsRGB | IsValid | 0x5F9EA0 Chartreuse = IsRGB | IsValid | 0x7FFF00 Chocolate = IsRGB | IsValid | 0xD2691E Coral = IsRGB | IsValid | 0xFF7F50 CornflowerBlue = IsRGB | IsValid | 0x6495ED Cornsilk = IsRGB | IsValid | 0xFFF8DC Crimson = IsRGB | IsValid | 0xDC143C DarkBlue = IsRGB | IsValid | 0x00008B DarkCyan = IsRGB | IsValid | 0x008B8B DarkGoldenrod = IsRGB | IsValid | 0xB8860B DarkGray = IsRGB | IsValid | 0xA9A9A9 DarkGreen = IsRGB | IsValid | 0x006400 DarkKhaki = IsRGB | IsValid | 0xBDB76B DarkMagenta = IsRGB | IsValid | 0x8B008B DarkOliveGreen = IsRGB | IsValid | 0x556B2F DarkOrange = IsRGB | IsValid | 0xFF8C00 DarkOrchid = IsRGB | IsValid | 0x9932CC DarkRed = IsRGB | IsValid | 0x8B0000 DarkSalmon = IsRGB | IsValid | 0xE9967A DarkSeaGreen = IsRGB | IsValid | 0x8FBC8F DarkSlateBlue = IsRGB | IsValid | 0x483D8B DarkSlateGray = IsRGB | IsValid | 0x2F4F4F DarkTurquoise = IsRGB | IsValid | 0x00CED1 DarkViolet = IsRGB | IsValid | 0x9400D3 DeepPink = IsRGB | IsValid | 0xFF1493 DeepSkyBlue = IsRGB | IsValid | 0x00BFFF DimGray = IsRGB | IsValid | 0x696969 DodgerBlue = IsRGB | IsValid | 0x1E90FF FireBrick = IsRGB | IsValid | 0xB22222 FloralWhite = IsRGB | IsValid | 0xFFFAF0 ForestGreen = IsRGB | IsValid | 0x228B22 Gainsboro = IsRGB | IsValid | 0xDCDCDC GhostWhite = IsRGB | IsValid | 0xF8F8FF Gold = IsRGB | IsValid | 0xFFD700 Goldenrod = IsRGB | IsValid | 0xDAA520 GreenYellow = IsRGB | IsValid | 0xADFF2F Honeydew = IsRGB | IsValid | 0xF0FFF0 HotPink = IsRGB | IsValid | 0xFF69B4 IndianRed = IsRGB | IsValid | 0xCD5C5C Indigo = IsRGB | IsValid | 0x4B0082 Ivory = IsRGB | IsValid | 0xFFFFF0 Khaki = IsRGB | IsValid | 0xF0E68C Lavender = IsRGB | IsValid | 0xE6E6FA LavenderBlush = IsRGB | IsValid | 0xFFF0F5 LawnGreen = IsRGB | IsValid | 0x7CFC00 LemonChiffon = IsRGB | IsValid | 0xFFFACD LightBlue = IsRGB | IsValid | 0xADD8E6 LightCoral = IsRGB | IsValid | 0xF08080 LightCyan = IsRGB | IsValid | 0xE0FFFF LightGoldenrodYellow = IsRGB | IsValid | 0xFAFAD2 LightGray = IsRGB | IsValid | 0xD3D3D3 LightGreen = IsRGB | IsValid | 0x90EE90 LightPink = IsRGB | IsValid | 0xFFB6C1 LightSalmon = IsRGB | IsValid | 0xFFA07A LightSeaGreen = IsRGB | IsValid | 0x20B2AA LightSkyBlue = IsRGB | IsValid | 0x87CEFA LightSlateGray = IsRGB | IsValid | 0x778899 LightSteelBlue = IsRGB | IsValid | 0xB0C4DE LightYellow = IsRGB | IsValid | 0xFFFFE0 LimeGreen = IsRGB | IsValid | 0x32CD32 Linen = IsRGB | IsValid | 0xFAF0E6 MediumAquamarine = IsRGB | IsValid | 0x66CDAA MediumBlue = IsRGB | IsValid | 0x0000CD MediumOrchid = IsRGB | IsValid | 0xBA55D3 MediumPurple = IsRGB | IsValid | 0x9370DB MediumSeaGreen = IsRGB | IsValid | 0x3CB371 MediumSlateBlue = IsRGB | IsValid | 0x7B68EE MediumSpringGreen = IsRGB | IsValid | 0x00FA9A MediumTurquoise = IsRGB | IsValid | 0x48D1CC MediumVioletRed = IsRGB | IsValid | 0xC71585 MidnightBlue = IsRGB | IsValid | 0x191970 MintCream = IsRGB | IsValid | 0xF5FFFA MistyRose = IsRGB | IsValid | 0xFFE4E1 Moccasin = IsRGB | IsValid | 0xFFE4B5 NavajoWhite = IsRGB | IsValid | 0xFFDEAD OldLace = IsRGB | IsValid | 0xFDF5E6 OliveDrab = IsRGB | IsValid | 0x6B8E23 Orange = IsRGB | IsValid | 0xFFA500 OrangeRed = IsRGB | IsValid | 0xFF4500 Orchid = IsRGB | IsValid | 0xDA70D6 PaleGoldenrod = IsRGB | IsValid | 0xEEE8AA PaleGreen = IsRGB | IsValid | 0x98FB98 PaleTurquoise = IsRGB | IsValid | 0xAFEEEE PaleVioletRed = IsRGB | IsValid | 0xDB7093 PapayaWhip = IsRGB | IsValid | 0xFFEFD5 PeachPuff = IsRGB | IsValid | 0xFFDAB9 Peru = IsRGB | IsValid | 0xCD853F Pink = IsRGB | IsValid | 0xFFC0CB Plum = IsRGB | IsValid | 0xDDA0DD PowderBlue = IsRGB | IsValid | 0xB0E0E6 RebeccaPurple = IsRGB | IsValid | 0x663399 RosyBrown = IsRGB | IsValid | 0xBC8F8F RoyalBlue = IsRGB | IsValid | 0x4169E1 SaddleBrown = IsRGB | IsValid | 0x8B4513 Salmon = IsRGB | IsValid | 0xFA8072 SandyBrown = IsRGB | IsValid | 0xF4A460 SeaGreen = IsRGB | IsValid | 0x2E8B57 Seashell = IsRGB | IsValid | 0xFFF5EE Sienna = IsRGB | IsValid | 0xA0522D Skyblue = IsRGB | IsValid | 0x87CEEB SlateBlue = IsRGB | IsValid | 0x6A5ACD SlateGray = IsRGB | IsValid | 0x708090 Snow = IsRGB | IsValid | 0xFFFAFA SpringGreen = IsRGB | IsValid | 0x00FF7F SteelBlue = IsRGB | IsValid | 0x4682B4 Tan = IsRGB | IsValid | 0xD2B48C Thistle = IsRGB | IsValid | 0xD8BFD8 Tomato = IsRGB | IsValid | 0xFF6347 Turquoise = IsRGB | IsValid | 0x40E0D0 Violet = IsRGB | IsValid | 0xEE82EE Wheat = IsRGB | IsValid | 0xF5DEB3 WhiteSmoke = IsRGB | IsValid | 0xF5F5F5 YellowGreen = IsRGB | IsValid | 0x9ACD32 ) // These are aliases for the color gray, because some of us spell // it as grey. const ( Grey = Gray DimGrey = DimGray DarkGrey = DarkGray DarkSlateGrey = DarkSlateGray LightGrey = LightGray LightSlateGrey = LightSlateGray SlateGrey = SlateGray ) // ColorValues maps color constants to their RGB values. var ColorValues = map[Color]int32{ Black: 0x000000, Maroon: 0x800000, Green: 0x008000, Olive: 0x808000, Navy: 0x000080, Purple: 0x800080, Teal: 0x008080, Silver: 0xC0C0C0, Gray: 0x808080, Red: 0xFF0000, Lime: 0x00FF00, Yellow: 0xFFFF00, Blue: 0x0000FF, Fuchsia: 0xFF00FF, Aqua: 0x00FFFF, White: 0xFFFFFF, XTerm16: 0x000000, // black XTerm17: 0x00005F, XTerm18: 0x000087, XTerm19: 0x0000AF, XTerm20: 0x0000D7, XTerm21: 0x0000FF, // blue XTerm22: 0x005F00, XTerm23: 0x005F5F, XTerm24: 0x005F87, XTerm25: 0x005FAF, XTerm26: 0x005FD7, XTerm27: 0x005FFF, XTerm28: 0x008700, XTerm29: 0x00875F, XTerm30: 0x008787, XTerm31: 0x0087Af, XTerm32: 0x0087D7, XTerm33: 0x0087FF, XTerm34: 0x00AF00, XTerm35: 0x00AF5F, XTerm36: 0x00AF87, XTerm37: 0x00AFAF, XTerm38: 0x00AFD7, XTerm39: 0x00AFFF, XTerm40: 0x00D700, XTerm41: 0x00D75F, XTerm42: 0x00D787, XTerm43: 0x00D7AF, XTerm44: 0x00D7D7, XTerm45: 0x00D7FF, XTerm46: 0x00FF00, // lime XTerm47: 0x00FF5F, XTerm48: 0x00FF87, XTerm49: 0x00FFAF, XTerm50: 0x00FFd7, XTerm51: 0x00FFFF, // aqua XTerm52: 0x5F0000, XTerm53: 0x5F005F, XTerm54: 0x5F0087, XTerm55: 0x5F00AF, XTerm56: 0x5F00D7, XTerm57: 0x5F00FF, XTerm58: 0x5F5F00, XTerm59: 0x5F5F5F, XTerm60: 0x5F5F87, XTerm61: 0x5F5FAF, XTerm62: 0x5F5FD7, XTerm63: 0x5F5FFF, XTerm64: 0x5F8700, XTerm65: 0x5F875F, XTerm66: 0x5F8787, XTerm67: 0x5F87AF, XTerm68: 0x5F87D7, XTerm69: 0x5F87FF, XTerm70: 0x5FAF00, XTerm71: 0x5FAF5F, XTerm72: 0x5FAF87, XTerm73: 0x5FAFAF, XTerm74: 0x5FAFD7, XTerm75: 0x5FAFFF, XTerm76: 0x5FD700, XTerm77: 0x5FD75F, XTerm78: 0x5FD787, XTerm79: 0x5FD7AF, XTerm80: 0x5FD7D7, XTerm81: 0x5FD7FF, XTerm82: 0x5FFF00, XTerm83: 0x5FFF5F, XTerm84: 0x5FFF87, XTerm85: 0x5FFFAF, XTerm86: 0x5FFFD7, XTerm87: 0x5FFFFF, XTerm88: 0x870000, XTerm89: 0x87005F, XTerm90: 0x870087, XTerm91: 0x8700AF, XTerm92: 0x8700D7, XTerm93: 0x8700FF, XTerm94: 0x875F00, XTerm95: 0x875F5F, XTerm96: 0x875F87, XTerm97: 0x875FAF, XTerm98: 0x875FD7, XTerm99: 0x875FFF, XTerm100: 0x878700, XTerm101: 0x87875F, XTerm102: 0x878787, XTerm103: 0x8787AF, XTerm104: 0x8787D7, XTerm105: 0x8787FF, XTerm106: 0x87AF00, XTerm107: 0x87AF5F, XTerm108: 0x87AF87, XTerm109: 0x87AFAF, XTerm110: 0x87AFD7, XTerm111: 0x87AFFF, XTerm112: 0x87D700, XTerm113: 0x87D75F, XTerm114: 0x87D787, XTerm115: 0x87D7AF, XTerm116: 0x87D7D7, XTerm117: 0x87D7FF, XTerm118: 0x87FF00, XTerm119: 0x87FF5F, XTerm120: 0x87FF87, XTerm121: 0x87FFAF, XTerm122: 0x87FFD7, XTerm123: 0x87FFFF, XTerm124: 0xAF0000, XTerm125: 0xAF005F, XTerm126: 0xAF0087, XTerm127: 0xAF00AF, XTerm128: 0xAF00D7, XTerm129: 0xAF00FF, XTerm130: 0xAF5F00, XTerm131: 0xAF5F5F, XTerm132: 0xAF5F87, XTerm133: 0xAF5FAF, XTerm134: 0xAF5FD7, XTerm135: 0xAF5FFF, XTerm136: 0xAF8700, XTerm137: 0xAF875F, XTerm138: 0xAF8787, XTerm139: 0xAF87AF, XTerm140: 0xAF87D7, XTerm141: 0xAF87FF, XTerm142: 0xAFAF00, XTerm143: 0xAFAF5F, XTerm144: 0xAFAF87, XTerm145: 0xAFAFAF, XTerm146: 0xAFAFD7, XTerm147: 0xAFAFFF, XTerm148: 0xAFD700, XTerm149: 0xAFD75F, XTerm150: 0xAFD787, XTerm151: 0xAFD7AF, XTerm152: 0xAFD7D7, XTerm153: 0xAFD7FF, XTerm154: 0xAFFF00, XTerm155: 0xAFFF5F, XTerm156: 0xAFFF87, XTerm157: 0xAFFFAF, XTerm158: 0xAFFFD7, XTerm159: 0xAFFFFF, XTerm160: 0xD70000, XTerm161: 0xD7005F, XTerm162: 0xD70087, XTerm163: 0xD700AF, XTerm164: 0xD700D7, XTerm165: 0xD700FF, XTerm166: 0xD75F00, XTerm167: 0xD75F5F, XTerm168: 0xD75F87, XTerm169: 0xD75FAF, XTerm170: 0xD75FD7, XTerm171: 0xD75FFF, XTerm172: 0xD78700, XTerm173: 0xD7875F, XTerm174: 0xD78787, XTerm175: 0xD787AF, XTerm176: 0xD787D7, XTerm177: 0xD787FF, XTerm178: 0xD7AF00, XTerm179: 0xD7AF5F, XTerm180: 0xD7AF87, XTerm181: 0xD7AFAF, XTerm182: 0xD7AFD7, XTerm183: 0xD7AFFF, XTerm184: 0xD7D700, XTerm185: 0xD7D75F, XTerm186: 0xD7D787, XTerm187: 0xD7D7AF, XTerm188: 0xD7D7D7, XTerm189: 0xD7D7FF, XTerm190: 0xD7FF00, XTerm191: 0xD7FF5F, XTerm192: 0xD7FF87, XTerm193: 0xD7FFAF, XTerm194: 0xD7FFD7, XTerm195: 0xD7FFFF, XTerm196: 0xFF0000, // red XTerm197: 0xFF005F, XTerm198: 0xFF0087, XTerm199: 0xFF00AF, XTerm200: 0xFF00D7, XTerm201: 0xFF00FF, // fuchsia XTerm202: 0xFF5F00, XTerm203: 0xFF5F5F, XTerm204: 0xFF5F87, XTerm205: 0xFF5FAF, XTerm206: 0xFF5FD7, XTerm207: 0xFF5FFF, XTerm208: 0xFF8700, XTerm209: 0xFF875F, XTerm210: 0xFF8787, XTerm211: 0xFF87AF, XTerm212: 0xFF87D7, XTerm213: 0xFF87FF, XTerm214: 0xFFAF00, XTerm215: 0xFFAF5F, XTerm216: 0xFFAF87, XTerm217: 0xFFAFAF, XTerm218: 0xFFAFD7, XTerm219: 0xFFAFFF, XTerm220: 0xFFD700, XTerm221: 0xFFD75F, XTerm222: 0xFFD787, XTerm223: 0xFFD7AF, XTerm224: 0xFFD7D7, XTerm225: 0xFFD7FF, XTerm226: 0xFFFF00, // yellow XTerm227: 0xFFFF5F, XTerm228: 0xFFFF87, XTerm229: 0xFFFFAF, XTerm230: 0xFFFFD7, XTerm231: 0xFFFFFF, // white XTerm232: 0x080808, XTerm233: 0x121212, XTerm234: 0x1C1C1C, XTerm235: 0x262626, XTerm236: 0x303030, XTerm237: 0x3A3A3A, XTerm238: 0x444444, XTerm239: 0x4E4E4E, XTerm240: 0x585858, XTerm241: 0x626262, XTerm242: 0x6C6C6C, XTerm243: 0x767676, XTerm244: 0x808080, // grey XTerm245: 0x8A8A8A, XTerm246: 0x949494, XTerm247: 0x9E9E9E, XTerm248: 0xA8A8A8, XTerm249: 0xB2B2B2, XTerm250: 0xBCBCBC, XTerm251: 0xC6C6C6, XTerm252: 0xD0D0D0, XTerm253: 0xDADADA, XTerm254: 0xE4E4E4, XTerm255: 0xEEEEEE, AliceBlue: 0xF0F8FF, AntiqueWhite: 0xFAEBD7, AquaMarine: 0x7FFFD4, Azure: 0xF0FFFF, Beige: 0xF5F5DC, Bisque: 0xFFE4C4, BlanchedAlmond: 0xFFEBCD, BlueViolet: 0x8A2BE2, Brown: 0xA52A2A, BurlyWood: 0xDEB887, CadetBlue: 0x5F9EA0, Chartreuse: 0x7FFF00, Chocolate: 0xD2691E, Coral: 0xFF7F50, CornflowerBlue: 0x6495ED, Cornsilk: 0xFFF8DC, Crimson: 0xDC143C, DarkBlue: 0x00008B, DarkCyan: 0x008B8B, DarkGoldenrod: 0xB8860B, DarkGray: 0xA9A9A9, DarkGreen: 0x006400, DarkKhaki: 0xBDB76B, DarkMagenta: 0x8B008B, DarkOliveGreen: 0x556B2F, DarkOrange: 0xFF8C00, DarkOrchid: 0x9932CC, DarkRed: 0x8B0000, DarkSalmon: 0xE9967A, DarkSeaGreen: 0x8FBC8F, DarkSlateBlue: 0x483D8B, DarkSlateGray: 0x2F4F4F, DarkTurquoise: 0x00CED1, DarkViolet: 0x9400D3, DeepPink: 0xFF1493, DeepSkyBlue: 0x00BFFF, DimGray: 0x696969, DodgerBlue: 0x1E90FF, FireBrick: 0xB22222, FloralWhite: 0xFFFAF0, ForestGreen: 0x228B22, Gainsboro: 0xDCDCDC, GhostWhite: 0xF8F8FF, Gold: 0xFFD700, Goldenrod: 0xDAA520, GreenYellow: 0xADFF2F, Honeydew: 0xF0FFF0, HotPink: 0xFF69B4, IndianRed: 0xCD5C5C, Indigo: 0x4B0082, Ivory: 0xFFFFF0, Khaki: 0xF0E68C, Lavender: 0xE6E6FA, LavenderBlush: 0xFFF0F5, LawnGreen: 0x7CFC00, LemonChiffon: 0xFFFACD, LightBlue: 0xADD8E6, LightCoral: 0xF08080, LightCyan: 0xE0FFFF, LightGoldenrodYellow: 0xFAFAD2, LightGray: 0xD3D3D3, LightGreen: 0x90EE90, LightPink: 0xFFB6C1, LightSalmon: 0xFFA07A, LightSeaGreen: 0x20B2AA, LightSkyBlue: 0x87CEFA, LightSlateGray: 0x778899, LightSteelBlue: 0xB0C4DE, LightYellow: 0xFFFFE0, LimeGreen: 0x32CD32, Linen: 0xFAF0E6, MediumAquamarine: 0x66CDAA, MediumBlue: 0x0000CD, MediumOrchid: 0xBA55D3, MediumPurple: 0x9370DB, MediumSeaGreen: 0x3CB371, MediumSlateBlue: 0x7B68EE, MediumSpringGreen: 0x00FA9A, MediumTurquoise: 0x48D1CC, MediumVioletRed: 0xC71585, MidnightBlue: 0x191970, MintCream: 0xF5FFFA, MistyRose: 0xFFE4E1, Moccasin: 0xFFE4B5, NavajoWhite: 0xFFDEAD, OldLace: 0xFDF5E6, OliveDrab: 0x6B8E23, Orange: 0xFFA500, OrangeRed: 0xFF4500, Orchid: 0xDA70D6, PaleGoldenrod: 0xEEE8AA, PaleGreen: 0x98FB98, PaleTurquoise: 0xAFEEEE, PaleVioletRed: 0xDB7093, PapayaWhip: 0xFFEFD5, PeachPuff: 0xFFDAB9, Peru: 0xCD853F, Pink: 0xFFC0CB, Plum: 0xDDA0DD, PowderBlue: 0xB0E0E6, RebeccaPurple: 0x663399, RosyBrown: 0xBC8F8F, RoyalBlue: 0x4169E1, SaddleBrown: 0x8B4513, Salmon: 0xFA8072, SandyBrown: 0xF4A460, SeaGreen: 0x2E8B57, Seashell: 0xFFF5EE, Sienna: 0xA0522D, Skyblue: 0x87CEEB, SlateBlue: 0x6A5ACD, SlateGray: 0x708090, Snow: 0xFFFAFA, SpringGreen: 0x00FF7F, SteelBlue: 0x4682B4, Tan: 0xD2B48C, Thistle: 0xD8BFD8, Tomato: 0xFF6347, Turquoise: 0x40E0D0, Violet: 0xEE82EE, Wheat: 0xF5DEB3, WhiteSmoke: 0xF5F5F5, YellowGreen: 0x9ACD32, } // Special colors. const ( // Reset is used to indicate that the color should use the // vanilla terminal colors. (Basically go back to the defaults.) Reset = IsSpecial | iota // None indicates that we should not change the color from // whatever is already displayed. This can only be used in limited // circumstances. None ) // Names holds the written names of colors. Useful to present a list of // recognized named colors. var Names = map[string]Color{ "black": Black, "maroon": Maroon, "green": Green, "olive": Olive, "navy": Navy, "purple": Purple, "teal": Teal, "silver": Silver, "gray": Gray, "red": Red, "lime": Lime, "yellow": Yellow, "blue": Blue, "fuchsia": Fuchsia, "aqua": Aqua, "white": White, "aliceblue": AliceBlue, "antiquewhite": AntiqueWhite, "aquamarine": AquaMarine, "azure": Azure, "beige": Beige, "bisque": Bisque, "blanchedalmond": BlanchedAlmond, "blueviolet": BlueViolet, "brown": Brown, "burlywood": BurlyWood, "cadetblue": CadetBlue, "chartreuse": Chartreuse, "chocolate": Chocolate, "coral": Coral, "cornflowerblue": CornflowerBlue, "cornsilk": Cornsilk, "crimson": Crimson, "darkblue": DarkBlue, "darkcyan": DarkCyan, "darkgoldenrod": DarkGoldenrod, "darkgray": DarkGray, "darkgreen": DarkGreen, "darkkhaki": DarkKhaki, "darkmagenta": DarkMagenta, "darkolivegreen": DarkOliveGreen, "darkorange": DarkOrange, "darkorchid": DarkOrchid, "darkred": DarkRed, "darksalmon": DarkSalmon, "darkseagreen": DarkSeaGreen, "darkslateblue": DarkSlateBlue, "darkslategray": DarkSlateGray, "darkturquoise": DarkTurquoise, "darkviolet": DarkViolet, "deeppink": DeepPink, "deepskyblue": DeepSkyBlue, "dimgray": DimGray, "dodgerblue": DodgerBlue, "firebrick": FireBrick, "floralwhite": FloralWhite, "forestgreen": ForestGreen, "gainsboro": Gainsboro, "ghostwhite": GhostWhite, "gold": Gold, "goldenrod": Goldenrod, "greenyellow": GreenYellow, "honeydew": Honeydew, "hotpink": HotPink, "indianred": IndianRed, "indigo": Indigo, "ivory": Ivory, "khaki": Khaki, "lavender": Lavender, "lavenderblush": LavenderBlush, "lawngreen": LawnGreen, "lemonchiffon": LemonChiffon, "lightblue": LightBlue, "lightcoral": LightCoral, "lightcyan": LightCyan, "lightgoldenrodyellow": LightGoldenrodYellow, "lightgray": LightGray, "lightgreen": LightGreen, "lightpink": LightPink, "lightsalmon": LightSalmon, "lightseagreen": LightSeaGreen, "lightskyblue": LightSkyBlue, "lightslategray": LightSlateGray, "lightsteelblue": LightSteelBlue, "lightyellow": LightYellow, "limegreen": LimeGreen, "linen": Linen, "mediumaquamarine": MediumAquamarine, "mediumblue": MediumBlue, "mediumorchid": MediumOrchid, "mediumpurple": MediumPurple, "mediumseagreen": MediumSeaGreen, "mediumslateblue": MediumSlateBlue, "mediumspringgreen": MediumSpringGreen, "mediumturquoise": MediumTurquoise, "mediumvioletred": MediumVioletRed, "midnightblue": MidnightBlue, "mintcream": MintCream, "mistyrose": MistyRose, "moccasin": Moccasin, "navajowhite": NavajoWhite, "oldlace": OldLace, "olivedrab": OliveDrab, "orange": Orange, "orangered": OrangeRed, "orchid": Orchid, "palegoldenrod": PaleGoldenrod, "palegreen": PaleGreen, "paleturquoise": PaleTurquoise, "palevioletred": PaleVioletRed, "papayawhip": PapayaWhip, "peachpuff": PeachPuff, "peru": Peru, "pink": Pink, "plum": Plum, "powderblue": PowderBlue, "rebeccapurple": RebeccaPurple, "rosybrown": RosyBrown, "royalblue": RoyalBlue, "saddlebrown": SaddleBrown, "salmon": Salmon, "sandybrown": SandyBrown, "seagreen": SeaGreen, "seashell": Seashell, "sienna": Sienna, "skyblue": Skyblue, "slateblue": SlateBlue, "slategray": SlateGray, "snow": Snow, "springgreen": SpringGreen, "steelblue": SteelBlue, "tan": Tan, "thistle": Thistle, "tomato": Tomato, "turquoise": Turquoise, "violet": Violet, "wheat": Wheat, "whitesmoke": WhiteSmoke, "yellowgreen": YellowGreen, "grey": Gray, "dimgrey": DimGray, "darkgrey": DarkGray, "darkslategrey": DarkSlateGray, "lightgrey": LightGray, "lightslategrey": LightSlateGray, "slategrey": SlateGray, } // Valid indicates the color is a valid value (has been set). func (c Color) Valid() bool { return c&IsValid != 0 } // IsRGB is true if the color is an RGB specific value. func (c Color) IsRGB() bool { return c&(IsValid|IsRGB) == (IsValid | IsRGB) } // CSS returns the CSS hex string ( #ABCDEF ) if valid // if not a valid color returns empty string func (c Color) CSS() string { if !c.Valid() { return "" } return fmt.Sprintf("#%06X", c.Hex()) } // String implements fmt.Stringer to return either the // W3C name if it has one or the CSS hex string '#ABCDEF' func (c Color) String() string { if !c.Valid() { switch c { case None: return "none" case Default: return "default" case Reset: return "reset" } return "" } return c.Name(true) } // Name returns W3C name or an empty string if no arguments // if passed true as an argument it will falls back to // the CSS hex string if no W3C name found '#ABCDEF' func (c Color) Name(css ...bool) string { for name, hex := range Names { if c == hex { return name } } if len(css) > 0 && css[0] { return c.CSS() } return "" } // Hex returns the color's hexadecimal RGB 24-bit value with each component // consisting of a single byte, R << 16 | G << 8 | B. If the color // is unknown or unset, -1 is returned. func (c Color) Hex() int32 { if !c.Valid() { return -1 } if c&IsRGB != 0 { return int32(c & 0xffffff) } if v, ok := ColorValues[c]; ok { return v } return -1 } // RGB returns the red, green, and blue components of the color, with // each component represented as a value 0-255. In the event that the // color cannot be broken up (not set usually), -1 is returned for each value. func (c Color) RGB() (int32, int32, int32) { v := c.Hex() if v < 0 { return -1, -1, -1 } return (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff } // TrueColor returns the true color (RGB) version of the provided color. // This is useful for ensuring color accuracy when using named colors. // This will override terminal theme colors. func (c Color) TrueColor() Color { if !c.Valid() { return Default } if c&IsRGB != 0 { return c | IsValid } if hex := c.Hex(); hex < 0 { return Default } else { return Color(hex) | IsRGB | IsValid } } // RGBA makes these colors directly usable as imageColor colors. // The values are scaled only to 16 bits. Invalid colors are returned // with all values being zero (notably the alpha is zero, so fully transparent), // otherwise the alpha channel is set to 0xffff (fully opaque). func (c Color) RGBA() (r, g, b, a uint32) { if !c.Valid() { return 0, 0, 0, 0 } r1, g1, b1 := c.RGB() r = uint32(r1) g = uint32(g1) b = uint32(b1) r = r | r<<8 g = g | g<<8 b = b | b<<8 a = 0xffff return r, g, b, a } // NewRGBColor returns a new color with the given red, green, and blue values. // Each value must be represented in the range 0-255. func NewRGBColor(r, g, b int32) Color { return NewHexColor(((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff)) } // NewHexColor returns a color using the given 24-bit RGB value. func NewHexColor(v int32) Color { return IsRGB | Color(v) | IsValid } // GetColor creates a Color from a color name (W3C name). A hex value may // be supplied as a string in the format "#ffffff". func GetColor(name string) Color { if c, ok := Names[name]; ok { return c } if len(name) == 7 && name[0] == '#' { if v, e := strconv.ParseInt(name[1:], 16, 32); e == nil { return NewHexColor(int32(v)) } } return Default } // PaletteColor creates a color based on the palette index. func PaletteColor(index int) Color { return Color(index) | IsValid } // FromImageColor converts an image/color.Color into Color. // The alpha value is limited to just zero and non-zero, so it should // be tracked separately if full detail is needed. (A zero alpha // becomes the default color, which means no color change at all.) func FromImageColor(imageColor ic.Color) Color { r, g, b, a := imageColor.RGBA() if a == 0 { return Default } // NOTE image/color.Color RGB values range is [0, 0xFFFF] as uint32 return NewRGBColor(int32(r>>8), int32(g>>8), int32(b>>8)) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/color/color_test.go000066400000000000000000000137271520475227200240720ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package color import ( "fmt" ic "image/color" "testing" ) // TestColorValues test variety of color edge cases. func TestColorValues(t *testing.T) { var values = []struct { color Color hex int32 }{ {Red, 0xFF0000}, {Green, 0x008000}, {Lime, 0x00FF00}, {Blue, 0x0000FF}, {Black, 0x000000}, {White, 0xFFFFFF}, {Silver, 0xC0C0C0}, {Navy, 0x000080}, {None, -1}, {Reset, -1}, {Color(300) | IsValid, -1}, // beyond the palette, marked valid } for _, tc := range values { if tc.color.Hex() != tc.hex { t.Errorf("Color: %x != %x", tc.color.Hex(), tc.hex) } if tc.color.TrueColor().Hex() != tc.hex { t.Errorf("TrueColor %x != %x", tc.color.TrueColor().Hex(), tc.hex) } } } // TestColorFitting tests color matching. func TestColorFitting(t *testing.T) { var pal []Color for i := range 255 { pal = append(pal, PaletteColor(i)) } // Exact color fitting on ANSI colors for i := range 7 { if Find(PaletteColor(i), pal[:8]) != PaletteColor(i) { t.Errorf("Color ANSI fit fail at %d", i) } } // Grey is closest to Silver if Find(PaletteColor(8), pal[:8]) != PaletteColor(7) { t.Errorf("Grey does not fit to silver") } // Color fitting of upper 8 colors. for i := 9; i < 16; i++ { if Find(PaletteColor(i), pal[:8]) != PaletteColor(i%8) { t.Errorf("Color fit fail at %d", i) } } // Imperfect fit if Find(OrangeRed, pal[:16]) != Red || Find(AliceBlue, pal[:16]) != White || Find(Pink, pal) != XTerm217 || Find(Sienna, pal) != XTerm173 || Find(GetColor("#00FD00"), pal) != Lime { t.Errorf("Imperfect color fit") } } // TestColorNameLookup tests looking up colors by a string name. func TestColorNameLookup(t *testing.T) { var values = []struct { name string color Color rgb bool }{ {"#FF0000", Red, true}, {"black", Black, false}, {"orange", Orange, true}, {"door", Default, false}, } for _, v := range values { c := GetColor(v.name) if c.Hex() != v.color.Hex() { t.Errorf("Wrong color for %v: %v", v.name, c.Hex()) } if v.rgb { if !c.IsRGB() { t.Errorf("Color should have RGB: %v", v.name) } } else { if c.IsRGB() { t.Errorf("Named color should not be RGB: %v", v.name) } } if c.TrueColor().Hex() != v.color.Hex() { t.Errorf("TrueColor did not match") } } // these colors only have strings (for debugging), you cannot use them to create a color if None.String() != "none" { t.Errorf("ColorNone did not match") } if Reset.String() != "reset" { t.Errorf("Reset did not match") } if Default.String() != "default" { t.Errorf("Default did not match") } } // TestColorRGB tests the Color.RGB API. func TestColorRGB(t *testing.T) { r, g, b := GetColor("#112233").RGB() if r != 0x11 || g != 0x22 || b != 0x33 { t.Errorf("RGB wrong (%x, %x, %x)", r, g, b) } r, g, b = None.RGB() if r != -1 || g != -1 || b != -1 { t.Errorf("None should not give valid RGB") } } // TestFromImageColor tests converting from image.Color to Color. func TestFromImageColor(t *testing.T) { red := ic.RGBA{0xFF, 0x00, 0x00, 0x01} white := ic.Gray{0xFF} cyan := ic.CMYK{0xFF, 0x00, 0x00, 0x00} transparent := ic.RGBA{0x01, 0x02, 0x03, 0x00} if hex := FromImageColor(red).Hex(); hex != 0xFF0000 { t.Errorf("%v is not 0xFF0000", hex) } if hex := FromImageColor(white).Hex(); hex != 0xFFFFFF { t.Errorf("%v is not 0xFFFFFF", hex) } if hex := FromImageColor(cyan).Hex(); hex != 0x00FFFF { t.Errorf("%v is not 0x00FFFF", hex) } if c := FromImageColor(transparent); c != Default { t.Errorf("transparent should be default") } } // TestColorRGBA tests the Color.RGBA API. func TestColorRGBA(t *testing.T) { r, g, b, a := Red.RGBA() if r != 0xffff || g != 0 || b != 0 || a != 0xffff { t.Errorf("Wrong RGBA for red: %x %x %x %x", r, g, b, a) } r, g, b, a = Red.TrueColor().RGBA() if r != 0xffff || g != 0 || b != 0 || a != 0xffff { t.Errorf("Wrong RGBA for red.TrueColor: %x %x %x %x", r, g, b, a) } r, g, b, a = Default.RGBA() if r != 0 || g != 0 || b != 0 || a != 0 { t.Errorf("Non-zero RGBA for default") } r, g, b, a = None.RGBA() if r != 0 || g != 0 || b != 0 || a != 0 { t.Errorf("Non-zero RGBA for none") } r, g, b, a = Reset.RGBA() if r != 0 || g != 0 || b != 0 || a != 0 { t.Errorf("Non-zero RGBA for reset") } } // TestColorNames tests the color.Name() API. func TestColorNames(t *testing.T) { cases := []struct { c Color name string css string }{ {Red, "red", "#FF0000"}, {Pink, "pink", "#FFC0CB"}, {Black, "black", "#000000"}, {Black.TrueColor(), "", "#000000"}, {XTerm100, "", "#878700"}, {Color(1), "", ""}, // invalid color } for i, cs := range cases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { if cs.c.CSS() != cs.css { t.Errorf("case %d: color css %q != %q", i, cs.c.CSS(), cs.css) } if cs.c.Name() != cs.name { t.Errorf("case %d: color name %q != %q", i, cs.c.Name(), cs.name) } exp := cs.c.Name() if exp == "" { exp = cs.c.CSS() } if cs.c.Name(true) != exp { // test css t.Errorf("case %d: color name(true) %q != %q", i, cs.c.Name(true), exp) } }) } } // TestColorString tests the color.String() API. func TestColorString(t *testing.T) { if s := Color(0).String(); s != "default" { t.Errorf("zero color not default: %q", s) } if s := Color(10).String(); s != "" { t.Errorf("invalid non-zero color did not yield empty string: %q", s) } if s := Red.String(); s != "red" { t.Errorf("wrong string for red: %q", s) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/color/fit.go000066400000000000000000000027101520475227200224650ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package color import ( "github.com/lucasb-eyer/go-colorful" ) // Find attempts to find a given color, or the best match possible for it, // from the palette given. This is an expensive operation, so results should // be cached by the caller. func Find(c Color, palette []Color) Color { match := Default dist := float64(0) r, g, b := c.RGB() c1 := colorful.Color{ R: float64(r) / 255.0, G: float64(g) / 255.0, B: float64(b) / 255.0, } for _, d := range palette { r, g, b = d.RGB() c2 := colorful.Color{ R: float64(r) / 255.0, G: float64(g) / 255.0, B: float64(b) / 255.0, } // CIE94 is more accurate, but really really expensive. nd := c1.DistanceCIE76(c2) // NB: nd < dist is false if is NaN. // We have never seen a case where the CIE76 algorithm returns NaN. if match == Default || nd < dist { match = d dist = nd } } return match } golang-github-gdamore-tcell.v3-3.4.0+dfsg/color_test.go000066400000000000000000000054511520475227200227470ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( ic "image/color" "testing" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/vt" ) // TestColorWrappers just tests the legacy API wrappers. // The meaty tests are in the color package. func TestColorWrappers(t *testing.T) { p := []color.Color{ color.XTerm0, color.XTerm1, color.XTerm2, color.XTerm3, color.XTerm4, color.XTerm5, color.XTerm6, color.XTerm7, } for c := ColorBlack; c < Color255; c++ { if FindColor(c, p) != color.Find(c, p) { t.Errorf("API mismatch for color %s", c.String()) } } if GetColor("#112233") != color.GetColor("#112233") { t.Errorf("Wrong colors %s != %s", GetColor("#112233").String(), color.GetColor("#112233")) } red := ic.RGBA{0xFF, 0x00, 0x00, 0x01} if FromImageColor(red) != color.FromImageColor(red) { t.Errorf("wrong colors %d %d", FromImageColor(red), color.FromImageColor(red)) } if NewHexColor(0x1234) != color.NewHexColor(0x1234) { t.Errorf("hex colors don't match: %d %d", NewHexColor(0x1234), color.NewHexColor(0x1234)) } if NewRGBColor(11, 22, 33) != color.NewRGBColor(11, 22, 33) { t.Errorf("rgb colors don't match: %d %d", NewRGBColor(11, 22, 33), color.NewRGBColor(11, 22, 33)) } } func TestColorNone(t *testing.T) { _, s := NewMockScreen(t, vt.MockOptSize{X: 80, Y: 24}) defer s.Fini() st := StyleDefault.Foreground(ColorBlack).Background(ColorWhite) s.Fill(' ', st) if _, s1, _ := s.Get(0, 0); s1 != st { t.Errorf("Wrong style! fg %s bg %s", s1.fg.String(), s1.bg.String()) } st2 := st.Foreground(ColorNone).Background(ColorNone) s.Fill('X', st2) if _, s1, _ := s.Get(0, 0); s1 != st { t.Errorf("Wrong style! fg %s bg %s", s1.fg.String(), s1.bg.String()) } red := st.Foreground(ColorRed).Background(ColorNone) s.Put(1, 0, " ", red) if _, s1, _ := s.Get(1, 0); s1 != red.Background(st.bg) { t.Errorf("Wrong style! fg %s bg %s", s1.fg.String(), s1.bg.String()) } if _, s1, _ := s.Get(0, 0); s1 != st { t.Errorf("Wrong style! fg %s bg %s", s1.fg.String(), s1.bg.String()) } pink := st.Background(ColorPink).Foreground(ColorNone) s.Put(1, 0, " ", pink) combined := pink.Foreground(ColorRed) if _, s1, _ := s.Get(1, 0); s1 != combined { t.Errorf("Wrong style! fg %s bg %s", s1.fg.String(), s1.bg.String()) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/000077500000000000000000000000001520475227200213455ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/beep/000077500000000000000000000000001520475227200222605ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/beep/beep.go000066400000000000000000000035421520475227200235260ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // beep makes a beep for every 3 seconds, or when B is pressed, until you press ESC or CTRL-Q package main import ( "fmt" "os" "time" "github.com/gdamore/tcell/v3" ) func draw(s tcell.Screen, remain int) { style := tcell.StyleDefault s.Clear() s.PutStrStyled(1, 1, fmt.Sprintf("Beep will occur in %d seconds...", remain), style) s.PutStrStyled(1, 3, "Press ESC or CTRL-Q to quit, B to beep now.", style.Italic(true)) s.Show() } func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defer s.Fini() s.SetStyle(tcell.StyleDefault) remain := 3 draw(s, remain) for { select { case ev := <-s.EventQ(): switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape, tcell.KeyCtrlQ: return case tcell.KeyCtrlL: draw(s, remain) s.Sync() case tcell.KeyRune: if ev.Str() == "b" || ev.Str() == "B" { remain = 3 s.Beep() draw(s, remain) } } case *tcell.EventResize: s.Sync() } case <-time.After(time.Second): // imprecise, but good enough for demo remain-- if remain == 0 { remain = 3 s.Beep() } draw(s, remain) } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/beep/beep_test.go000066400000000000000000000030001520475227200245520ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) func TestBeep(t *testing.T) { mt := vt.NewMockTerm() scr, err := tcell.NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() // give enough time for the screen to draw // this is needed because main is running asynchronously. time.Sleep(time.Millisecond * 50) mt.Drain() // simulate two presses of B mt.KeyTap(vt.KeyB) time.Sleep(time.Millisecond * 100) mt.KeyTap(vt.KeyB) // control L (forces sync) mt.KeyTap(vt.KeyRCtrl, vt.KeyL) // sleep at least 3 seconds to get the time driven beep time.Sleep(time.Millisecond * 3500) mt.KeyTap(vt.KeyLCtrl, vt.KeyQ) wg.Wait() if cnt := mt.Bells(); cnt != 3 { t.Errorf("incorrect bell count %d != 3", cnt) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/boxes/000077500000000000000000000000001520475227200224655ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/boxes/boxes.go000066400000000000000000000047641520475227200241470ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // boxes just displays random colored boxes on your terminal screen. // Press escape or control-q to exit the program. package main import ( "fmt" "math/rand" "os" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) func makeBox(s tcell.Screen) { w, h := s.Size() if w == 0 || h == 0 { return } glyphs := []string{"@", "#", "&", "*", "=", "%", "Z", "A"} lx := rand.Int() % w ly := rand.Int() % h lw := rand.Int() % (w - lx) lh := rand.Int() % (h - ly) st := tcell.StyleDefault gl := " " if s.Colors() > 256 { rgb := tcell.NewHexColor(int32(rand.Int() & 0xffffff)) st = st.Background(rgb) } else if s.Colors() > 1 { st = st.Background(color.Color(rand.Int()%s.Colors()) | color.IsValid) } else { st = st.Reverse(rand.Int()%2 == 0) gl = glyphs[rand.Int()%len(glyphs)] } for row := range lh { for col := range lw { s.PutStrStyled(lx+col, ly+row, gl, st) } } s.Show() } var ( count = 0 interval = time.Millisecond * 5 drawTime time.Duration ) func main() { tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } s.SetStyle(tcell.StyleDefault. Foreground(color.Black). Background(color.White)) s.Clear() loop: for { select { case ev := <-s.EventQ(): switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape, tcell.KeyCtrlQ: break loop case tcell.KeyCtrlL: s.Sync() } case *tcell.EventResize: s.Sync() } case <-time.After(interval): } start := time.Now() makeBox(s) count++ drawTime += time.Since(start) } s.Fini() fmt.Printf("Finished %d boxes in %s (drawing time only)\n", count, drawTime) if count > 0 { fmt.Printf("Average is %0.3f ms / box\n", (float64(drawTime)/float64(count))/1000000.0) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/boxes/boxes_test.go000066400000000000000000000037511520475227200252010ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) func TestBoxes(t *testing.T) { for _, colors := range []int{0, 8, 16, 88, 256, 1 << 24} { t.Run(fmt.Sprintf("%d_colors)", colors), func(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptColors(colors)) scr, err := tcell.NewTerminfoScreenFromTty(mt, tcell.OptColors(colors)) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) count = 0 drawTime = 0 interval = time.Microsecond * 10 go func() { defer wg.Done() main() }() time.Sleep(time.Millisecond * 25) mt.KeyTap(vt.KeyRCtrl, vt.KeyL) mt.SetSize(vt.Coord{X: 10, Y: 10}) mt.Drain() time.Sleep(time.Millisecond * 25) mt.KeyTap(vt.KeyLCtrl, vt.KeyQ) mt.Drain() wg.Wait() if count < 10 { // CI runs *slow* t.Errorf("Too few boxes: %d", count) } if drawTime < time.Microsecond { // on windows this can happen because our tick counter is too coarse, // and our mock screen is basically limited only by CPU. t.Logf("Draw time very short: %s", drawTime) } // It should not take 10 milliseconds to draw a box, // as we generally see values sub millisecond here. if drawTime > 10*time.Millisecond*time.Duration(count) { t.Errorf("Draw time too long: %s", drawTime) } }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/clipboard/000077500000000000000000000000001520475227200233045ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/clipboard/clipboard.go000066400000000000000000000054141520475227200255760ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "unicode/utf8" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) var clipboard []byte func displayHelloWorld(s tcell.Screen) { w, h := s.Size() s.Clear() style := tcell.StyleDefault.Foreground(color.CadetBlue.TrueColor()).Background(color.White) has := "does NOT claim" if s.HasClipboard() { has = "claims to" } term, version := s.Terminal() termId := "(unidentified)" if term != "" { termId = fmt.Sprintf("(%s %s)", term, version) } line := fmt.Sprintf("Your terminal %s %s to allow clipboard access.", termId, has) s.PutStrStyled(w/2-14, h/2, "Press 1 to set clipboard", style) s.PutStrStyled(w/2-14, h/2+1, "Press 2 to get clipboard", style) s.PutStr((w-len(line))/2, h/2+9, line) msg := "" if utf8.Valid(clipboard) { cp := string(clipboard) if len(cp) >= w-25 { cp = cp[:21] + " ..." } msg = fmt.Sprintf("Clipboard (%d bytes): %s", len(clipboard), cp) } else if clipboard != nil { msg = fmt.Sprintf("Clipboard (%d bytes) Not Valid UTF-8", len(clipboard)) } else { msg = "No clipboard data" } s.PutStr((w-len(msg))/2, h/2+3, msg) line = "Press Control-Q to exit." s.PutStr((w-len(line))/2, h/2+5, line) s.Show() } // This program demonstrates access to the clipboard. // Not all terminals support, and many only support write access. // (Read access to the clipboard is often restricted for security reasons.) func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e := s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defer s.Fini() defStyle := tcell.StyleDefault. Background(color.Black). Foreground(color.White) s.SetStyle(defStyle) displayHelloWorld(s) for { ev1 := <-s.EventQ() switch ev := ev1.(type) { case *tcell.EventResize: s.Sync() displayHelloWorld(s) case *tcell.EventKey: switch ev.Key() { case tcell.KeyRune: switch ev.Str() { case "1": s.SetClipboard([]byte("Enjoy your new clipboard content!")) case "2": s.GetClipboard() } case tcell.KeyEscape, tcell.KeyCtrlQ: return } case *tcell.EventClipboard: clipboard = ev.Data() displayHelloWorld(s) } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/clipboard/clipboard_test.go000066400000000000000000000037511520475227200266370ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) // TestDemo tests the clipboard demo. func TestDemo(t *testing.T) { mt := vt.NewMockTerm() scr, err := tcell.NewTerminfoScreenFromTty(mt, tcell.OptColors(8), tcell.OptTerm("ansi")) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() // give enough time for the screen to draw // this is needed because main is running asynchronously. time.Sleep(time.Millisecond * 50) mt.KeyTap(vt.Key1) mt.Drain() time.Sleep(time.Millisecond * 20) mt.KeyTap(vt.Key2) mt.Drain() time.Sleep(time.Millisecond * 20) expect := "Enjoy your new clipboard content!" if result := string(mt.Backend().GetClipboard()); result != expect { t.Errorf("clipboard content did not match! %q != %q", result, expect) } // a long string mt.Backend().SetClipboard([]byte("The quick brown fox jumps over the lazy dog.")) mt.KeyTap(vt.Key2) mt.Drain() time.Sleep(time.Millisecond * 20) // stick some invalid utf-8 mt.Backend().SetClipboard([]byte{0xff}) mt.KeyTap(vt.Key2) mt.Drain() time.Sleep(time.Millisecond * 20) // now nil mt.Backend().SetClipboard(nil) mt.KeyTap(vt.Key2) mt.Drain() time.Sleep(time.Millisecond * 20) mt.KeyTap(vt.KeyLCtrl, vt.KeyQ) mt.Backend().GetSize() wg.Wait() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/colors/000077500000000000000000000000001520475227200226465ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/colors/colors.go000066400000000000000000000062301520475227200244770ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // colors just displays a single centered rectangle that should pulse // through available colors. It uses the RGB color cube, bumping at // predefined larger intervals (values of about 8) in order that the // changes happen quickly enough to be appreciable. // // Press ESC to exit the program. package main import ( "fmt" "math/rand" "os" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) var red = int32(rand.Int() % 256) var grn = int32(rand.Int() % 256) var blu = int32(rand.Int() % 256) var inc = int32(8) // rate of color change var redi = int32(inc) var grni = int32(inc) var blui = int32(inc) var interval = time.Millisecond * 50 func makeBox(s tcell.Screen) { w, h := s.Size() if w == 0 || h == 0 { return } glyphs := []string{"@", "#", "&", "*", "=", "%", "Z", "A"} lh := h / 2 lw := w / 2 lx := w / 4 ly := h / 4 st := tcell.StyleDefault gl := " " if s.Colors() == 0 { st = st.Reverse(rand.Int()%2 == 0) gl = glyphs[rand.Int()%len(glyphs)] } else { red += redi if (red >= 256) || (red < 0) { redi = -redi red += redi } grn += grni if (grn >= 256) || (grn < 0) { grni = -grni grn += grni } blu += blui if (blu >= 256) || (blu < 0) { blui = -blui blu += blui } st = st.Background(tcell.NewRGBColor(red, grn, blu)) } for row := range lh { for col := range lw { s.Put(lx+col, ly+row, gl, st) } } s.Show() } func flipCoin() bool { if rand.Int()&1 == 0 { return false } return true } func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } s.SetStyle(tcell.StyleDefault. Foreground(color.Black). Background(color.White)) s.Clear() quit := make(chan struct{}) go func() { for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape, tcell.KeyEnter, tcell.KeyCtrlQ: close(quit) return case tcell.KeyCtrlL: s.Sync() } case *tcell.EventResize: s.Sync() } } }() cnt := 0 dur := time.Duration(0) loop: for { select { case <-quit: break loop case <-time.After(interval): } start := time.Now() makeBox(s) dur += time.Since(start) cnt++ if cnt%(256/int(inc)) == 0 { if flipCoin() { redi = -redi } if flipCoin() { grni = -grni } if flipCoin() { blui = -blui } } } s.Fini() fmt.Printf("Finished %d boxes in %s\n", cnt, dur) fmt.Printf("Average is %0.3f ms / box\n", (float64(dur)/float64(cnt))/1000000.0) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/colors/colors_test.go000066400000000000000000000030501520475227200255330ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) // TestColors just exercises the code in the colors demo program. // It does not validate that the content is accurate, that should be done func TestColors(t *testing.T) { interval = time.Microsecond * 10 for _, colors := range []int{0, 8, 16, 88, 256, 1 << 24} { t.Run(fmt.Sprintf("%d_colors)", colors), func(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptColors(colors)) scr, err := tcell.NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() time.Sleep(time.Millisecond * 25) mt.KeyTap(vt.KeyLCtrl, vt.KeyL) mt.SetSize(vt.Coord{X: 10, Y: 10}) mt.Drain() time.Sleep(time.Millisecond * 25) mt.KeyTap(vt.KeyLCtrl, vt.KeyQ) mt.Drain() wg.Wait() }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/cursors/000077500000000000000000000000001520475227200230455ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/cursors/cursors.go000066400000000000000000000045261520475227200251030ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) func main() { tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defer s.Fini() s.SetStyle(tcell.StyleDefault) s.Clear() text := "This demonstrates cursor styles. Press 0 through 6 to change the style." s.PutStr(1, 1, text) s.Put(2, 2, "0", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleDefault) s.ShowCursor(3, 2) for { s.Show() ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyRune: switch ev.Str() { case "0": s.Put(2, 2, "0", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleDefault, color.Reset) case "1": s.Put(2, 2, "1", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleBlinkingBlock, color.Green) case "2": s.Put(2, 2, "2", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleSteadyBlock, color.Blue) case "3": s.Put(2, 2, "3", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleBlinkingUnderline, color.Red) case "4": s.Put(2, 2, "4", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleSteadyUnderline, color.Orange) case "5": s.Put(2, 2, "5", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleBlinkingBar, color.Yellow) case "6": s.Put(2, 2, "6", tcell.StyleDefault) s.SetCursorStyle(tcell.CursorStyleSteadyBar, color.Pink) } case tcell.KeyEscape, tcell.KeyEnter, tcell.KeyCtrlQ: return case tcell.KeyCtrlL: s.Sync() } case *tcell.EventResize: s.Sync() } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/cursors/cursors_test.go000066400000000000000000000042311520475227200261330ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) func verifyCursor(t *testing.T, term vt.MockTerm, expected vt.CursorStyle) { t.Helper() if actual := term.Backend().GetCursor(); actual != expected { t.Errorf("wrong cursor style %x != %x", actual, expected) } } // TestDemo tests the cursors demo. func TestDemo(t *testing.T) { mt := vt.NewMockTerm() scr, err := tcell.NewTerminfoScreenFromTty(mt, tcell.OptColors(8), tcell.OptTerm("ansi")) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() // give enough time for the screen to draw // this is needed because main is running asynchronously. time.Sleep(time.Millisecond * 50) mt.KeyTap(vt.Key1) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.BlinkingBlock) mt.KeyTap(vt.Key2) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.SteadyBlock) mt.KeyTap(vt.Key3) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.BlinkingUnderline) mt.KeyTap(vt.Key4) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.SteadyUnderline) mt.KeyTap(vt.Key5) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.BlinkingBar) mt.KeyTap(vt.Key6) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.SteadyBar) mt.KeyTap(vt.Key0) time.Sleep(time.Millisecond * 20) verifyCursor(t, mt, vt.BlinkingBlock) mt.KeyTap(vt.KeyRCtrl, vt.KeyL) mt.Drain() mt.KeyTap(vt.KeyRCtrl, vt.KeyQ) mt.Backend().GetSize() wg.Wait() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/hello/000077500000000000000000000000001520475227200224505ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/hello/hello.go000066400000000000000000000033361520475227200241070ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "os" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) func displayHelloWorld(s tcell.Screen) { w, h := s.Size() s.Clear() s.SetTitle("Hello World") x := w/2 - 7 y := h/2 - 1 style := tcell.StyleDefault.Foreground(color.CadetBlue.TrueColor()).Background(color.White) s.PutStrStyled(x, y, "Hello, World!", style) x = w/2 - 9 y += 2 s.PutStr(x, y, "Press ") x += len("Press ") s.PutStrStyled(x, y, "ESC", tcell.StyleDefault.Bold(true)) x += len("ESC") s.PutStr(x, y, " to exit.") s.Show() } // This program just prints "Hello, World!". Press ESC to exit. func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e := s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defStyle := tcell.StyleDefault. Background(color.Black). Foreground(color.White) s.SetStyle(defStyle) displayHelloWorld(s) for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventResize: s.Sync() displayHelloWorld(s) case *tcell.EventKey: if ev.Key() == tcell.KeyEscape { s.Fini() return } } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/hello/hello_test.go000066400000000000000000000075431520475227200251520ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/vt" ) func TestHello(t *testing.T) { // ensure we only use 8 color ANSI for now mt := vt.NewMockTerm(vt.MockOptColors(8)) scr, err := tcell.NewTerminfoScreenFromTty(mt, tcell.OptColors(8), tcell.OptTerm("ansi")) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() // give enough time for the screen to draw // this is needed because main is running asynchronously. time.Sleep(time.Millisecond * 50) mt.Drain() expect := []struct { X vt.Col Y vt.Row C string Fg color.Color Bg color.Color Attr vt.Attr }{ {X: 33, Y: 11, C: "H", Fg: color.Teal, Bg: color.Silver}, {X: 34, Y: 11, C: "e", Fg: color.Teal, Bg: color.Silver}, {X: 35, Y: 11, C: "l", Fg: color.Teal, Bg: color.Silver}, {X: 36, Y: 11, C: "l", Fg: color.Teal, Bg: color.Silver}, {X: 37, Y: 11, C: "o", Fg: color.Teal, Bg: color.Silver}, {X: 38, Y: 11, C: ",", Fg: color.Teal, Bg: color.Silver}, {X: 39, Y: 11, C: " ", Fg: color.Teal, Bg: color.Silver}, {X: 40, Y: 11, C: "W", Fg: color.Teal, Bg: color.Silver}, {X: 41, Y: 11, C: "o", Fg: color.Teal, Bg: color.Silver}, {X: 42, Y: 11, C: "r", Fg: color.Teal, Bg: color.Silver}, {X: 43, Y: 11, C: "l", Fg: color.Teal, Bg: color.Silver}, {X: 44, Y: 11, C: "d", Fg: color.Teal, Bg: color.Silver}, {X: 45, Y: 11, C: "!", Fg: color.Teal, Bg: color.Silver}, {X: 31, Y: 13, C: "P", Fg: color.Silver, Bg: color.Black}, {X: 32, Y: 13, C: "r", Fg: color.Silver, Bg: color.Black}, {X: 33, Y: 13, C: "e", Fg: color.Silver, Bg: color.Black}, {X: 34, Y: 13, C: "s", Fg: color.Silver, Bg: color.Black}, {X: 35, Y: 13, C: "s", Fg: color.Silver, Bg: color.Black}, {X: 36, Y: 13, C: " ", Fg: color.Silver, Bg: color.Black}, {X: 37, Y: 13, C: "E", Fg: color.Silver, Bg: color.Black, Attr: vt.Bold}, {X: 38, Y: 13, C: "S", Fg: color.Silver, Bg: color.Black, Attr: vt.Bold}, {X: 39, Y: 13, C: "C", Fg: color.Silver, Bg: color.Black, Attr: vt.Bold}, {X: 40, Y: 13, C: " ", Fg: color.Silver, Bg: color.Black}, {X: 41, Y: 13, C: "t", Fg: color.Silver, Bg: color.Black}, {X: 42, Y: 13, C: "o", Fg: color.Silver, Bg: color.Black}, {X: 43, Y: 13, C: " ", Fg: color.Silver, Bg: color.Black}, {X: 44, Y: 13, C: "e", Fg: color.Silver, Bg: color.Black}, {X: 45, Y: 13, C: "x", Fg: color.Silver, Bg: color.Black}, {X: 46, Y: 13, C: "i", Fg: color.Silver, Bg: color.Black}, {X: 47, Y: 13, C: "t", Fg: color.Silver, Bg: color.Black}, {X: 48, Y: 13, C: ".", Fg: color.Silver, Bg: color.Black}, } for _, v := range expect { cell := mt.GetCell(vt.Coord{X: v.X, Y: v.Y}) if v.C != string(cell.C) { t.Errorf("Mismatch string at %d,%d: %q != %q", v.X, v.Y, string(cell.C), v.C) } if v.Fg != cell.S.Fg() { t.Errorf("Mismatch foreground at %d,%d: %s != %s", v.X, v.Y, cell.S.Fg().String(), v.Fg.String()) } if v.Bg != cell.S.Bg() { t.Errorf("Mismatch background at %d,%d: %s != %s", v.X, v.Y, cell.S.Bg().String(), v.Bg.String()) } if v.Attr != cell.S.Attr() { t.Errorf("Mismatch attr at %d,%d: %x != %x", v.X, v.Y, cell.S.Attr(), v.Attr) } } mt.KeyTap(vt.KeyEsc) wg.Wait() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/styles/000077500000000000000000000000001520475227200226705ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/styles/styles.go000066400000000000000000000073341520475227200245510ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // styles just displays some sample output styles. // Press CTRL-Q to exit the program. package main import ( "fmt" "os" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) var row = 0 var style = tcell.StyleDefault func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } defer s.Fini() plain := tcell.StyleDefault bold := style.Bold(true) s.SetStyle(tcell.StyleDefault. Foreground(color.Black). Background(color.White)) s.Clear() quit := make(chan struct{}) style = bold.Foreground(color.Blue).Background(color.Silver) row = 2 s.PutStrStyled(2, row, "Press ESC to Exit", style) row = 4 s.PutStrStyled(2, row, "Note: Style support is dependent on your terminal.", plain) row = 6 plain = tcell.StyleDefault.Foreground(color.Black).Background(color.White) style = plain s.PutStrStyled(2, row, "Plain", style) row++ style = plain.Blink(true) s.PutStrStyled(2, row, "Blink", style) row++ style = plain.Reverse(true) s.PutStrStyled(2, row, "Reverse", style) row++ style = plain.Dim(true) s.PutStrStyled(2, row, "Dim", style) row++ style = plain.Underline(true) s.PutStrStyled(2, row, "Underline", style) row++ style = plain.Italic(true) s.PutStrStyled(2, row, "Italic", style) row++ style = plain.Bold(true) s.PutStrStyled(2, row, "Bold", style) row++ style = plain.Bold(true).Italic(true) s.PutStrStyled(2, row, "Bold Italic", style) row++ style = plain.Bold(true).Italic(true).Underline(true) s.PutStrStyled(2, row, "Bold Italic Underline", style) row++ style = plain.StrikeThrough(true) s.PutStrStyled(2, row, "StrikeThrough", style) row++ style = plain.Underline(tcell.UnderlineStyleDouble) s.PutStrStyled(2, row, "Double Underline", style) row++ style = plain.Underline(tcell.UnderlineStyleCurly) s.PutStrStyled(2, row, "Curly Underline", style) row++ style = plain.Underline(tcell.UnderlineStyleDotted) s.PutStrStyled(2, row, "Dotted Underline", style) row++ style = plain.Underline(tcell.UnderlineStyleDashed) s.PutStrStyled(2, row, "Dashed Underline", style) row++ style = plain.Underline(true, color.Blue) s.PutStrStyled(2, row, "Blue Underline", style) row++ style = plain.Underline(tcell.UnderlineStyleSolid, color.FireBrick) s.PutStrStyled(2, row, "Firebrick Underline", style) row++ style = plain.Underline(tcell.UnderlineStyleCurly, color.NewRGBColor(0xc5, 0x8a, 0xf9)) s.PutStrStyled(2, row, "Pink Curly Underline", style) row++ style = plain.Url("http://github.com/gdamore/tcell") s.PutStrStyled(2, row, "HyperLink", style) row++ style = plain.Foreground(color.Red) s.PutStrStyled(2, row, "Red Foreground", style) row++ style = plain.Background(color.Red) s.PutStrStyled(2, row, "Red Background", style) row++ s.Show() go func() { for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape, tcell.KeyEnter, tcell.KeyCtrlQ: close(quit) return case tcell.KeyCtrlL: s.Sync() } case *tcell.EventResize: s.Sync() } } }() <-quit } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/styles/styles_test.go000066400000000000000000000033361520475227200256060ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) // TestStyles just exercises the code in the styles demo program. // It does not validate that the content is accurate, that should be done // manually by running the program with a real terminal. func TestStyles(t *testing.T) { mt := vt.NewMockTerm() scr, err := tcell.NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() // give enough time for the screen to draw // this is needed because main is running asynchronously. time.Sleep(time.Millisecond * 50) mt.Drain() // control L (forces sync) mt.KeyTap(vt.KeyRCtrl, vt.KeyL) attrs := 0 var lastAttr vt.Attr for y := range vt.Row(24) { for x := range vt.Col(80) { if attr := mt.GetCell(vt.Coord{X: vt.Col(x), Y: vt.Row(y)}).S.Attr(); attr != lastAttr { attrs++ lastAttr = attr } } } mt.KeyTap(vt.KeyRCtrl, vt.KeyQ) wg.Wait() if attrs < 8 { t.Errorf("Not enough attribute changes changes: %d", attrs) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/unicode/000077500000000000000000000000001520475227200227735ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/unicode/unicode.go000066400000000000000000000061221520475227200247510ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // unicode just displays a Unicode test on your screen. // Press ESC to exit the program. package main import ( "fmt" "os" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/encoding" ) var row = 0 var style = tcell.StyleDefault func putln(s tcell.Screen, str string) { s.PutStrStyled(1, row, str, style) row++ } func main() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } encoding.Register() if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(1) } plain := tcell.StyleDefault bold := style.Bold(true) s.SetStyle(tcell.StyleDefault. Foreground(color.Black). Background(color.White)) s.Clear() // we can even try to use unicode window titles! s.SetTitle("Unicode Demonstration -- 🤯") quit := make(chan struct{}) style = bold putln(s, "Press Control-Q to Exit") putln(s, "Character set: "+s.CharacterSet()) style = plain putln(s, "English: October") putln(s, "Icelandic: október") putln(s, "Arabic: أكتوبر") putln(s, "Russian: октября") putln(s, "Greek: Οκτωβρίου") putln(s, "Chinese: 十月 (note, two double wide characters)") putln(s, "Combining: A\u030a (should look like Angstrom)") putln(s, "Emoticon: \U0001f618 (blowing a kiss)") putln(s, "Airplane: \u2708 (fly away)") putln(s, "Command: \u2318 (mac clover key)") putln(s, "Enclose: !\u20e3 (should be enclosed exclamation)") putln(s, "ZWJ: \U0001f9db\u200d\u2640 (female vampire)") putln(s, "ZWJ: \U0001f9db\u200d\u2642 (male vampire)") putln(s, "Family: \U0001f469\u200d\U0001f467\u200d\U0001f467 (woman girl girl)\n") putln(s, "Region: \U0001f1fa\U0001f1f8 (USA! USA!)\n") putln(s, "") putln(s, "Box:") putln(s, "┌─┬─┬──┐") putln(s, "│·│§│月│ (bullet, lantern, CJK)") putln(s, "├─┼─┼──┤") putln(s, "│A│1│😘│ (A, 1, Kiss)") putln(s, "├─┼─┼──┤") putln(s, "│·│Ω│🇨🇭│ (bullet, omega, Swiss)") putln(s, "├─┼─┼──┤") putln(s, "│◆│↑│>=│ (diamond, up arrow, greater-or-equal)") putln(s, "└─┴─┴──┘") s.Show() go func() { for { ev := <-s.EventQ() switch ev := ev.(type) { case *tcell.EventKey: switch ev.Key() { case tcell.KeyEscape, tcell.KeyEnter, tcell.KeyCtrlQ: close(quit) return case tcell.KeyCtrlL: s.Sync() } case *tcell.EventResize: s.Sync() } } }() <-quit s.Fini() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/demos/unicode/unicode_test.go000066400000000000000000000032621520475227200260120ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "sync" "testing" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/vt" ) // TestUnicode just exercises the code in the unicode demo program. // It does not validate that the content is accurate, that should be done // manually by running the program with a real terminal. func TestUnicode(t *testing.T) { mt := vt.NewMockTerm() scr, err := tcell.NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to create screen: %v", err) } tcell.ShimScreen(scr) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() main() }() // give enough time for the screen to draw // this is needed because main is running asynchronously. time.Sleep(time.Millisecond * 50) mt.Drain() // control L (forces sync) mt.KeyTap(vt.KeyLCtrl, vt.KeyL) attrs := 0 var lastAttr vt.Attr for y := range vt.Row(24) { for x := range vt.Col(80) { if attr := mt.GetCell(vt.Coord{X: vt.Col(x), Y: vt.Row(y)}).S.Attr(); attr != lastAttr { attrs++ lastAttr = attr } } } time.Sleep(time.Millisecond * 10) mt.KeyTap(vt.KeyLCtrl, vt.KeyQ) wg.Wait() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/doc.go000066400000000000000000000020511520475227200213300ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tcell provides a lower-level, portable API for building // programs that interact with terminals or consoles. It works with // both common (and many uncommon!) terminals or terminal emulators, // and Windows console implementations. // // It supports rich color, and modern terminal capabilities such as // rich key reporting, mouse tracking, bracketed paste, desktop notifications // and 24-bit color, when the underlying terminal supports it. package tcell golang-github-gdamore-tcell.v3-3.4.0+dfsg/eastasian.go000066400000000000000000000013121520475227200225320ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "github.com/gdamore/tcell/v3/internal/widthutil" ) var textWidthOptions = widthutil.Options() golang-github-gdamore-tcell.v3-3.4.0+dfsg/encoding.go000066400000000000000000000114041520475227200223530ustar00rootroot00000000000000// Copyright 2022 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "strings" "sync" "golang.org/x/text/encoding" gencoding "github.com/gdamore/encoding" ) var encodings map[string]encoding.Encoding var encodingLk sync.Mutex var encodingFallback EncodingFallback = EncodingFallbackASCII // RegisterEncoding may be called by the application to register an encoding. // The presence of additional encodings will facilitate application usage with // terminal environments where the I/O subsystem does not support Unicode. // // Modern systems and terminal emulators usually use UTF-8, and for those // systems, this API is also unnecessary. For example, Windows, macOS, and // modern Linux systems generally will work out of the box without any of this. // // Use of UTF-8 is recommended when possible, as it saves quite a lot processing overhead. // // Aliases can be registered as well, for example "8859-15" could be an alias // for "ISO8859-15". // // For POSIX systems, this package will check the environment variables // LC_ALL, LC_CTYPE, and LANG (in that order) to determine the character set. // These are expected to have the following pattern: // // $language[.$codeset[@$variant] // // We extract only the $codeset part, which will usually be something like // UTF-8 or ISO8859-15 or KOI8-R. Note that if the locale is either "POSIX" // or "C", then we assume US-ASCII (the POSIX 'portable character set' // and assume all other characters are somehow invalid.) // // Please see the Go documentation for golang.org/x/text/encoding -- most of // the common ones exist already as stock variables. For example, ISO8859-15 // can be registered using the following code: // // Note that some encodings are quite large (for example GB18030 which is a // superset of Unicode) and so the application size can be expected to // increase quite a bit as each encoding is added. // // The East Asian encodings have been seen to add 100-200K per encoding to the // size of the resulting binary. func RegisterEncoding(charset string, enc encoding.Encoding) { encodingLk.Lock() charset = strings.ToLower(charset) encodings[charset] = enc encodingLk.Unlock() } // EncodingFallback describes how the system behaves when the locale // requires a character set that we do not support. The system always // supports UTF-8 and US-ASCII. On Windows consoles, UTF-16LE is also // supported automatically. Other character sets must be added using the // RegisterEncoding API. (A large group of nearly all of them can be // added using the RegisterAll function in the encoding sub package.) // The default action will be to fallback to UTF-8. type EncodingFallback int const ( // EncodingFallbackUTF8 behavior causes GetEncoding to assume // UTF8 can pass unmodified upon failure. EncodingFallbackUTF8 = iota // EncodingFallbackFail behavior causes GetEncoding to fail // when it cannot find an encoding. EncodingFallbackFail // EncodingFallbackASCII behavior causes GetEncoding to fall back // to a 7-bit ASCII encoding, if no other encoding can be found. EncodingFallbackASCII ) // SetEncodingFallback changes the behavior of GetEncoding when a suitable // encoding is not found. The default is EncodingFallbackFail, which // causes GetEncoding to simply return nil. func SetEncodingFallback(fb EncodingFallback) { encodingLk.Lock() encodingFallback = fb encodingLk.Unlock() } // GetEncoding is used by Screen implementors who want to locate an encoding // for the given character set name. Note that this will return nil for // either the Unicode (UTF-8) or ASCII encodings, since we don't use // encodings for them but instead have our own native methods. func GetEncoding(charset string) encoding.Encoding { charset = strings.ToLower(charset) encodingLk.Lock() defer encodingLk.Unlock() if enc, ok := encodings[charset]; ok { return enc } switch encodingFallback { case EncodingFallbackASCII: return gencoding.ASCII case EncodingFallbackUTF8: return encoding.Nop } return nil } func init() { // We always support UTF-8 and ASCII. encodings = make(map[string]encoding.Encoding) encodings["utf-8"] = gencoding.UTF8 encodings["utf8"] = gencoding.UTF8 encodings["us-ascii"] = gencoding.ASCII encodings["ascii"] = gencoding.ASCII encodings["iso646"] = gencoding.ASCII } golang-github-gdamore-tcell.v3-3.4.0+dfsg/encoding/000077500000000000000000000000001520475227200220245ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/encoding/all.go000066400000000000000000000105601520475227200231250ustar00rootroot00000000000000// Copyright 2015 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package encoding is used to provide a fairly complete set of encodings // for tcell applications. Importing this package will automatically // register encodings. Note that this package will add several MB to the // generated binaries, as the encodings themselves can be somewhat large, // particularly for the East Asian locales. package encoding import ( "github.com/gdamore/encoding" "github.com/gdamore/tcell/v3" "golang.org/x/text/encoding/charmap" "golang.org/x/text/encoding/japanese" "golang.org/x/text/encoding/korean" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/traditionalchinese" ) // Register registers all known encodings. This is a short-cut to // add full character set support to your program. Note that this can // add several megabytes to your program's size, because some of the encodings // are rather large (particularly those from East Asia.) // // Deprecated: This is no longer needed, importing the package is sufficient. func Register() { registerAll() } func registerAll() { // We supply latin1 and latin5, because Go doesn't tcell.RegisterEncoding("ISO8859-1", encoding.ISO8859_1) tcell.RegisterEncoding("ISO8859-9", encoding.ISO8859_9) tcell.RegisterEncoding("ISO8859-10", charmap.ISO8859_10) tcell.RegisterEncoding("ISO8859-13", charmap.ISO8859_13) tcell.RegisterEncoding("ISO8859-14", charmap.ISO8859_14) tcell.RegisterEncoding("ISO8859-15", charmap.ISO8859_15) tcell.RegisterEncoding("ISO8859-16", charmap.ISO8859_16) tcell.RegisterEncoding("ISO8859-2", charmap.ISO8859_2) tcell.RegisterEncoding("ISO8859-3", charmap.ISO8859_3) tcell.RegisterEncoding("ISO8859-4", charmap.ISO8859_4) tcell.RegisterEncoding("ISO8859-5", charmap.ISO8859_5) tcell.RegisterEncoding("ISO8859-6", charmap.ISO8859_6) tcell.RegisterEncoding("ISO8859-7", charmap.ISO8859_7) tcell.RegisterEncoding("ISO8859-8", charmap.ISO8859_8) tcell.RegisterEncoding("KOI8-R", charmap.KOI8R) tcell.RegisterEncoding("KOI8-U", charmap.KOI8U) // Asian stuff tcell.RegisterEncoding("EUC-JP", japanese.EUCJP) tcell.RegisterEncoding("SHIFT_JIS", japanese.ShiftJIS) tcell.RegisterEncoding("ISO2022JP", japanese.ISO2022JP) tcell.RegisterEncoding("EUC-KR", korean.EUCKR) tcell.RegisterEncoding("GB18030", simplifiedchinese.GB18030) tcell.RegisterEncoding("GB2312", simplifiedchinese.HZGB2312) tcell.RegisterEncoding("GBK", simplifiedchinese.GBK) tcell.RegisterEncoding("Big5", traditionalchinese.Big5) // Common aliases aliases := map[string]string{ "8859-1": "ISO8859-1", "ISO-8859-1": "ISO8859-1", "8859-13": "ISO8859-13", "ISO-8859-13": "ISO8859-13", "8859-14": "ISO8859-14", "ISO-8859-14": "ISO8859-14", "8859-15": "ISO8859-15", "ISO-8859-15": "ISO8859-15", "8859-16": "ISO8859-16", "ISO-8859-16": "ISO8859-16", "8859-2": "ISO8859-2", "ISO-8859-2": "ISO8859-2", "8859-3": "ISO8859-3", "ISO-8859-3": "ISO8859-3", "8859-4": "ISO8859-4", "ISO-8859-4": "ISO8859-4", "8859-5": "ISO8859-5", "ISO-8859-5": "ISO8859-5", "8859-6": "ISO8859-6", "ISO-8859-6": "ISO8859-6", "8859-7": "ISO8859-7", "ISO-8859-7": "ISO8859-7", "8859-8": "ISO8859-8", "ISO-8859-8": "ISO8859-8", "8859-9": "ISO8859-9", "ISO-8859-9": "ISO8859-9", "SJIS": "Shift_JIS", "EUCJP": "EUC-JP", "2022-JP": "ISO2022JP", "ISO-2022-JP": "ISO2022JP", "EUCKR": "EUC-KR", // ISO646 isn't quite exactly ASCII, but the 1991 IRV // (international reference version) is so. This helps // some older systems that may use "646" for POSIX locales. "646": "US-ASCII", "ISO646": "US-ASCII", // Other names for UTF-8 "UTF8": "UTF-8", } for n, v := range aliases { if enc := tcell.GetEncoding(v); enc != nil { tcell.RegisterEncoding(n, enc) } } } func init() { registerAll() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/encoding/encoding_init_test.go000066400000000000000000000035161520475227200262300ustar00rootroot00000000000000// Copyright 2022 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package encoding import ( "testing" "github.com/gdamore/tcell/v3" ) func TestGBK(t *testing.T) { enc := tcell.GetEncoding("GBK") if enc == nil { t.Fatal("NULL encoding for GBK") } glyph, _ := enc.NewDecoder().Bytes([]byte{0x82, 0x74}) if string(glyph) != "倀" { t.Errorf("failed to match: %s != 倀", string(glyph)) } } func TestAscii(t *testing.T) { encodings := []string{ "ASCII", "ISO-8859-1", "KOI8-R", "KOI8-U", "SJIS", "Big5", "GB2312", "GB18030", "EUC-JP", "EUCKR", } for _, name := range encodings { t.Run(name, func(t *testing.T) { enc := tcell.GetEncoding(name) if enc == nil { t.Errorf("Failed getting encoding for %s", name) return } encoder := enc.NewEncoder() decoder := enc.NewDecoder() // Ensure that all US-ASCII (lower 7 bit values) encode and decode identically for i := byte(0); i < 126; i++ { // well, KOI8-R has some problem with "~" s := string([]byte{i}) if x, err := encoder.String(s); err != nil || x != s { t.Errorf("failed encoding for character: %d, err %v expect %s got %s", i, err, s, x) } if x, err := decoder.String(s); err != nil || x != s { t.Errorf("failed decoding for character: %d, err %v expect %s got %s", i, err, s, x) } } }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/encoding_test.go000066400000000000000000000015631520475227200234170ustar00rootroot00000000000000// Copyright 2022 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "fmt" "golang.org/x/text/encoding/simplifiedchinese" ) func ExampleRegisterEncoding() { RegisterEncoding("GBK", simplifiedchinese.GBK) enc := GetEncoding("GBK") glyph, _ := enc.NewDecoder().Bytes([]byte{0x82, 0x74}) fmt.Println(string(glyph)) // Output: 倀 } golang-github-gdamore-tcell.v3-3.4.0+dfsg/errors.go000066400000000000000000000041361520475227200221050ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "errors" ) var ( // ErrNoScreen indicates that no suitable screen could be found. // This may result from attempting to run on a platform where there // is no support for either termios or console I/O (such as js), // or from running in an environment where there is no access to // a suitable console/terminal device. (For example, running on // without a controlling TTY or with no /dev/tty on POSIX platforms.) ErrNoScreen = errors.New("no suitable screen available") // ErrNoCharset indicates that the locale environment the // program is not supported by the program, because no suitable // encoding was found for it. This problem never occurs if // the environment is UTF-8 or UTF-16. ErrNoCharset = errors.New("character set not supported") // ErrEventQFull indicates that the event queue is full, and // cannot accept more events. ErrEventQFull = errors.New("event queue full") ) // An EventError is an event representing some sort of error, and carries // an error payload. type EventError struct { EventTime err error } // Error implements the error. func (ev *EventError) Error() string { return ev.err.Error() } // Unwrap exposes the underlying error payload so callers can use // errors.Is / errors.As to match against sentinel values such as // io.EOF. func (ev *EventError) Unwrap() error { return ev.err } // NewEventError creates an ErrorEvent with the given error payload. func NewEventError(err error) *EventError { ev := &EventError{err: err} ev.SetEventNow() return ev } golang-github-gdamore-tcell.v3-3.4.0+dfsg/event.go000066400000000000000000000030261520475227200217070ustar00rootroot00000000000000// Copyright 2015 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "time" ) // Event is a generic interface used for passing around Events. // Concrete types follow. type Event interface { // When reports the time when the event was generated. When() time.Time } // EventTime is a simple base event class, suitable for easy reuse. // It can be used to deliver actual timer events as well. type EventTime struct { when time.Time } // When returns the time stamp when the event occurred. func (e *EventTime) When() time.Time { return e.when } // SetEventTime sets the time of occurrence for the event. func (e *EventTime) SetEventTime(t time.Time) { e.when = t } // SetEventNow sets the time of occurrence for the event to the current time. func (e *EventTime) SetEventNow() { e.SetEventTime(time.Now()) } // EventHandler is anything that handles events. If the handler has // consumed the event, it should return true. False otherwise. type EventHandler interface { HandleEvent(Event) bool } golang-github-gdamore-tcell.v3-3.4.0+dfsg/focus.go000066400000000000000000000016551520475227200217130ustar00rootroot00000000000000// Copyright 2023 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // EventFocus is a focus event. It is sent when the terminal window (or tab) // gets or loses focus. type EventFocus struct { EventTime // True if the window received focus, false if it lost focus Focused bool } func NewEventFocus(focused bool) *EventFocus { ev := &EventFocus{Focused: focused} ev.SetEventNow() return ev } golang-github-gdamore-tcell.v3-3.4.0+dfsg/go.mod000066400000000000000000000004711520475227200213460ustar00rootroot00000000000000module github.com/gdamore/tcell/v3 go 1.25.0 require ( github.com/clipperhouse/displaywidth v0.11.0 github.com/gdamore/encoding v1.0.1 github.com/lucasb-eyer/go-colorful v1.4.0 golang.org/x/sys v0.44.0 golang.org/x/term v0.43.0 golang.org/x/text v0.37.0 ) require github.com/clipperhouse/uax29/v2 v2.7.0 golang-github-gdamore-tcell.v3-3.4.0+dfsg/go.sum000066400000000000000000000101461520475227200213730ustar00rootroot00000000000000github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang-github-gdamore-tcell.v3-3.4.0+dfsg/input.go000066400000000000000000001231371520475227200217330ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This file describes a generic VT input processor. It parses key sequences, // (input bytes) and loads them into events. It expects UTF-8 or UTF-16 as the input // feed, along with ECMA-48 sequences. The assumption here is that all potential // key sequences are unambiguous between terminal variants (analysis of extant terminfo // data appears to support this conjecture). This allows us to implement this once, // in the most efficient and terminal-agnostic way possible. // // There is unfortunately *one* conflict, with aixterm, for CSI-P - which is KeyDelete // in aixterm, but F1 in others. //go:build (!js && !wasm) || (js && wasm) // +build !js,!wasm js,wasm package tcell import ( "encoding/base64" "os" "strconv" "strings" "sync" "time" "unicode/utf16" "unicode/utf8" "github.com/gdamore/tcell/v3/vt" ) type inputState int const ( istInit = inputState(iota) istUtf // utf8 state istEsc // escape istCsi // control sequence introducer istOsc // operating system command istDcs // device control string istSos // start of string (unused) istPm // privacy message (unused) istApc // application program command istSt // string terminator istSs2 // single shift 2 istSs3 // single shift 3 istLnx // linux F-key (not ECMA-48 compliant - bogus CSI) istXda // extended device attributes (ESC P Ps ST) ) // defaultControlStringLimit caps inbound OSC/XDA control-string payloads // before they can grow without bound while waiting for a string terminator. const defaultControlStringLimit = 64 * 1024 func newInputParser(eq chan<- Event) *inputParser { return &inputParser{ evch: eq, buf: make([]rune, 0, 128), controlStringMax: defaultControlStringLimit, } } type inputParser struct { buf []rune // bytes to process (ingest data) utfBuf []byte // accrued UTF8 bytes strBuf []byte // accrued string data (for ST, OSC, etc.) csiParams []byte // accrued parameter bytes for CSI (and SS3) csiInterm []byte // accrued intermediate bytes for CSI escChar byte // last byte for escape escaped bool // true if next key should be modified by ESC btnsDown ButtonMask // mouse buttons down (excludes wheel buttons) state inputState // tracks processor state strState inputState // saved str state (needed for ST) l sync.Mutex // protects local state evch chan<- Event // where events are routed rows int // used for clipping mouse coordinates cols int // used for clipping mouse coordinates pixelMouse bool // mouse reports in pixels (CSI ?1016h); skip cell clipping keyTime time.Time // time of last key press / byte ingested nested *inputParser // for buggy win32-input-mode implementations surrogate rune // high surrogate pair seen (for Win32 input mode) advanced bool // use advanced key reporting semantics controlStringMax int // maximum inbound OSC/XDA payload size; 0 means unlimited discardString bool // drop the rest of an over-limit OSC/XDA sequence } func keyFromInt(n int) (Key, bool) { if n < 0 || n > 32767 { return 0, false } return Key(n), true } func keyFromRune(r rune) (Key, bool) { if r < 0 || r > 32767 { return 0, false } return Key(r), true } func asciiByteFromInt(n int) (byte, bool) { if n <= 0 || n >= 0x80 { return 0, false } return byte(n), true } // Waiting returns true if the processor is waiting for // some more input (i.e. we are not in in the initial state.) // This can occur when we have ambiguous escape sequences, such // as the lone escape. If this is typed, we expect at least a minimal // inter-key delay before the next stroke occurs, and the caller // should check for waiting, and call Scan() or ScanUTF8() to // finish the processing. (Typically after a delay of around 100ms.) func (ip *inputParser) Waiting() bool { ip.l.Lock() defer ip.l.Unlock() return ip.state != istInit } // SetPixelMouse toggles whether SGR mouse reports are interpreted as // pixel coordinates (CSI ?1016h) rather than character cells (CSI ?1006h). // When enabled, mouse coordinates are not clipped to the screen size. // The setting is also forwarded to the lazily-created nested parser used // for win32-input-mode, if one exists, so both stay in sync. func (ip *inputParser) SetPixelMouse(on bool) { ip.l.Lock() ip.pixelMouse = on nested := ip.nested ip.l.Unlock() if nested != nil { nested.SetPixelMouse(on) } } func (ip *inputParser) SetSize(w, h int) { if ip.nested != nil { ip.nested.SetSize(w, h) return } go func() { ip.l.Lock() ip.rows = h ip.cols = w ip.post(NewEventResize(w, h)) ip.l.Unlock() }() } func (ip *inputParser) post(ev Event) { if ip.escaped { ip.escaped = false if ke, ok := ev.(*EventKey); ok { ev = ip.newKey(ke.Key(), ke.Str(), ke.Modifiers()|ModAlt, ke.Pressed(), ke.Physical(), ke.Repeat()) } } else if ke, ok := ev.(*EventKey); ok { switch ke.Key() { case keyPasteStart: ev = NewEventPaste(true) case keyPasteEnd: ev = NewEventPaste(false) } } ip.evch <- ev } func (ip *inputParser) newKey(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) *EventKey { if ip.advanced { return NewEventKeyEx(k, str, mod, pressed, physical, repeat) } return NewEventKey(k, str, mod) } func (ip *inputParser) postKey(k Key, str string, mod ModMask) { ip.post(ip.newKey(k, str, mod, true, 0, 1)) } func (ip *inputParser) postKeyEx(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) { ip.post(ip.newKey(k, str, mod, pressed, physical, repeat)) } func (ip *inputParser) postControlKey(r rune, mod ModMask) { if r == 0 { ip.postKeyEx(KeyRune, " ", mod|ModCtrl, true, Key(' '), 1) } else if ip.advanced && r >= 1 && r <= 26 { ip.postKeyEx(KeyRune, string('a'+r-1), mod|ModCtrl, true, Key('a'+r-1), 1) } else { ip.postKey(KeyRune, string(r+0x40), mod|ModCtrl) } } type csiParamMode struct { M rune // Mode P int // Parameter (first) } type keyMap struct { Key Key Mod ModMask Rune rune } var csiAllKeys = map[csiParamMode]keyMap{ {M: 'A'}: {Key: KeyUp}, {M: 'B'}: {Key: KeyDown}, {M: 'C'}: {Key: KeyRight}, {M: 'D'}: {Key: KeyLeft}, {M: 'E'}: {Key: KeyClear}, {M: 'F'}: {Key: KeyEnd}, {M: 'H'}: {Key: KeyHome}, {M: 'L'}: {Key: KeyInsert}, {M: 'P'}: {Key: KeyF1}, // except for aixterm, where this is Delete {M: 'Q'}: {Key: KeyF2}, {M: 'S'}: {Key: KeyF4}, {M: 'Z'}: {Key: KeyBacktab}, {M: 'a'}: {Key: KeyUp, Mod: ModShift}, {M: 'b'}: {Key: KeyDown, Mod: ModShift}, {M: 'c'}: {Key: KeyRight, Mod: ModShift}, {M: 'd'}: {Key: KeyLeft, Mod: ModShift}, {M: 'q', P: 1}: {Key: KeyF1}, // all these 'q' are for aixterm {M: 'q', P: 2}: {Key: KeyF2}, {M: 'q', P: 3}: {Key: KeyF3}, {M: 'q', P: 4}: {Key: KeyF4}, {M: 'q', P: 5}: {Key: KeyF5}, {M: 'q', P: 6}: {Key: KeyF6}, {M: 'q', P: 7}: {Key: KeyF7}, {M: 'q', P: 8}: {Key: KeyF8}, {M: 'q', P: 9}: {Key: KeyF9}, {M: 'q', P: 10}: {Key: KeyF10}, {M: 'q', P: 11}: {Key: KeyF11}, {M: 'q', P: 12}: {Key: KeyF12}, {M: 'q', P: 13}: {Key: KeyF13}, {M: 'q', P: 14}: {Key: KeyF14}, {M: 'q', P: 15}: {Key: KeyF15}, {M: 'q', P: 16}: {Key: KeyF16}, {M: 'q', P: 17}: {Key: KeyF17}, {M: 'q', P: 18}: {Key: KeyF18}, {M: 'q', P: 19}: {Key: KeyF19}, {M: 'q', P: 20}: {Key: KeyF20}, {M: 'q', P: 21}: {Key: KeyF21}, {M: 'q', P: 22}: {Key: KeyF22}, {M: 'q', P: 23}: {Key: KeyF23}, {M: 'q', P: 24}: {Key: KeyF24}, {M: 'q', P: 25}: {Key: KeyF25}, {M: 'q', P: 26}: {Key: KeyF26}, {M: 'q', P: 27}: {Key: KeyF27}, {M: 'q', P: 28}: {Key: KeyF28}, {M: 'q', P: 29}: {Key: KeyF29}, {M: 'q', P: 30}: {Key: KeyF30}, {M: 'q', P: 31}: {Key: KeyF31}, {M: 'q', P: 32}: {Key: KeyF32}, {M: 'q', P: 33}: {Key: KeyF33}, {M: 'q', P: 34}: {Key: KeyF34}, {M: 'q', P: 35}: {Key: KeyF35}, {M: 'q', P: 36}: {Key: KeyF36}, {M: 'q', P: 144}: {Key: KeyClear}, {M: 'q', P: 146}: {Key: KeyEnd}, {M: 'q', P: 150}: {Key: KeyPgUp}, {M: 'q', P: 154}: {Key: KeyPgDn}, {M: 'z', P: 214}: {Key: KeyHome}, {M: 'z', P: 216}: {Key: KeyPgUp}, {M: 'z', P: 220}: {Key: KeyEnd}, {M: 'z', P: 222}: {Key: KeyPgDn}, {M: 'z', P: 224}: {Key: KeyF1}, {M: 'z', P: 225}: {Key: KeyF2}, {M: 'z', P: 226}: {Key: KeyF3}, {M: 'z', P: 227}: {Key: KeyF4}, {M: 'z', P: 228}: {Key: KeyF5}, {M: 'z', P: 229}: {Key: KeyF6}, {M: 'z', P: 230}: {Key: KeyF7}, {M: 'z', P: 231}: {Key: KeyF8}, {M: 'z', P: 232}: {Key: KeyF9}, {M: 'z', P: 233}: {Key: KeyF10}, {M: 'z', P: 234}: {Key: KeyF11}, {M: 'z', P: 235}: {Key: KeyF12}, {M: 'z', P: 247}: {Key: KeyInsert}, {M: '^', P: 1}: {Key: KeyHome, Mod: ModCtrl}, {M: '^', P: 2}: {Key: KeyInsert, Mod: ModCtrl}, {M: '^', P: 3}: {Key: KeyDelete, Mod: ModCtrl}, {M: '^', P: 4}: {Key: KeyEnd, Mod: ModCtrl}, {M: '^', P: 5}: {Key: KeyPgUp, Mod: ModCtrl}, {M: '^', P: 6}: {Key: KeyPgDn, Mod: ModCtrl}, {M: '^', P: 7}: {Key: KeyHome, Mod: ModCtrl}, {M: '^', P: 8}: {Key: KeyEnd, Mod: ModCtrl}, {M: '^', P: 11}: {Key: KeyF23}, {M: '^', P: 12}: {Key: KeyF24}, {M: '^', P: 13}: {Key: KeyF25}, {M: '^', P: 14}: {Key: KeyF26}, {M: '^', P: 15}: {Key: KeyF27}, {M: '^', P: 17}: {Key: KeyF28}, // 16 is a gap {M: '^', P: 18}: {Key: KeyF29}, {M: '^', P: 19}: {Key: KeyF30}, {M: '^', P: 20}: {Key: KeyF31}, {M: '^', P: 21}: {Key: KeyF32}, {M: '^', P: 23}: {Key: KeyF33}, // 22 is a gap {M: '^', P: 24}: {Key: KeyF34}, {M: '^', P: 25}: {Key: KeyF35}, {M: '^', P: 26}: {Key: KeyF36}, // 27 is a gap {M: '^', P: 28}: {Key: KeyF37}, {M: '^', P: 29}: {Key: KeyF38}, // 30 is a gap {M: '^', P: 31}: {Key: KeyF39}, {M: '^', P: 32}: {Key: KeyF40}, {M: '^', P: 33}: {Key: KeyF41}, {M: '^', P: 34}: {Key: KeyF42}, {M: '@', P: 23}: {Key: KeyF43}, {M: '@', P: 24}: {Key: KeyF44}, {M: '@', P: 1}: {Key: KeyHome, Mod: ModShift | ModCtrl}, {M: '@', P: 2}: {Key: KeyInsert, Mod: ModShift | ModCtrl}, {M: '@', P: 3}: {Key: KeyDelete, Mod: ModShift | ModCtrl}, {M: '@', P: 4}: {Key: KeyEnd, Mod: ModShift | ModCtrl}, {M: '@', P: 5}: {Key: KeyPgUp, Mod: ModShift | ModCtrl}, {M: '@', P: 6}: {Key: KeyPgDn, Mod: ModShift | ModCtrl}, {M: '@', P: 7}: {Key: KeyHome, Mod: ModShift | ModCtrl}, {M: '@', P: 8}: {Key: KeyEnd, Mod: ModShift | ModCtrl}, {M: '$', P: 1}: {Key: KeyHome, Mod: ModShift}, {M: '$', P: 2}: {Key: KeyInsert, Mod: ModShift}, {M: '$', P: 3}: {Key: KeyDelete, Mod: ModShift}, {M: '$', P: 5}: {Key: KeyPgUp, Mod: ModShift}, {M: '$', P: 6}: {Key: KeyPgDn, Mod: ModShift}, {M: '$', P: 7}: {Key: KeyHome, Mod: ModShift}, {M: '$', P: 8}: {Key: KeyEnd, Mod: ModShift}, {M: '$', P: 23}: {Key: KeyF21}, {M: '$', P: 24}: {Key: KeyF22}, {M: '~', P: 1}: {Key: KeyHome}, {M: '~', P: 2}: {Key: KeyInsert}, {M: '~', P: 3}: {Key: KeyDelete}, {M: '~', P: 4}: {Key: KeyEnd}, {M: '~', P: 5}: {Key: KeyPgUp}, {M: '~', P: 6}: {Key: KeyPgDn}, {M: '~', P: 7}: {Key: KeyHome}, {M: '~', P: 8}: {Key: KeyEnd}, {M: '~', P: 11}: {Key: KeyF1}, {M: '~', P: 12}: {Key: KeyF2}, {M: '~', P: 13}: {Key: KeyF3}, {M: '~', P: 14}: {Key: KeyF4}, {M: '~', P: 15}: {Key: KeyF5}, {M: '~', P: 17}: {Key: KeyF6}, {M: '~', P: 18}: {Key: KeyF7}, {M: '~', P: 19}: {Key: KeyF8}, {M: '~', P: 20}: {Key: KeyF9}, {M: '~', P: 21}: {Key: KeyF10}, {M: '~', P: 23}: {Key: KeyF11}, {M: '~', P: 24}: {Key: KeyF12}, {M: '~', P: 25}: {Key: KeyF13}, {M: '~', P: 26}: {Key: KeyF14}, {M: '~', P: 28}: {Key: KeyF15}, // aka KeyHelp {M: '~', P: 29}: {Key: KeyF16}, {M: '~', P: 31}: {Key: KeyF17}, {M: '~', P: 32}: {Key: KeyF18}, {M: '~', P: 33}: {Key: KeyF19}, {M: '~', P: 34}: {Key: KeyF20}, {M: '~', P: 200}: {Key: keyPasteStart}, {M: '~', P: 201}: {Key: keyPasteEnd}, } // keys reported using Kitty csi-u protocol var csiUKeys = map[int]keyMap{ 27: {Key: KeyESC}, 9: {Key: KeyTAB}, 13: {Key: KeyEnter}, 127: {Key: KeyBS}, 57358: {Key: KeyCapsLock}, 57359: {Key: KeyScrollLock}, 57360: {Key: KeyNumLock}, 57361: {Key: KeyPrint}, 57362: {Key: KeyPause}, 57363: {Key: KeyMenu}, 57376: {Key: KeyF13}, 57377: {Key: KeyF14}, 57378: {Key: KeyF15}, 57379: {Key: KeyF16}, 57380: {Key: KeyF17}, 57381: {Key: KeyF18}, 57382: {Key: KeyF19}, 57383: {Key: KeyF20}, 57384: {Key: KeyF21}, 57385: {Key: KeyF22}, 57386: {Key: KeyF23}, 57387: {Key: KeyF24}, 57388: {Key: KeyF25}, 57389: {Key: KeyF26}, 57390: {Key: KeyF27}, 57391: {Key: KeyF28}, 57392: {Key: KeyF29}, 57393: {Key: KeyF30}, 57394: {Key: KeyF31}, 57395: {Key: KeyF32}, 57396: {Key: KeyF33}, 57397: {Key: KeyF34}, 57398: {Key: KeyF35}, 57399: {Key: KeyRune, Rune: '0'}, // KP 0 57400: {Key: KeyRune, Rune: '1'}, // KP 1 57401: {Key: KeyRune, Rune: '2'}, // KP 2 57402: {Key: KeyRune, Rune: '3'}, // KP 3 57403: {Key: KeyRune, Rune: '4'}, // KP 4 57404: {Key: KeyRune, Rune: '5'}, // KP 5 57405: {Key: KeyRune, Rune: '6'}, // KP 6 57406: {Key: KeyRune, Rune: '7'}, // KP 7 57407: {Key: KeyRune, Rune: '8'}, // KP 8 57408: {Key: KeyRune, Rune: '9'}, // KP 9 57409: {Key: KeyRune, Rune: '.'}, // KP_DECIMAL 57410: {Key: KeyRune, Rune: '/'}, // KP_DIVIDE 57411: {Key: KeyRune, Rune: '*'}, // KP_MULTIPLY 57412: {Key: KeyRune, Rune: '-'}, // KP_SUBTRACT 57413: {Key: KeyRune, Rune: '+'}, // KP_ADD 57414: {Key: KeyEnter}, // KP_ENTER 57415: {Key: KeyRune, Rune: '='}, // KP_EQUAL 57416: {Key: KeyClear}, // KP_SEPARATOR 57417: {Key: KeyLeft}, // KP_LEFT 57418: {Key: KeyRight}, // KP_RIGHT 57419: {Key: KeyUp}, // KP_UP 57420: {Key: KeyDown}, // KP_DOWN 57421: {Key: KeyPgUp}, // KP_PG_UP 57422: {Key: KeyPgDn}, // KP_PG_DN 57423: {Key: KeyHome}, // KP_HOME 57424: {Key: KeyEnd}, // KP_END 57425: {Key: KeyInsert}, // KP_INSERT 57426: {Key: KeyDelete}, // KP_DELETE // 57427: {Key: KeyBegin}, // KP_BEGIN 57441: {Key: KeyShift}, // LEFT_SHIFT 57442: {Key: KeyCtrl}, // LEFT_CONTROL 57443: {Key: KeyAlt}, // LEFT_ALT 57444: {Key: KeyMeta}, // LEFT_SUPER 57447: {Key: KeyShift}, // RIGHT_SHIFT 57448: {Key: KeyCtrl}, // RIGHT_CONTROL 57449: {Key: KeyAlt}, // RIGHT_ALT 57450: {Key: KeyMeta}, // RIGHT_SUPER // TODO: Media keys } // windows virtual key codes per microsoft var winKeys = map[int]Key{ 0x03: KeyCancel, // vkCancel 0x08: KeyBackspace, // vkBackspace 0x09: KeyTab, // vkTab 0x0d: KeyEnter, // vkReturn 0x13: KeyPause, // vkPause 0x1b: KeyEscape, // vkEscape 0x21: KeyPgUp, // vkPrior 0x22: KeyPgDn, // vkNext 0x23: KeyEnd, // vkEnd 0x24: KeyHome, // vkHome 0x25: KeyLeft, // vkLeft 0x26: KeyUp, // vkUp 0x27: KeyRight, // vkRight 0x28: KeyDown, // vkDown 0x2a: KeyPrint, // vkPrint 0x2c: KeyPrint, // vkPrtScr 0x2d: KeyInsert, // vkInsert 0x2e: KeyDelete, // vkDelete 0x2f: KeyHelp, // vkHelp 0x70: KeyF1, // vkF1 0x71: KeyF2, // vkF2 0x72: KeyF3, // vkF3 0x73: KeyF4, // vkF4 0x74: KeyF5, // vkF5 0x75: KeyF6, // vkF6 0x76: KeyF7, // vkF7 0x77: KeyF8, // vkF8 0x78: KeyF9, // vkF9 0x79: KeyF10, // vkF10 0x7a: KeyF11, // vkF11 0x7b: KeyF12, // vkF12 0x7c: KeyF13, // vkF13 0x7d: KeyF14, // vkF14 0x7e: KeyF15, // vkF15 0x7f: KeyF16, // vkF16 0x80: KeyF17, // vkF17 0x81: KeyF18, // vkF18 0x82: KeyF19, // vkF19 0x83: KeyF20, // vkF20 0x84: KeyF21, // vkF21 0x85: KeyF22, // vkF22 0x86: KeyF23, // vkF23 0x87: KeyF24, // vkF24 } // keys by their SS3 - used in application mode usually (legacy VT-style) var ss3Keys = map[rune]Key{ 'A': KeyUp, 'B': KeyDown, 'C': KeyRight, 'D': KeyLeft, 'E': KeyClear, 'F': KeyEnd, 'H': KeyHome, 'P': KeyF1, 'Q': KeyF2, 'R': KeyF3, 'S': KeyF4, 't': KeyF5, 'u': KeyF6, 'v': KeyF7, 'l': KeyF8, 'w': KeyF9, 'x': KeyF10, } // linux terminal uses these non ECMA keys prefixed by CSI-[ var linuxFKeys = map[rune]Key{ 'A': KeyF1, 'B': KeyF2, 'C': KeyF3, 'D': KeyF4, 'E': KeyF5, } func (ip *inputParser) scan() { for _, r := range ip.buf { ip.buf = ip.buf[1:] ip.escChar = 0 ip.keyTime = time.Now() if r >= 0xA0 { // 8-bit extended Unicode we just treat as such - this will swallow anything else queued up ip.state = istInit physical, _ := keyFromRune(r) ip.postKeyEx(KeyRune, string(r), ModNone, true, physical, 1) continue } else if r >= 0x80 { // ISO 2022 control chars ip.state = istEsc r -= 0x40 // we fall through so it will be treated as the 7-bit equivalent } switch ip.state { case istInit: switch r { case '\x1b': // escape.. pending ip.state = istEsc ip.escChar = 0 case '\t': ip.postKey(KeyTab, "", ModNone) case '\b', '\x7F': ip.postKey(KeyBackspace, "", ModNone) case '\r': ip.postKey(KeyEnter, "", ModNone) default: // Control keys - legacy handling if r == 0 { ip.postControlKey(r, ModNone) } else if r < ' ' { ip.postControlKey(r, ModNone) } else { physical, _ := keyFromRune(r) ip.postKeyEx(KeyRune, string(r), ModNone, true, physical, 1) } } case istEsc: switch r { case '[': ip.state = istCsi ip.csiInterm = nil ip.csiParams = nil ip.escChar = byte(r) case ']': ip.state = istOsc ip.strBuf = nil ip.discardString = false ip.escChar = byte(r) case 'N': ip.state = istSs2 // no known uses ip.strBuf = nil ip.escChar = byte(r) case 'O': ip.state = istSs3 ip.csiParams = nil ip.strBuf = nil ip.escChar = byte(r) case 'P': ip.state = istXda ip.csiParams = nil ip.strBuf = nil ip.discardString = false ip.escChar = byte(r) case 'X': ip.state = istSos ip.strBuf = nil ip.escChar = byte(r) case '^': ip.state = istPm ip.strBuf = nil ip.escChar = byte(r) case '_': ip.state = istApc ip.strBuf = nil ip.escChar = byte(r) case '\\': // string terminator reached, (orphaned?) ip.state = istInit case '\t': // Linux console only, does not conform to ECMA ip.state = istInit ip.postKey(KeyBacktab, "", ModNone) default: if r == '\x1b' { // leading ESC to capture alt ip.escaped = true ip.escChar = byte(r) } else { // treat as alt-key ... legacy emulators only (no CSI-u or other) ip.state = istInit mod := ModAlt if r < ' ' { mod |= ModCtrl r += 0x60 } physical, _ := keyFromRune(r) ip.postKeyEx(KeyRune, string(r), mod, true, physical, 1) } } case istCsi: // usual case for incoming keys // NB: rxvt uses terminating '$' which is not a legal CSI terminator, // for certain shifted key sequences. We special case this, and it's ok // because no other terminal seems to use this for CSI intermediates from // the terminal to the host (queries in the other direction can use it.) // However, this is only true if the first parameter does not have a "?", // because it *does* collide with DEC private mode queries otherwise. if r == '\x1b' { // Per ECMA-48 §5.3.1, ESC restarts the escape // sequence machine from any intermediate state. ip.state = istEsc ip.escChar = 0 } else if r >= 0x30 && r <= 0x3F { // parameter bytes ip.csiParams = append(ip.csiParams, byte(r)) } else if r == '$' && len(ip.csiParams) > 0 && ip.csiParams[0] != '?' { // rxvt non-standard ip.handleCsi(r, ip.csiParams, ip.csiInterm) } else if r >= 0x20 && r <= 0x2F { // intermediate bytes, rarely used ip.csiInterm = append(ip.csiInterm, byte(r)) } else if r >= 0x40 && r <= 0x7F { // final byte ip.handleCsi(r, ip.csiParams, ip.csiInterm) } else { // bad parse, just swallow it all ip.state = istInit } case istSs2: // No known uses for SS2 ip.state = istInit case istSs3: // typically application mode keys or older terminals ip.state = istInit // some SS3 sequences (old VTE) encode modifiers here just like CSI if r == '\x1b' { // Per ECMA-48 §5.3.1, ESC restarts the escape // sequence machine from any intermediate state. ip.state = istEsc ip.escChar = 0 } else if r >= 0x30 && r <= 0x3F { ip.csiParams = append(ip.csiParams, byte(r)) ip.state = istSs3 } else if k, ok := ss3Keys[r]; ok { // If there are no parameters, then it's simple without modifiers. // The options for parameters are "1;" , or ";modifiers" (empty // first parameter defaults to 1), or just . If a sequence has // parameters that do not match one of these forms, we just discard it. if len(ip.csiParams) == 0 { // simple SS3 case ip.postKey(k, "", ModNone) } else if parts := strings.Split(string(ip.csiParams), ";"); len(parts) >= 1 { // SS3 with modifier (old style). Note old terminfo would declare these as high // numbered function keys, but we encode as modified since that's how they are entered. if len(parts) >= 2 { if m, err := strconv.Atoi(parts[1]); err == nil && (parts[0] == "1" || parts[0] == "") { ip.postKey(k, "", calcModifier(m)) } } else if m, err := strconv.Atoi(parts[0]); err == nil { ip.postKey(k, "", calcModifier(m)) } } } case istPm, istApc, istSos, istDcs: // these we just eat switch r { case '\x1b': ip.strState = ip.state ip.state = istSt case '\x07': // bell - some send this instead of ST ip.state = istInit } case istXda: switch r { case '\x1b': ip.strState = ip.state ip.state = istSt case '\x07': if ip.discardString { ip.discardString = false ip.state = istInit } else { ip.handleXda(string(ip.strBuf)) } default: if !ip.discardString { ip.appendStringBytes(byte(r & 0x7f)) } } case istOsc: // not sure if used switch r { case '\x1b': ip.strState = ip.state ip.state = istSt case '\x07': if ip.discardString { ip.discardString = false ip.state = istInit } else { ip.handleOsc(string(ip.strBuf)) } default: if !ip.discardString { ip.appendStringBytes(byte(r & 0x7f)) } } case istSt: if r == '\\' || r == '\x07' { ip.state = istInit if ip.discardString { ip.discardString = false } else { switch ip.strState { case istOsc: ip.handleOsc(string(ip.strBuf)) case istXda: ip.handleXda(string(ip.strBuf)) case istPm, istApc, istSos, istDcs: ip.state = istInit } } } else { if !ip.discardString { ip.appendStringBytes('\x1b', byte(r)) } ip.state = ip.strState } case istLnx: // linux console does not follow ECMA if k, ok := linuxFKeys[r]; ok { ip.postKey(k, "", ModNone) } ip.state = istInit } } if ip.state != istInit && time.Since(ip.keyTime) > time.Millisecond*50 { if ip.state == istEsc { ip.postKey(KeyEscape, "", ModNone) } else if ec := ip.escChar; ec != 0 { ip.postKey(KeyRune, string(ec), ModAlt) } // if we take too long between bytes, reset the state machine. ip.state = istInit ip.discardString = false } } func (ip *inputParser) appendStringBytes(bs ...byte) { if ip.controlStringMax > 0 && len(ip.strBuf)+len(bs) > ip.controlStringMax { ip.strBuf = nil ip.discardString = true return } ip.strBuf = append(ip.strBuf, bs...) } func (ip *inputParser) handleOsc(str string) { ip.state = istInit if content, ok := strings.CutPrefix(str, "52;c;"); ok { decoded := make([]byte, base64.StdEncoding.DecodedLen(len(content))) if count, err := base64.StdEncoding.Decode(decoded, []byte(content)); err == nil { ip.post(NewEventClipboard(decoded[:count])) return } } } func (ip *inputParser) handleXda(str string) { ip.state = istInit if content, ok := strings.CutPrefix(str, ">|"); ok { // two approaches, one with version like (1.23) another with just spaces if name, vers, ok := strings.Cut(content, "("); ok && strings.HasSuffix(vers, ")") { name = strings.TrimSpace(name) vers = strings.TrimSpace(strings.TrimSuffix(vers, ")")) ip.post(&eventTermName{Name: name, Version: vers}) } else if name, vers, ok = strings.Cut(content, " "); ok { ip.post(&eventTermName{Name: name, Version: vers}) } } } func calcModifier(n int) ModMask { n-- m := ModNone if n&1 != 0 { m |= ModShift } if n&2 != 0 { m |= ModAlt } if n&4 != 0 { m |= ModCtrl } if n&8 != 0 { m |= ModMeta // kitty calls this Super } if n&16 != 0 { m |= ModHyper } if n&32 != 0 { m |= ModMeta // for now not separating from Super } // Not doing (kitty only): // caps_lock 0b1000000 (64) // num_lock 0b10000000 (128) return m } func calcWinModifier(n int, advanced bool) ModMask { m := ModNone if n&0x010 != 0 { m |= ModShift } if advanced { // Bits through 0x0100 match Win32 dwControlKeyState. 0x0040 and // 0x0080 are ScrollLock and CapsLock, not Meta. The 0x0200 and // 0x0400 bits are tcell extensions used by the WASM browser shim, // which has Meta keys but no native Win32 bit assignment for them. if n&0x0008 != 0 { m |= ModLCtrl } if n&0x0004 != 0 { m |= ModRCtrl } if n&0x0002 != 0 { m |= ModLAlt } if n&0x0001 != 0 { m |= ModRAlt } if n&0x0200 != 0 { m |= ModLMeta } if n&0x0400 != 0 { m |= ModRMeta } } else { if n&0x000c != 0 { m |= ModCtrl } if n&0x0003 != 0 { m |= ModAlt } } return m } func winModifierKey(vk int) (Key, ModMask, bool) { switch vk { case 0x10: return KeyShift, ModShift, true case 0xa0: return KeyShift, ModLShift, true case 0xa1: return KeyShift, ModRShift, true case 0x11: return KeyCtrl, ModCtrl, true case 0xa2: return KeyCtrl, ModLCtrl, true case 0xa3: return KeyCtrl, ModRCtrl, true case 0x12: return KeyAlt, ModAlt, true case 0xa4: return KeyAlt, ModLAlt, true case 0xa5: return KeyAlt, ModRAlt, true case 0x5b: return KeyMeta, ModLMeta, true case 0x5c: return KeyMeta, ModRMeta, true case 0x14: return KeyCapsLock, ModNone, true default: return 0, ModNone, false } } func kittyModifierKey(code int) ModMask { switch code { case 57441: return ModLShift case 57447: return ModRShift case 57442: return ModLCtrl case 57448: return ModRCtrl case 57443: return ModLAlt case 57449: return ModRAlt case 57444: return ModLMeta case 57450: return ModRMeta default: return ModNone } } func (ip *inputParser) handleMouse(mode rune, params []int) { // XTerm mouse events only report at most one button at a time, // which may include a wheel button. Wheel motion events are // reported as single impulses, while other button events are reported // as separate press & release events. if len(params) < 3 { return } btn := params[0] // Some terminals will report mouse coordinates outside the // screen, especially with click-drag events. Clip the coordinates // to the screen in that case. In pixel-reporting mode (CSI ?1016h) // the values are already pixels rather than cells, so skip the clip // and pass them through unchanged for the application to interpret. x := params[1] - 1 y := params[2] - 1 if !ip.pixelMouse { x = max(min(x, ip.cols-1), 0) y = max(min(y, ip.rows-1), 0) } button := ButtonNone mod := ModNone // Mouse wheel has bit 6 set, no release events. It should be noted // that wheel events are sometimes misdelivered as mouse button events // during a click-drag, so we debounce these, considering them to be // button press events unless we see an intervening release event. // This excludes motion (bit 5) and modifiers (bits 2, 3, 4) for now. switch btn & 0xC3 { case 0: button = Button1 case 1: button = Button3 // Note we prefer to treat right as button 2 case 2: button = Button2 // And the middle button as button 3 case 3: button = ButtonNone case 0x40: button = WheelUp case 0x41: button = WheelDown case 0x42: button = WheelLeft case 0x43: button = WheelRight case 0x80: button = Button4 case 0x81: button = Button5 case 0x82: button = Button6 case 0x83: button = Button7 } switch mode { case 'm': if (ip.btnsDown & button) == 0 { // a release without a corresponding press, so clear it button = ButtonNone } else { ip.btnsDown &^= button button = ip.btnsDown } case 'M': if btn&0x20 != 0 && button != ButtonNone && (ip.btnsDown&button) == 0 { // Ghostty may send out motion signals that indicate a button has // been pressed, even when the button is not actually pressed. // Do not create a synthetic button-down state from these packets. button = ip.btnsDown break } // record this press ip.btnsDown |= button // and use the full set so can see chords button = ip.btnsDown // mice wheel do not have release events ip.btnsDown &^= (WheelDown | WheelUp | WheelLeft | WheelRight) } if btn&0x4 != 0 { mod |= ModShift } if btn&0x8 != 0 { mod |= ModAlt } if btn&0x10 != 0 { mod |= ModCtrl } ip.post(NewEventMouse(x, y, button, mod)) } func (ip *inputParser) handleWinKey(P []int) { // win32-input-mode // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ // Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'. // Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'. // Uc: the decimal value of UnicodeChar - for example, NUL is "0", LF is // "10", the character 'A' is "65". If omitted, defaults to '0'. // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'. // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'. // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. // // Note that some 3rd party terminal emulators (not Terminal) suffer from a bug // where other events, such as mouse events, are doubly encoded, using Vk 0 // for each character. (So a CSI-M sequence is encoded as a series of CSI-_ // sequences.) We consider this a bug in those terminal emulators -- Windows 11 // Terminal does not suffer this brain damage. (We've observed this with both Alacritty // and WezTerm.) for len(P) < 6 { P = append(P, 0) // ensure sufficient length } if P[3] == 0 && !ip.advanced { // key up event ignore ignore return } // these terminals never send ambiguous escapes ip.escaped = false if P[0] == 0 && P[1] == 0 { // only ASCII in win32-input-mode if b, ok := asciiByteFromInt(P[2]); ok { if ip.nested == nil { ip.nested = &inputParser{ evch: ip.evch, rows: ip.rows, cols: ip.cols, advanced: ip.advanced, pixelMouse: ip.pixelMouse, controlStringMax: ip.controlStringMax, } } ip.nested.ScanUTF8([]byte{b}) return } } key := KeyRune chr := rune(P[2]) mod := ModNone rpt := max(1, P[5]) decoded := false if k1, ok := winKeys[P[0]]; ok { chr = 0 key = k1 decoded = true } else if ip.advanced { if k1, mod1, ok := winModifierKey(P[0]); ok { key = k1 mod = mod1 chr = 0 decoded = true } } if decoded { // Already decoded. } else if chr == 0 && P[0] >= 0x30 && P[0] <= 0x39 { chr = rune(P[0]) } else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a { if ip.advanced { key = KeyRune chr = rune(P[0] + 0x20) } else { var ok bool if key, ok = keyFromInt(P[0]); !ok { return } chr = 0 } } else if chr >= 0xD800 && chr <= 0xDBFF { // high surrogate pair if ip.surrogate != 0 { ip.postKeyEx(KeyRune, string(utf8.RuneError), mod, P[3] != 0, 0, rpt) } ip.surrogate = chr return } else if chr >= 0xDC00 && chr <= 0xDFFF { // low surrogate pair if ip.surrogate == 0 { chr = utf8.RuneError } else { chr = utf16.DecodeRune(ip.surrogate, chr) } } else if ip.surrogate != 0 { ip.postKeyEx(KeyRune, string(utf8.RuneError), mod, P[3] != 0, 0, rpt) } else if _, _, ok := winModifierKey(P[0]); ok { // Lone modifier releases are ignored unless advanced mode is enabled. ip.surrogate = 0 return } ip.surrogate = 0 mod |= calcWinModifier(P[4], ip.advanced) if key == KeyRune && chr > ' ' && mod == ModShift && !ip.advanced { // filter out lone shift for printable chars mod = ModNone } if chr != 0 && mod&(ModCtrl|ModAlt) == ModCtrl|ModAlt { // Filter out ctrl+alt (it means AltGr) mod = ModNone } physical := key if key == KeyRune && chr != 0 { physical, _ = keyFromRune(chr) if ip.advanced && P[0] >= 0x41 && P[0] <= 0x5a { physical, _ = keyFromInt(P[0] + 0x20) } } if key != KeyRune { ip.postKeyEx(key, "", mod, P[3] != 0, physical, rpt) } else if chr != 0 { ip.postKeyEx(KeyRune, string(chr), mod, P[3] != 0, physical, rpt) } } func (ip *inputParser) handlePrimaryDA(params []int) { if len(params) < 1 { return } evDA := &eventPrimaryAttributes{Class: params[0]} params = params[1:] if evDA.Class >= 60 { for _, v := range params { switch v { case 3: evDA.ReGIS = true case 4: evDA.Sixel = true case 9: evDA.National = true case 12: evDA.SerboCroation = true case 22: evDA.Color = true case 23: evDA.Greek = true case 24: evDA.Turkish = true case 42: evDA.Latin2 = true case 52: evDA.Clipboard = true } } } ip.post(evDA) } func (ip *inputParser) handlePrivateModeResponse(params []int) { for len(params) < 2 { params = append(params, 0) } if params[1] >= 0 && params[1] <= 4 { ev := &eventPrivateMode{ Mode: vt.PrivateMode(params[0]), Status: vt.ModeStatus(params[1]), } ip.post(ev) } } func (ip *inputParser) handleKittyMode(params []int) { if len(params) == 1 && params[0] >= 0 && params[0] < 32 { ev := &eventKittyKbdMode{ Mode: KittyKbdMode(params[0] & 0xffff), } ip.post(ev) } } func (ip *inputParser) handleXTermMode(params []int) { if len(params) >= 1 && params[0] == 4 { if len(params) == 1 { params = append(params, 0) } ev := &eventXTermKbdMode{ Mode: XtermKbdMode(params[1] & 0x3), } ip.post(ev) } } func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte) { // reset state ip.state = istInit var P []int hasLT := false hasQM := false hasGT := false pstr := string(params) // extract numeric parameters if strings.HasPrefix(pstr, "<") { hasLT = true pstr = pstr[1:] } else if strings.HasPrefix(pstr, "?") { hasQM = true pstr = pstr[1:] } else if strings.HasPrefix(pstr, ">") { hasGT = true pstr = pstr[1:] } pressed := true repeat := 1 physical := Key(0) if pstr != "" && pstr[0] >= '0' && pstr[0] <= '9' { var PSubs [][]int parts := strings.Split(pstr, ";") for i := range parts { subparts := strings.Split(parts[i], ":") if subparts[0] != "" { if n, e := strconv.ParseInt(subparts[0], 10, 32); e == nil { P = append(P, int(n)) } else { P = append(P, 0) } } else { P = append(P, 0) } subs := []int{} for _, sub := range subparts[1:] { if sub != "" { if n, e := strconv.ParseInt(sub, 10, 32); e == nil { subs = append(subs, int(n)) } } else { subs = append(subs, 0) } } PSubs = append(PSubs, subs) } if len(PSubs) > 1 && len(PSubs[1]) > 0 { switch PSubs[1][0] { case 2: repeat = 2 case 3: pressed = false } } if len(PSubs) > 0 && len(PSubs[0]) > 0 { base := PSubs[0][0] if baseKey, ok := csiUKeys[base]; ok { physical = baseKey.Key if physical == KeyRune && baseKey.Rune != 0 { physical, _ = keyFromRune(baseKey.Rune) } } else if base != 0 { physical, _ = keyFromInt(base) } } } var P0 int if len(P) > 0 { P0 = P[0] } if hasLT && len(intermediate) == 0 { switch mode { case 'm', 'M': // mouse event, we only do SGR tracking ip.handleMouse(mode, P) } return } if hasQM { switch mode { case 'c': if len(intermediate) == 0 { ip.handlePrimaryDA(P) } case 'y': if string(intermediate) == "$" { ip.handlePrivateModeResponse(P) } case 'u': if len(intermediate) == 0 { ip.handleKittyMode(P) } } return } if hasGT { switch mode { case 'm': if len(intermediate) == 0 { ip.handleXTermMode(P) } } return } if len(intermediate) != 0 { // we don't know what to do with these for now return } switch mode { case 'I': // focus in ip.post(NewEventFocus(true)) return case 'O': // focus out ip.post(NewEventFocus(false)) return case '[': // linux console F-key - CSI-[ modifies next key ip.state = istLnx return case 'u': // CSI-u kitty keyboard protocol, is unambiguous if len(P) > 0 { mod := ModNone key := KeyRune chr := rune(0) if k1, ok := csiUKeys[P0]; ok { key = k1.Key chr = k1.Rune } else { chr = rune(P0) } if len(P) > 1 { mod = calcModifier(P[1]) } if mod1 := kittyModifierKey(P0); mod1 != ModNone { mod |= mod1 } if key != KeyRune { ip.postKeyEx(key, "", mod, pressed, physical, repeat) } else if chr != 0 { ip.postKeyEx(KeyRune, string(chr), mod, pressed, physical, repeat) } return } case '_': if len(P) > 0 { ip.handleWinKey(P) return } case 't': if len(P) < 1 { break } switch P[0] { case 8: if len(P) > 2 { // window size report h := P[1] w := P[2] if h != ip.rows || w != ip.cols { ip.SetSize(w, h) } return } case 48: if len(P) > 2 { // window resize report ip.post(NewEventResize(P[2], P[1])) return } } case '~': if len(P) >= 2 { mod := calcModifier(P[1]) if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok { ip.postKeyEx(ks.Key, "", mod, pressed, 0, repeat) return } if P0 == 27 && len(P) > 2 && P[2] > 0 && P[2] <= utf8.MaxRune { if P[2] < ' ' || P[2] == 0x7F { if key, ok := keyFromInt(P[2]); ok { ip.postKey(key, "", mod) } } else { physical, _ := keyFromRune(rune(P[2])) ip.postKeyEx(KeyRune, string(rune(P[2])), mod, true, physical, 1) } return } } } if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok { if mode == '~' && len(P) > 1 && ks.Mod == ModNone { // apply modifiers if present ks.Mod = calcModifier(P[1]) } else if mode == 'P' && os.Getenv("TERM") == "aixterm" { ks.Key = KeyDelete // aixterm hack - conflicts with kitty protocol } ip.postKey(ks.Key, "", ks.Mod) return } // this might have been an SS3 style key with modifiers applied if k, ok := ss3Keys[mode]; ok && P0 == 1 && len(P) > 1 { ip.postKeyEx(k, "", calcModifier(P[1]), pressed, 0, repeat) return } // if we got here we just swallow the unknown sequence } func (ip *inputParser) ScanUTF8(b []byte) { ip.l.Lock() defer ip.l.Unlock() ip.utfBuf = append(ip.utfBuf, b...) for len(ip.utfBuf) > 0 { // fast path, basic ascii, also includes ISO2022 8-bit controls if ip.utfBuf[0] < 0xA0 { ip.buf = append(ip.buf, rune(ip.utfBuf[0])) ip.utfBuf = ip.utfBuf[1:] } else { r, utfLen := utf8.DecodeRune(ip.utfBuf) if r == utf8.RuneError { // discard the leading byte as bad, // hopefully it will recover. utfLen = 1 } else { ip.buf = append(ip.buf, r) } ip.utfBuf = ip.utfBuf[utfLen:] } } ip.scan() } // Scan scans the existing input, but does not take new content. // This is typically called after a delay when Waiting() is true. func (ip *inputParser) Scan() { ip.l.Lock() ip.scan() ip.l.Unlock() } // Private events between input and tscreen. // eventPrimaryAttributes is for primary device attributes -- this should be // the last event returned during initial handshaking type eventPrimaryAttributes struct { EventTime Class int // Terminal class, 1 is vt100, vt101, 6 is vt102, > 60 for vt200 and up ReGIS bool // Terminal supports ReGIS graphics (DA 3) Sixel bool // Terminal supports Sixel graphics (DA 4) National bool // Terminal supports national replacement character sets (DA 9) SerboCroation bool // Serbo-Croatian(DA 12) Color bool // Terminal supports color (DA 22) Greek bool // Greek (DA 23) Turkish bool // Turkish (DA 24) Latin2 bool // ISO Latin-2 (DA 42) Clipboard bool // OSC 52 support (DA 52) } // eventTermName is for extended attributes type eventTermName struct { EventTime Name string Version string } type eventPrivateMode struct { EventTime Mode vt.PrivateMode // numeric mode e.g. 7 for auto-margin, 1006 for SGR mouse reports, etc Status vt.ModeStatus // value of status } type KittyKbdMode uint16 const ( KittyKbdModeOff = KittyKbdMode(0) // Disable Kitty keyboard mode KittyKbdModeBase = KittyKbdMode(1) // Enable disambiguated keys KittyKbdModeEvents = KittyKbdMode(2) // Report event types (e.g. key release) KittyKbdModeAlternate = KittyKbdMode(4) // Report alternate keys KittyKbdModeAll = KittyKbdMode(8) // Report all keys using kitty keyboard protocol KittyKbdModeText = KittyKbdMode(16) // Report associated text ) type eventKittyKbdMode struct { EventTime Mode KittyKbdMode } type XtermKbdMode uint16 const ( XtermKbdModeOff = XtermKbdMode(0) // Disabled XtermKbdModeBase = XtermKbdMode(1) // Enabled except for ones with legacy behavior XtermKbdModeExt = XtermKbdMode(2) // Enabled for all modified keys XtermKbdModeAll = XtermKbdMode(3) // Send all keys (including unmodified) ) type eventXTermKbdMode struct { EventTime Mode XtermKbdMode } golang-github-gdamore-tcell.v3-3.4.0+dfsg/input_test.go000066400000000000000000001330731520475227200227720ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !js && !wasm // +build !js,!wasm package tcell import ( "bytes" "fmt" "testing" "time" "github.com/gdamore/tcell/v3/vt" ) // TestInputNullByte tests that null byte (0x00) is correctly handled // as Ctrl+Space per the fix in the scan() function. func TestInputNullByte(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Send null byte ip.ScanUTF8([]byte{0x00}) // Wait briefly for processing select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune { t.Errorf("Expected KeyRune for null byte, got %v", kev.Key()) } if kev.Str() != " " { t.Errorf("Expected space character ' ' for null byte, got %q", kev.Str()) } if kev.Modifiers() != ModCtrl { t.Errorf("Expected ModCtrl for null byte, got %v", kev.Modifiers()) } } else { t.Errorf("Expected EventKey, got %T", ev) } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout waiting for null byte event") } } // TestInputControlKeys tests control key handling for bytes 1-31. // Note: NewEventKey converts control characters to KeyCtrlA-Z for 0x01-0x1A, // and KeyRune for 0x1C-0x1F with the character and ModCtrl. func TestInputControlKeys(t *testing.T) { tests := []struct { name string input byte key Key str string mod ModMask }{ {"Ctrl+A", 0x01, KeyCtrlA, "", ModCtrl}, {"Ctrl+B", 0x02, KeyCtrlB, "", ModCtrl}, {"Ctrl+C", 0x03, KeyCtrlC, "", ModCtrl}, {"Ctrl+D", 0x04, KeyCtrlD, "", ModCtrl}, {"Ctrl+E", 0x05, KeyCtrlE, "", ModCtrl}, {"Ctrl+F", 0x06, KeyCtrlF, "", ModCtrl}, {"Ctrl+G", 0x07, KeyCtrlG, "", ModCtrl}, {"Ctrl+H", 0x08, KeyBackspace, "", ModNone}, // BS is special {"Ctrl+I", 0x09, KeyTab, "", ModNone}, // Tab is special {"Ctrl+J", 0x0A, KeyCtrlJ, "", ModCtrl}, {"Ctrl+K", 0x0B, KeyCtrlK, "", ModCtrl}, {"Ctrl+L", 0x0C, KeyCtrlL, "", ModCtrl}, {"Ctrl+M", 0x0D, KeyEnter, "", ModNone}, // CR is Enter {"Ctrl+N", 0x0E, KeyCtrlN, "", ModCtrl}, {"Ctrl+O", 0x0F, KeyCtrlO, "", ModCtrl}, {"Ctrl+P", 0x10, KeyCtrlP, "", ModCtrl}, {"Ctrl+Q", 0x11, KeyCtrlQ, "", ModCtrl}, {"Ctrl+R", 0x12, KeyCtrlR, "", ModCtrl}, {"Ctrl+S", 0x13, KeyCtrlS, "", ModCtrl}, {"Ctrl+T", 0x14, KeyCtrlT, "", ModCtrl}, {"Ctrl+U", 0x15, KeyCtrlU, "", ModCtrl}, {"Ctrl+V", 0x16, KeyCtrlV, "", ModCtrl}, {"Ctrl+W", 0x17, KeyCtrlW, "", ModCtrl}, {"Ctrl+X", 0x18, KeyCtrlX, "", ModCtrl}, {"Ctrl+Y", 0x19, KeyCtrlY, "", ModCtrl}, {"Ctrl+Z", 0x1A, KeyCtrlZ, "", ModCtrl}, {"Ctrl+[", 0x1B, KeyEscape, "", ModNone}, // ESC is special {"Ctrl+\\", 0x1C, KeyRune, "\\", ModCtrl}, // becomes KeyRune with string {"Ctrl+]", 0x1D, KeyRune, "]", ModCtrl}, // becomes KeyRune with string {"Ctrl+^", 0x1E, KeyRune, "^", ModCtrl}, // becomes KeyRune with string {"Ctrl+_", 0x1F, KeyRune, "_", ModCtrl}, // becomes KeyRune with string } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Skip ESC (0x1B) as it has special timeout handling if tt.input == 0x1B { t.Skip("ESC byte triggers escape sequence state machine with timeout") return } ip.ScanUTF8([]byte{tt.input}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != tt.key { t.Errorf("Expected key %v, got %v", tt.key, kev.Key()) } if kev.Str() != tt.str { t.Errorf("Expected string %q, got %q", tt.str, kev.Str()) } if kev.Modifiers() != tt.mod { t.Errorf("Expected modifiers %v, got %v", tt.mod, kev.Modifiers()) } } else { t.Errorf("Expected EventKey, got %T", ev) } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout waiting for control key event") } }) } } // TestInputNullVsOtherControlChars tests the boundary between // null byte and other control characters. func TestInputNullVsOtherControlChars(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Test null (0x00) - should be KeyRune with Ctrl+Space ip.ScanUTF8([]byte{0x00}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune { t.Errorf("Null byte: expected KeyRune, got %v", kev.Key()) } if kev.Str() != " " { t.Errorf("Null byte: expected ' ', got %q", kev.Str()) } if kev.Modifiers() != ModCtrl { t.Errorf("Null byte: expected ModCtrl, got %v", kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout for null byte") } // Test 0x01 (Ctrl+A) - should be KeyCtrlA ip.ScanUTF8([]byte{0x01}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyCtrlA { t.Errorf("Byte 0x01: expected KeyCtrlA, got %v", kev.Key()) } if kev.Modifiers() != ModCtrl { t.Errorf("Byte 0x01: expected ModCtrl, got %v", kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout for 0x01") } } // TestInputPrintableCharacters tests that printable characters // are handled correctly without control modifiers. func TestInputPrintableCharacters(t *testing.T) { tests := []struct { name string input string expected string mod ModMask }{ {"Space", " ", " ", ModNone}, {"ExclamationMark", "!", "!", ModNone}, {"LetterA", "A", "A", ModNone}, {"LetterZ", "Z", "Z", ModNone}, {"Lowera", "a", "a", ModNone}, {"Lowerz", "z", "z", ModNone}, {"Digit0", "0", "0", ModNone}, {"Digit9", "9", "9", ModNone}, {"At", "@", "@", ModNone}, {"Tilde", "~", "~", ModNone}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8([]byte(tt.input)) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune { t.Errorf("Expected KeyRune, got %v", kev.Key()) } if kev.Str() != tt.expected { t.Errorf("Expected %q, got %q", tt.expected, kev.Str()) } if kev.Modifiers() != tt.mod { t.Errorf("Expected modifiers %v, got %v", tt.mod, kev.Modifiers()) } } else { t.Errorf("Expected EventKey, got %T", ev) } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout waiting for character event") } }) } } // TestInputSpecialKeys tests special key handling (tab, backspace, enter). func TestInputSpecialKeys(t *testing.T) { tests := []struct { name string input byte expected Key mod ModMask }{ {"Tab", '\t', KeyTab, ModNone}, {"Backspace_BS", '\b', KeyBackspace, ModNone}, {"Backspace_DEL", 0x7F, KeyBackspace, ModNone}, {"Enter_LF", '\n', KeyCtrlJ, ModCtrl}, {"Enter_CR", '\r', KeyEnter, ModNone}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8([]byte{tt.input}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != tt.expected { t.Errorf("Expected key %v, got %v", tt.expected, kev.Key()) } if kev.Modifiers() != tt.mod { t.Errorf("Expected modifiers %v, got %v", tt.mod, kev.Modifiers()) } } else { t.Errorf("Expected EventKey, got %T", ev) } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout waiting for special key event") } }) } } // TestInputSequentialInput tests handling multiple inputs in sequence. func TestInputSequentialInput(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Send: null, Ctrl+A, 'B', space inputs := []byte{0x00, 0x01, 'B', ' '} expected := []struct { key Key str string mod ModMask }{ {KeyRune, " ", ModCtrl}, // null -> Ctrl+Space {KeyCtrlA, "", ModCtrl}, // 0x01 -> KeyCtrlA with ModCtrl {KeyRune, "B", ModNone}, // 'B' {KeyRune, " ", ModNone}, // space } for i, b := range inputs { ip.ScanUTF8([]byte{b}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != expected[i].key { t.Errorf("Input %d: expected key %v, got %v", i, expected[i].key, kev.Key()) } if kev.Str() != expected[i].str { t.Errorf("Input %d: expected %q, got %q", i, expected[i].str, kev.Str()) } if kev.Modifiers() != expected[i].mod { t.Errorf("Input %d: expected modifiers %v, got %v", i, expected[i].mod, kev.Modifiers()) } } else { t.Errorf("Input %d: expected EventKey, got %T", i, ev) } case <-time.After(100 * time.Millisecond): t.Fatalf("Timeout waiting for event %d", i) } } } // TestInputUTF8Characters tests UTF-8 multibyte character handling. func TestInputUTF8Characters(t *testing.T) { tests := []struct { name string input []byte expected string }{ {"Euro", []byte("€"), "€"}, {"CJK", []byte("中"), "中"}, {"Emoji", []byte("😀"), "😀"}, {"Cyrillic", []byte("Ж"), "Ж"}, {"Arabic", []byte("ع"), "ع"}, {"SMP", []byte("🝁"), "🝁"}, // needs full 4 character UTF-8 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8(tt.input) time.Sleep(time.Millisecond * 100) ip.Scan() select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Str() != tt.expected { t.Errorf("Expected %q, got %q", tt.expected, kev.Str()) } if kev.Modifiers() != ModNone { t.Errorf("Expected ModNone, got %v", kev.Modifiers()) } } else { t.Errorf("Expected EventKey, got %T", ev) } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout waiting for UTF-8 character event") } }) } } // TestInputEdgeCases tests edge cases and boundary conditions. func TestInputEdgeCases(t *testing.T) { t.Run("EmptyInput", func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8([]byte{}) time.Sleep(time.Millisecond * 100) ip.Scan() select { case ev := <-evch: t.Errorf("Expected no event for empty input, got %T", ev) case <-time.After(50 * time.Millisecond): // Success - no event generated } }) t.Run("MultipleNullBytes", func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8([]byte{0x00, 0x00, 0x00}) for i := range 3 { select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune || kev.Str() != " " || kev.Modifiers() != ModCtrl { t.Errorf("Null byte %d: expected KeyRune with Ctrl+Space, got key=%v str=%q mod=%v", i, kev.Key(), kev.Str(), kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatalf("Timeout waiting for null byte %d", i) } } }) t.Run("BoundaryByte0x1F", func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // 0x1F is the last control character before space (0x20) // NewEventKey transforms it to KeyRune with "_" and ModCtrl ip.ScanUTF8([]byte{0x1F}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune { t.Errorf("Expected KeyRune for 0x1F, got %v", kev.Key()) } if kev.Str() != "_" { t.Errorf("Expected '_' for 0x1F, got %q", kev.Str()) } if kev.Modifiers() != ModCtrl { t.Errorf("Expected ModCtrl for 0x1F, got %v", kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout for 0x1F") } }) t.Run("BoundaryByte0x20", func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // 0x20 is space - first printable character ip.ScanUTF8([]byte{0x20}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune { t.Errorf("Expected KeyRune for 0x20, got %v", kev.Key()) } if kev.Str() != " " { t.Errorf("Expected ' ' for 0x20, got %q", kev.Str()) } if kev.Modifiers() != ModNone { t.Errorf("Expected ModNone for 0x20, got %v", kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout for 0x20") } }) } // TestInputConcurrentAccess tests that the parser is safe // for concurrent access (it uses a mutex internally). func TestInputConcurrentAccess(t *testing.T) { evch := make(chan Event, 100) ip := newInputParser(evch) done := make(chan bool) // Goroutine 1: Send null bytes go func() { for range 10 { ip.ScanUTF8([]byte{0x00}) time.Sleep(time.Millisecond) } done <- true }() // Goroutine 2: Send regular characters go func() { for range 10 { ip.ScanUTF8([]byte{'A'}) time.Sleep(time.Millisecond) } done <- true }() // Wait for both goroutines <-done <-done // Verify we got events (exact order not guaranteed) eventCount := 0 timeout := time.After(200 * time.Millisecond) for eventCount < 20 { select { case <-evch: eventCount++ case <-timeout: t.Fatalf("Expected 20 events, got %d", eventCount) } } } // TestInputStateTransitions tests that the null byte fix doesn't // interfere with state machine transitions. func TestInputStateTransitions(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Send null in initial state ip.ScanUTF8([]byte{0x00}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune || kev.Str() != " " || kev.Modifiers() != ModCtrl { t.Errorf("Expected KeyRune with Ctrl+Space, got key=%v str=%q mod=%v", kev.Key(), kev.Str(), kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout") } // Verify state machine returns to initial state by sending a normal char ip.ScanUTF8([]byte{'X'}) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != KeyRune || kev.Str() != "X" || kev.Modifiers() != ModNone { t.Errorf("Expected KeyRune 'X' with ModNone after null, got key=%v str=%q mod=%v", kev.Key(), kev.Str(), kev.Modifiers()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout") } } // TestSpecialKeys tests that special keys (F-keys, home, del, etc. work as expected) func TestSpecialKeys(t *testing.T) { tests := []struct { name string input []byte expectedKey Key expectedMod ModMask expectedStr string }{ {"Esc", []byte{'\x1b'}, KeyEscape, ModNone, ""}, {"Esc-Esc", []byte{'\x1b', '\x1b'}, KeyEscape, ModAlt, ""}, {"Esc-Y", []byte{'\x1b', 'Y'}, KeyRune, ModAlt, "Y"}, {"Esc-Ctrl-B", []byte{'\x1b', '\x02'}, KeyRune, ModAlt | ModCtrl, "b"}, {"Esc-[", []byte{'\x1b', '['}, KeyRune, ModAlt, "["}, {"Tab", []byte{'\t'}, KeyTab, ModNone, ""}, {"NL", []byte{'\n'}, KeyCtrlJ, ModCtrl, ""}, {"CR", []byte{'\r'}, KeyEnter, ModNone, ""}, {"Backspace", []byte{'\b'}, KeyBackspace, ModNone, ""}, {"Delete", []byte{'\x7f'}, KeyBackspace, ModNone, ""}, {"CSI-A", []byte{'\x1b', '[', 'A'}, KeyUp, ModNone, ""}, {"CSI-B", []byte{'\x1b', '[', 'B'}, KeyDown, ModNone, ""}, {"CSI-C", []byte{'\x1b', '[', 'C'}, KeyRight, ModNone, ""}, {"CSI-D", []byte{'\x1b', '[', 'D'}, KeyLeft, ModNone, ""}, {"CSI-E", []byte{'\x1b', '[', 'E'}, KeyClear, ModNone, ""}, {"CSI-F", []byte{'\x1b', '[', 'F'}, KeyEnd, ModNone, ""}, {"CSI-H", []byte{'\x1b', '[', 'H'}, KeyHome, ModNone, ""}, {"CSI-L", []byte{'\x1b', '[', 'L'}, KeyInsert, ModNone, ""}, {"CSI-P", []byte{'\x1b', '[', 'P'}, KeyF1, ModNone, ""}, {"CSI-Q", []byte{'\x1b', '[', 'Q'}, KeyF2, ModNone, ""}, {"CSI-S", []byte{'\x1b', '[', 'S'}, KeyF4, ModNone, ""}, {"CSI-Z", []byte{'\x1b', '[', 'Z'}, KeyBacktab, ModNone, ""}, {"CSI-a", []byte{'\x1b', '[', 'a'}, KeyUp, ModShift, ""}, {"CSI-b", []byte{'\x1b', '[', 'b'}, KeyDown, ModShift, ""}, {"CSI-c", []byte{'\x1b', '[', 'c'}, KeyRight, ModShift, ""}, {"CSI-d", []byte{'\x1b', '[', 'd'}, KeyLeft, ModShift, ""}, {"CSI-15~", []byte{'\x1b', '[', '1', '5', '~'}, KeyF5, ModNone, ""}, {"CSI-17~", []byte{'\x1b', '[', '1', '7', '~'}, KeyF6, ModNone, ""}, {"CSI-18~", []byte{'\x1b', '[', '1', '8', '~'}, KeyF7, ModNone, ""}, {"CSI-19~", []byte{'\x1b', '[', '1', '9', '~'}, KeyF8, ModNone, ""}, {"CSI-20~", []byte{'\x1b', '[', '2', '0', '~'}, KeyF9, ModNone, ""}, {"CSI-21~", []byte{'\x1b', '[', '2', '1', '~'}, KeyF10, ModNone, ""}, {"CSI-23~", []byte{'\x1b', '[', '2', '3', '~'}, KeyF11, ModNone, ""}, {"CSI-24~", []byte{'\x1b', '[', '2', '4', '~'}, KeyF12, ModNone, ""}, {"CSI-1-$", []byte{'\x1b', '[', '1', '$'}, KeyHome, ModShift, ""}, // rxvt bs {"SS3-F1", []byte{'\x1b', 'O', 'P'}, KeyF1, ModNone, ""}, {"SS3-F2", []byte{'\x1b', 'O', 'Q'}, KeyF2, ModNone, ""}, {"SS3-F3", []byte{'\x1b', 'O', 'R'}, KeyF3, ModNone, ""}, {"SS3-F4", []byte{'\x1b', 'O', 'S'}, KeyF4, ModNone, ""}, {"SS3-F4-Shift", []byte{'\x1b', 'O', '1', ';', '2', 'S'}, KeyF4, ModShift, ""}, {"SS3-F4-Ctrl", []byte{'\x1b', 'O', '1', ';', '5', 'S'}, KeyF4, ModCtrl, ""}, {"SS3-F4-Ctrl-Short", []byte{'\x1b', 'O', '5', 'S'}, KeyF4, ModCtrl, ""}, {"SS3-F4-Ctrl-Shift", []byte{'\x1b', 'O', ';', '6', 'S'}, KeyF4, ModCtrl | ModShift, ""}, {"SS3-F2-Meta", []byte{'\x1b', 'O', ';', '9', 'Q'}, KeyF2, ModMeta, ""}, {"CSI-F2-Alt", []byte{'\x1b', '[', '1', ';', '3', 'Q'}, KeyF2, ModAlt, ""}, {"CSI-F2-Hyper", []byte{'\x1b', '[', '1', ';', '1', '7', 'Q'}, KeyF2, ModHyper, ""}, {"CSI-F2-Super", []byte{'\x1b', '[', '1', ';', '3', '3', 'Q'}, KeyF2, ModMeta, ""}, {"Ctrl-Home", []byte{'\x1b', '[', '1', ';', '5', '~'}, KeyHome, ModCtrl, ""}, {"SS3-Home", []byte{'\x1b', 'O', 'H'}, KeyHome, ModNone, ""}, {"SS3-Clear", []byte{'\x1b', 'O', 'E'}, KeyClear, ModNone, ""}, {"ESC-Tab", []byte{'\x1b', '\t'}, KeyBacktab, ModNone, ""}, // linux console special {"Linux-F1", []byte{'\x1b', '[', '[', 'A'}, KeyF1, ModNone, ""}, {"Linux-F2", []byte{'\x1b', '[', '[', 'B'}, KeyF2, ModNone, ""}, {"Linux-F3", []byte{'\x1b', '[', '[', 'C'}, KeyF3, ModNone, ""}, {"Linux-F4", []byte{'\x1b', '[', '[', 'D'}, KeyF4, ModNone, ""}, {"Linux-F5", []byte{'\x1b', '[', '[', 'E'}, KeyF5, ModNone, ""}, {"XTerm-Alt-Tab", []byte{'\x1b', '[', '2', '7', ';', '3', ';', '9', '~'}, KeyTab, ModAlt, ""}, // modifyOtherKeys == 1 {"Alt-F7", []byte{'\x1b', '[', '1', '8', ';', '3', '~'}, KeyF7, ModAlt, ""}, {"XTerm-Shift-Tab", []byte{'\x1b', '[', '2', '7', ';', '2', ';', '9', '~'}, KeyBacktab, ModNone, ""}, // modifyOtherKeys == 2 {"XTerm-Space", []byte{'\x1b', '[', '2', '7', ';', '1', ';', '3', '2', '~'}, KeyRune, ModNone, " "}, // modifyOtherKeys == 3 {"XTerm-Alt-CJK", []byte{'\x1b', '[', '2', '7', ';', '3', ';', '2', '0', '3', '2', '0', '~'}, KeyRune, ModAlt, "你"}, {"Kitty-Esc", []byte{'\x1b', '[', '2', '7', 'u'}, KeyEsc, ModNone, ""}, {"Kitty-Control-I", []byte{'\x1b', '[', '1', '0', '5', ';', '5', 'u'}, 'I', ModCtrl, ""}, {"Win-Shift-A", []byte{'\x1b', '[', '6', '5', ';', '0', ';', '6', '5', ';', '1', ';', '1', '6', '_'}, KeyRune, ModNone, "A"}, {"Win-Ctrl-1", []byte{'\x1b', '[', '4', '9', ';', '0', ';', '4', '9', ';', '1', ';', '8', '_'}, KeyRune, ModCtrl, "1"}, {"Win-Ctrl-A", []byte{'\x1b', '[', '6', '5', ';', '0', ';', '1', ';', '1', ';', '8', '_'}, KeyCtrlA, ModCtrl, ""}, {"Win-Ctrl-Up", []byte{'\x1b', '[', '3', '8', ';', '0', ';', '0', ';', '1', ';', '8', '_'}, KeyUp, ModCtrl, ""}, {"Win-Ctrl-Up-2", []byte{'\x1b', '[', '3', '8', ';', '0', ';', '0', ';', '1', ';', '4', '_'}, KeyUp, ModCtrl, ""}, {"Win-Alt-F1", []byte{'\x1b', '[', '1', '1', '2', ';', '0', ';', '0', ';', '1', ';', '1', '_'}, KeyF1, ModAlt, ""}, {"Win-Alt-F1-2", []byte{'\x1b', '[', '1', '1', '2', ';', '0', ';', '0', ';', '1', ';', '2', '_'}, KeyF1, ModAlt, ""}, {"Win-AltGr-E", []byte{'\x1b', '[', '6', '9', ';', '0', ';', '6', '9', ';', '1', ';', '5', '_'}, KeyRune, ModNone, "E"}, {"Win-Ignore-Release", []byte{'\x1b', '[', '6', '5', ';', '0', ';', '6', '5', ';', '0', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"}, {"Win-Mod-Ignore-Shift", []byte{'\x1b', '[', '1', '6', ';', '0', ';', '1', '1', ';', '1', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"}, {"Win-Mod-Ignore-Ctrl", []byte{'\x1b', '[', '1', '7', ';', '0', ';', '1', '3', ';', '1', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"}, {"Win-Mod-Ignore-Alt", []byte{'\x1b', '[', '1', '8', ';', '0', ';', '1', '4', ';', '1', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"}, {"Win-Surrogates", []byte{ '\x1b', '[', '0', ';', '0', ';', '5', '5', '3', '5', '6', ';', '1', ';', '0', ';', '0', '_', '\x1b', '[', '0', ';', '0', ';', '5', '7', '2', '5', '6', ';', '1', ';', '0', ';', '0', '_', }, KeyRune, ModNone, "🎨"}, {"Win-Nested-Shift-B", []byte{ '\x1b', '[', '0', ';', '0', ';', '2', '7', ';', '1', ';', '0', ';', '0', '_', // ESC '\x1b', '[', '0', ';', '0', ';', '9', '1', ';', '1', ';', '0', ';', '0', '_', // [ '\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6 '\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6 '\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ; '\x1b', '[', '0', ';', '0', ';', '4', '8', ';', '1', ';', '0', ';', '0', '_', // 0 '\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ; '\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6 '\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6 '\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ; '\x1b', '[', '0', ';', '0', ';', '4', '9', ';', '1', ';', '0', ';', '0', '_', // 1 '\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ; '\x1b', '[', '0', ';', '0', ';', '4', '9', ';', '1', ';', '0', ';', '0', '_', // 1 '\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6 '\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ; '\x1b', '[', '0', ';', '0', ';', '4', '9', ';', '1', ';', '0', ';', '0', '_', // 1 '\x1b', '[', '0', ';', '0', ';', '9', '5', ';', '1', ';', '0', ';', '0', '_', // _ }, KeyRune, ModNone, "B"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8(tt.input) expected := NewEventKey(tt.expectedKey, "", tt.expectedMod) select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != tt.expectedKey || kev.Modifiers() != tt.expectedMod || kev.Str() != string(tt.expectedStr) { t.Errorf("Expected %q, got %q (rune expected %q got %q)", expected.Name(), kev.Name(), string(tt.expectedStr), kev.Str()) } else { t.Logf("Key %s ok", kev.Name()) } } else { t.Errorf("Expected EventKey, got %T", ev) } case <-time.After(100 * time.Millisecond): ip.Scan() select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { if kev.Key() != tt.expectedKey || kev.Modifiers() != tt.expectedMod { t.Errorf("Expected %q, got %q", expected.Name(), kev.Name()) } } else { t.Errorf("Expected EventKey, got %T", ev) } default: t.Fatalf("Timeout waiting for key event") } } }) } } // TestDecPrivateModeResponse tests responses to various DEC private mode queries func TestDecPrivateModeResponse(t *testing.T) { tests := []struct { bytes string result eventPrivateMode usable bool }{ {"\x1b[?1001;0$y", eventPrivateMode{Mode: 1001, Status: vt.ModeNA}, false}, {"\x1b[?1004;2$y", eventPrivateMode{Mode: 1004, Status: vt.ModeOff}, true}, {"\x1b[?7;1$y", eventPrivateMode{Mode: vt.PmAutoMargin, Status: vt.ModeOn}, true}, {"\x1b[?25;3$y", eventPrivateMode{Mode: vt.PmShowCursor, Status: vt.ModeOnLocked}, false}, {"\x1b[?12;4$y", eventPrivateMode{Mode: vt.PmBlinkCursor, Status: vt.ModeOffLocked}, false}, {"\x1b[?990;$y", eventPrivateMode{Mode: 990, Status: vt.ModeNA}, false}, {"\x1b[?991$y", eventPrivateMode{Mode: 991, Status: vt.ModeNA}, false}, } for i, tt := range tests { t.Run(fmt.Sprintf("case #%d", i), func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Send null in initial state ip.ScanUTF8([]byte(tt.bytes)) select { case ev := <-evch: if pev, ok := ev.(*eventPrivateMode); ok { if pev.Status.Changeable() != tt.usable { t.Errorf("Private mode usability wrong %v != %v", pev.Status.Changeable(), tt.usable) } if *pev != tt.result { t.Errorf("Private mode mismatch for %d", tt.result.Mode) } } else { t.Errorf("Got unexpected event %T", ev) } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout") } }) } } func TestIgnoredSequences(t *testing.T) { tests := []struct { name string bytes string }{ {"LoneST", "\x9c"}, // 7 bit version would be confused with Alt-\ {"SoS", "\x1bXdata\x1b\\"}, {"SoS-Bell", "\x1bXdata\x07"}, {"SoS-Embed-ESC", "\x1bXab\x1bcde\x1b\\"}, {"PM", "\x1b^data\x07"}, {"PM8", "\x9edata\x07"}, {"PM-Bell", "\x1b^data\x07"}, {"APC", "\x1b_data\x07"}, {"APC8", "\x9fdata\x07"}, {"APC-Bell", "\x1b_data\x07"}, {"OSC", "\x1b]junk\x1b\\"}, {"OSC8", "\x9djunk\x1b\\"}, {"OSC-Bell", "\x1b]junk\x07"}, {"DCS", "\x1bPjunk\x1b\\"}, {"DCS8", "\x90junk\x1b\\"}, {"DCS-Bell", "\x1bPjunk\x07"}, {"SS2", "\x1bN1"}, {"SS28", "\x8e1"}, {"BadCSI", "\x1b[\x07"}, {"BadUTF8", "\xe0\xff"}, {"Win32Shift", "\x1b[16;0;0;1;1;1_"}, {"Win32Ctrl", "\x1b[17;0;0;1;1;1_"}, {"Win32Alt", "\x1b[18;0;0;1;1;1_"}, {"Win32CapsLock", "\x1b[20;0;0;1;1;1_"}, {"Win32KeyUp", "\x1b[13;0;13;0;1;1_"}, {"RuntDA1", "\x1b[?c"}, {"RuntWindowNotice", "\x1b[t"}, {"OtherIntermediates", "\x1b[1 ~"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // send event plus DECID so we get a final event ip.ScanUTF8(append([]byte(test.bytes), '\x1B', '[', '?', '6', 'c')) select { case ev := <-evch: if _, ok := ev.(*eventPrimaryAttributes); ok { return } else { t.Errorf("Got unexpected event %T", ev) if ev, ok := ev.(*EventKey); ok { t.Logf("Key %s", ev.Name()) } } case <-time.After(100 * time.Millisecond): t.Fatal("Timeout") } }) } } func TestStringSequenceLimit(t *testing.T) { tests := []struct { name string prefix string }{ {"OSC", "\x1b]"}, {"XDA", "\x1bP"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) input := append([]byte(test.prefix), bytes.Repeat([]byte{'x'}, defaultControlStringLimit+1)...) ip.ScanUTF8(input) if ip.state == istInit { t.Fatalf("parser state = %v, want string discard state", ip.state) } if len(ip.strBuf) != 0 { t.Fatalf("string buffer length = %d, want 0", len(ip.strBuf)) } if !ip.discardString { t.Fatal("parser is not discarding over-limit string") } ip.ScanUTF8([]byte("tail\x07a")) select { case ev := <-evch: kev, ok := ev.(*EventKey) if !ok { t.Fatalf("got %T, want *EventKey", ev) } if kev.Key() != KeyRune || kev.Str() != "a" { t.Fatalf("got %v %q, want %v %q", kev.Key(), kev.Str(), KeyRune, "a") } case <-time.After(100 * time.Millisecond): t.Fatal("timeout waiting for parser recovery") } }) } } func TestStringSequenceLimitAfterEmbeddedEscape(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) input := append([]byte("\x1b]"), bytes.Repeat([]byte{'x'}, defaultControlStringLimit-1)...) input = append(input, '\x1b', 'x') ip.ScanUTF8(input) if ip.state != istOsc { t.Fatalf("parser state = %v, want %v", ip.state, istOsc) } if len(ip.strBuf) != 0 { t.Fatalf("string buffer length = %d, want 0", len(ip.strBuf)) } if !ip.discardString { t.Fatal("parser is not discarding over-limit string") } ip.ScanUTF8([]byte("tail\x1b\\a")) select { case ev := <-evch: kev, ok := ev.(*EventKey) if !ok { t.Fatalf("got %T, want *EventKey", ev) } if kev.Key() != KeyRune || kev.Str() != "a" { t.Fatalf("got %v %q, want %v %q", kev.Key(), kev.Str(), KeyRune, "a") } case <-time.After(100 * time.Millisecond): t.Fatal("timeout waiting for parser recovery") } } func TestStringSequenceCustomLimit(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.controlStringMax = 4 ip.ScanUTF8([]byte("\x1b]12345")) if ip.state != istOsc { t.Fatalf("parser state = %v, want %v", ip.state, istOsc) } if len(ip.strBuf) != 0 { t.Fatalf("string buffer length = %d, want 0", len(ip.strBuf)) } if !ip.discardString { t.Fatal("parser is not discarding over-limit string") } } func TestStringSequenceExactLimit(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.controlStringMax = 4 ip.ScanUTF8([]byte("\x1b]1234")) if ip.state != istOsc { t.Fatalf("parser state = %v, want %v", ip.state, istOsc) } if got := string(ip.strBuf); got != "1234" { t.Fatalf("string buffer = %q, want %q", got, "1234") } } func TestStringSequenceUnlimited(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.controlStringMax = 0 payload := bytes.Repeat([]byte{'x'}, defaultControlStringLimit+1) ip.ScanUTF8(append([]byte("\x1b]"), payload...)) if ip.state != istOsc { t.Fatalf("parser state = %v, want %v", ip.state, istOsc) } if !bytes.Equal(ip.strBuf, payload) { t.Fatalf("string buffer length = %d, want %d", len(ip.strBuf), len(payload)) } } // TestKeyboardMode tests the responses to various keyboard modes func TestKeyboardMode(t *testing.T) { tests := []struct { bytes string result Event }{ {"\x1b[?9001;0$y", &eventPrivateMode{Mode: 9001, Status: vt.ModeNA}}, {"\x1b[?9001;2$y", &eventPrivateMode{Mode: 9001, Status: vt.ModeOff}}, {"\x1b[>4;0m", &eventXTermKbdMode{Mode: XtermKbdModeOff}}, {"\x1b[?0u", &eventKittyKbdMode{Mode: KittyKbdModeOff}}, } for i, tt := range tests { t.Run(fmt.Sprintf("case #%d", i), func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Send null in initial state ip.ScanUTF8([]byte(tt.bytes)) select { case ev := <-evch: switch expect := tt.result.(type) { case *eventPrivateMode: if actual, ok := ev.(*eventPrivateMode); ok { if actual.Mode != expect.Mode || actual.Status != expect.Status { t.Errorf("Wrong mode or status: %v %v != %v %v", actual.Mode, actual.Status, expect.Mode, expect.Status) } return } case *eventXTermKbdMode: if actual, ok := ev.(*eventXTermKbdMode); ok { if actual.Mode != expect.Mode { t.Errorf("Wrong mode: %v != %v", actual.Mode, expect.Mode) } return } case *eventKittyKbdMode: if actual, ok := ev.(*eventKittyKbdMode); ok { if actual.Mode != expect.Mode { t.Errorf("Wrong mode: %v != %v", actual.Mode, expect.Mode) } return } } t.Errorf("Wrong type, expected %T got %T", tt.result, ev) case <-time.After(100 * time.Millisecond): t.Fatal("Timeout") } }) } } // firstKey drains the event channel and returns the first EventKey received, // or nil if none arrives within 100ms. func firstKey(evch chan Event) *EventKey { var got *EventKey for { select { case ev := <-evch: if got == nil { if kev, ok := ev.(*EventKey); ok { got = kev } } continue case <-time.After(100 * time.Millisecond): } break } return got } func nextKey(evch chan Event) *EventKey { for { select { case ev := <-evch: if kev, ok := ev.(*EventKey); ok { return kev } case <-time.After(100 * time.Millisecond): return nil } } } func TestAdvancedControlKeys(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8([]byte{0x01, '\t'}) got := nextKey(evch) if got == nil { t.Fatal("expected Ctrl-A event") } if got.Key() != KeyRune || got.Str() != "a" || got.Modifiers() != ModCtrl || got.Physical() != Key('a') { t.Fatalf("expected Ctrl-A as KeyRune a + ModCtrl, got key=%v str=%q mod=%v physical=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical()) } got = nextKey(evch) if got == nil { t.Fatal("expected Tab event") } if got.Key() != KeyTab || got.Modifiers() != ModNone { t.Fatalf("expected ambiguous Ctrl-I/Tab to remain Tab, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers()) } } func TestAdvancedBacktabReportsShiftTab(t *testing.T) { tests := []struct { name string input []byte }{ {"CSI-Z", []byte("\x1b[Z")}, {"ESC-Tab", []byte{'\x1b', '\t'}}, {"XTerm-Shift-Tab", []byte("\x1b[27;2;9~")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8(tt.input) got := nextKey(evch) if got == nil { t.Fatal("expected Shift-Tab event") } if got.Key() != KeyTab || got.Modifiers() != ModShift { t.Fatalf("expected KeyTab + ModShift, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers()) } }) } } func TestAdvancedKittyKeyMetadata(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8([]byte("\x1b[65:97;2u\x1b[65:97;2:3u\x1b[57448;1:3u")) got := nextKey(evch) if got == nil { t.Fatal("expected shifted A press") } if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || !got.Pressed() || got.Repeat() != 1 { t.Fatalf("unexpected shifted A press: key=%v str=%q mod=%v physical=%v pressed=%v repeat=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed(), got.Repeat()) } got = nextKey(evch) if got == nil { t.Fatal("expected shifted A release") } if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || got.Pressed() { t.Fatalf("unexpected shifted A release: key=%v str=%q mod=%v physical=%v pressed=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed()) } got = nextKey(evch) if got == nil { t.Fatal("expected right Ctrl release") } if got.Key() != KeyCtrl || got.Modifiers()&ModRCtrl != ModRCtrl || got.Modifiers()&ModCtrl == 0 || got.Pressed() { t.Fatalf("unexpected right Ctrl release: key=%v mod=%v pressed=%v", got.Key(), got.Modifiers(), got.Pressed()) } } func TestAdvancedKittyParameterParseErrorKeepsSubparametersAligned(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8([]byte("\x1b[65;999999999999999999999999999999999:3u")) got := nextKey(evch) if got == nil { t.Fatal("expected key event") } if got.Key() != KeyRune || got.Str() != "A" || got.Pressed() { t.Fatalf("expected shifted A release despite malformed modifier parameter, got key=%v str=%q pressed=%v", got.Key(), got.Str(), got.Pressed()) } } func TestKeyConversionBounds(t *testing.T) { if key, ok := keyFromInt(32767); !ok || key != Key(32767) { t.Fatalf("keyFromInt max = %v, %v", key, ok) } if _, ok := keyFromInt(32768); ok { t.Fatal("keyFromInt accepted overflow") } if _, ok := keyFromInt(-1); ok { t.Fatal("keyFromInt accepted negative") } if key, ok := keyFromRune(32767); !ok || key != Key(32767) { t.Fatalf("keyFromRune max = %v, %v", key, ok) } if _, ok := keyFromRune(32768); ok { t.Fatal("keyFromRune accepted overflow") } if b, ok := asciiByteFromInt(0x7f); !ok || b != 0x7f { t.Fatalf("asciiByteFromInt max = %v, %v", b, ok) } if _, ok := asciiByteFromInt(0); ok { t.Fatal("asciiByteFromInt accepted zero") } if _, ok := asciiByteFromInt(0x80); ok { t.Fatal("asciiByteFromInt accepted non-ASCII") } } func TestAdvancedPhysicalKeyOverflowIsUnknown(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8([]byte("\x1b[65:40000;2u")) got := nextKey(evch) if got == nil { t.Fatal("expected key event") } if got.Key() != KeyRune || got.Str() != "A" || got.Physical() != 0 { t.Fatalf("expected overflow physical key to be unknown, got key=%v str=%q physical=%v", got.Key(), got.Str(), got.Physical()) } } func TestLargeUnicodePhysicalKeyIsUnknown(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8([]byte("😀")) got := nextKey(evch) if got == nil { t.Fatal("expected key event") } if got.Key() != KeyRune || got.Str() != "😀" || got.Physical() != 0 { t.Fatalf("expected large Unicode physical key to be unknown, got key=%v str=%q physical=%v", got.Key(), got.Str(), got.Physical()) } } func TestAdvancedWinKeyMetadata(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.advanced = true ip.ScanUTF8([]byte("\x1b[65;0;65;1;16;3_\x1b[65;0;65;0;16;1_\x1b[17;0;0;1;8;1_\x1b[65;0;1;1;4;1_\x1b[160;0;0;1;20;1_\x1b[76;0;108;1;512;1_")) got := nextKey(evch) if got == nil { t.Fatal("expected Win32 shifted A press") } if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || !got.Pressed() || got.Repeat() != 3 { t.Fatalf("unexpected Win32 shifted A press: key=%v str=%q mod=%v physical=%v pressed=%v repeat=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed(), got.Repeat()) } got = nextKey(evch) if got == nil { t.Fatal("expected Win32 shifted A release") } if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || got.Pressed() { t.Fatalf("unexpected Win32 shifted A release: key=%v str=%q mod=%v physical=%v pressed=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed()) } got = nextKey(evch) if got == nil { t.Fatal("expected Win32 Ctrl modifier press") } if got.Key() != KeyCtrl || got.Modifiers()&ModLCtrl != ModLCtrl || got.Modifiers()&ModCtrl == 0 || !got.Pressed() { t.Fatalf("unexpected Win32 Ctrl press: key=%v mod=%v pressed=%v", got.Key(), got.Modifiers(), got.Pressed()) } got = nextKey(evch) if got == nil { t.Fatal("expected Win32 right-Ctrl A press") } if got.Key() != KeyRune || got.Str() != "a" || got.Modifiers()&ModRCtrl != ModRCtrl || got.Modifiers()&ModCtrl == 0 { t.Fatalf("unexpected Win32 right-Ctrl A press: key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers()) } got = nextKey(evch) if got == nil { t.Fatal("expected Win32 left-Shift press with right-Ctrl held") } if got.Key() != KeyShift || got.Modifiers()&ModLShift != ModLShift || got.Modifiers()&ModRCtrl != ModRCtrl || got.Modifiers()&ModCtrl == 0 { t.Fatalf("unexpected Win32 left-Shift press with right-Ctrl held: key=%v mod=%v", got.Key(), got.Modifiers()) } got = nextKey(evch) if got == nil { t.Fatal("expected Win32 left-Meta l press") } if got.Key() != KeyRune || got.Str() != "l" || got.Modifiers()&ModLMeta != ModLMeta || got.Modifiers()&ModMeta == 0 { t.Fatalf("unexpected Win32 left-Meta l press: key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers()) } } func TestWinKeyASCIIFallbackRangeCheck(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8([]byte("\x1b[0;0;65;1;0;1_")) got := nextKey(evch) if got == nil { t.Fatal("expected Win32 ASCII fallback key") } if got.Key() != KeyRune || got.Str() != "A" { t.Fatalf("unexpected Win32 ASCII fallback key: key=%v str=%q", got.Key(), got.Str()) } ip.ScanUTF8([]byte("\x1b[0;0;0;1;0;1_")) if got = nextKey(evch); got != nil { t.Fatalf("unexpected Win32 NUL fallback key: key=%v str=%q", got.Key(), got.Str()) } } func TestWinKeySurrogateErrorsArePreserved(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.ScanUTF8([]byte("\x1b[1;0;55357;1;0;1_\x1b[1;0;32;1;0;1_")) first := nextKey(evch) if first == nil || first.Str() != "�" { t.Fatalf("expected orphaned high surrogate as replacement rune, got %#v", first) } second := nextKey(evch) if second == nil || second.Str() != " " { t.Fatalf("expected following BMP rune to be preserved, got %#v", second) } ip.ScanUTF8([]byte("\x1b[1;0;56832;1;0;1_")) third := nextKey(evch) if third == nil || third.Str() != "�" { t.Fatalf("expected orphaned low surrogate as replacement rune, got %#v", third) } ip.ScanUTF8([]byte("\x1b[1;0;55357;1;0;1_\x1b[1;0;55357;1;0;1_\x1b[1;0;56832;1;0;1_")) fourth := nextKey(evch) if fourth == nil || fourth.Str() != "�" { t.Fatalf("expected superseded high surrogate as replacement rune, got %#v", fourth) } fifth := nextKey(evch) if fifth == nil || fifth.Str() != "😀" { t.Fatalf("expected replacement high surrogate to pair with following low surrogate, got %#v", fifth) } } func TestAdvancedModifierHelpers(t *testing.T) { winCases := []struct { vk int wantKey Key wantMod ModMask }{ {0x10, KeyShift, ModShift}, {0xa0, KeyShift, ModLShift}, {0xa1, KeyShift, ModRShift}, {0x11, KeyCtrl, ModCtrl}, {0xa2, KeyCtrl, ModLCtrl}, {0xa3, KeyCtrl, ModRCtrl}, {0x12, KeyAlt, ModAlt}, {0xa4, KeyAlt, ModLAlt}, {0xa5, KeyAlt, ModRAlt}, {0x5b, KeyMeta, ModLMeta}, {0x5c, KeyMeta, ModRMeta}, {0x14, KeyCapsLock, ModNone}, } for _, tt := range winCases { key, mod, ok := winModifierKey(tt.vk) if !ok || key != tt.wantKey || mod != tt.wantMod { t.Fatalf("winModifierKey(%#x) = %v, %v, %v; want %v, %v, true", tt.vk, key, mod, ok, tt.wantKey, tt.wantMod) } } if _, _, ok := winModifierKey(0xff); ok { t.Fatal("winModifierKey accepted unknown key") } kittyCases := map[int]ModMask{ 57441: ModLShift, 57447: ModRShift, 57442: ModLCtrl, 57448: ModRCtrl, 57443: ModLAlt, 57449: ModRAlt, 57444: ModLMeta, 57450: ModRMeta, } for code, want := range kittyCases { if got := kittyModifierKey(code); got != want { t.Fatalf("kittyModifierKey(%d) = %v, want %v", code, got, want) } } if got := kittyModifierKey(12345); got != ModNone { t.Fatalf("kittyModifierKey unknown = %v, want ModNone", got) } } func TestWinModifierState(t *testing.T) { if got := calcWinModifier(0x71f, true); got != ModShift|ModLCtrl|ModRCtrl|ModLAlt|ModRAlt|ModLMeta|ModRMeta { t.Fatalf("advanced win modifier state = %v", got) } if got := calcWinModifier(0xdf, false); got != ModShift|ModCtrl|ModAlt { t.Fatalf("legacy win modifier state = %v", got) } if got := calcWinModifier(0xc0, true); got&ModMeta != 0 { t.Fatalf("win lock bits reported Meta: %v", got) } } // TestEscDuringCsiResetsParser verifies that an ESC byte received while // the parser is in CSI state correctly transitions to escape state per // ECMA-48 §5.3.1, rather than being swallowed as a "bad parse". func TestEscDuringCsiResetsParser(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Feed a partial CSI (ESC [) followed immediately by a new escape // sequence for Down arrow (ESC [ B). Without the fix, the second // ESC would be swallowed and 'B' would be emitted as a literal key. ip.ScanUTF8([]byte("\x1b[\x1b[B")) got := firstKey(evch) if got == nil { t.Fatal("expected a key event, got none") } if got.Key() != KeyDown { t.Errorf("expected KeyDown, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers()) } } // TestEscDuringSs3ResetsParser verifies that an ESC byte received while // the parser is accumulating SS3 parameters correctly transitions to // escape state per ECMA-48 §5.3.1. func TestEscDuringSs3ResetsParser(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) // Feed a partial SS3 with parameter (ESC O 1) followed immediately // by a new escape sequence for Down arrow (ESC [ B). Without the // fix, the ESC would be lost inside the SS3 parameter accumulation. ip.ScanUTF8([]byte("\x1bO1\x1b[B")) got := firstKey(evch) if got == nil { t.Fatal("expected a key event, got none") } if got.Key() != KeyDown { t.Errorf("expected KeyDown, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers()) } } // firstMouse drains the next EventMouse off the channel. func firstMouse(ch <-chan Event) *EventMouse { for { select { case ev := <-ch: if me, ok := ev.(*EventMouse); ok { return me } case <-time.After(100 * time.Millisecond): return nil } } } // TestInputMouseSgrClipsToScreen verifies that legacy SGR mouse reports // (CSI ?1006h) have their coordinates clamped to the cell-based screen // size, matching long-standing tcell behavior for terminals that report // out-of-range coordinates during click-drag. func TestInputMouseSgrClipsToScreen(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.cols = 80 ip.rows = 24 // SGR press at column 500, row 300 (well off-screen) — should clip. ip.ScanUTF8([]byte("\x1b[<0;500;300M")) me := firstMouse(evch) if me == nil { t.Fatal("expected a mouse event") } if x, y := me.Position(); x != 79 || y != 23 { t.Errorf("expected clipped position (79,23), got (%d,%d)", x, y) } } // TestInputMouseSgrPixelNoClip verifies that when the input parser is // in pixel-reporting mode (CSI ?1016h), mouse coordinates pass through // unchanged so applications can resolve sub-cell positions. func TestInputMouseSgrPixelNoClip(t *testing.T) { evch := make(chan Event, 10) ip := newInputParser(evch) ip.cols = 80 ip.rows = 24 ip.SetPixelMouse(true) // Same press at pixel (500, 300). Screen is 80x24 cells, so cell-mode // would clip; pixel-mode must not. ip.ScanUTF8([]byte("\x1b[<0;500;300M")) me := firstMouse(evch) if me == nil { t.Fatal("expected a mouse event") } if x, y := me.Position(); x != 499 || y != 299 { t.Errorf("expected unclipped position (499,299), got (%d,%d)", x, y) } // Toggling pixel mode off should restore clipping behavior. ip.SetPixelMouse(false) ip.ScanUTF8([]byte("\x1b[<0;500;300M")) me = firstMouse(evch) if me == nil { t.Fatal("expected a mouse event after disabling pixel mode") } if x, y := me.Position(); x != 79 || y != 23 { t.Errorf("expected re-clipped position (79,23), got (%d,%d)", x, y) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/internal/000077500000000000000000000000001520475227200220525ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/internal/widthutil/000077500000000000000000000000001520475227200240675ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/internal/widthutil/widthutil.go000066400000000000000000000020351520475227200264330ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package widthutil import ( "os" "strings" "github.com/clipperhouse/displaywidth" ) // Options returns the display-width options derived from the user's // environment. We preserve the historical RUNEWIDTH_EASTASIAN toggle. func Options() displaywidth.Options { if rw := strings.ToLower(os.Getenv("RUNEWIDTH_EASTASIAN")); rw == "1" || rw == "true" || rw == "yes" { return displaywidth.Options{EastAsianWidth: true} } return displaywidth.Options{} } golang-github-gdamore-tcell.v3-3.4.0+dfsg/interrupt.go000066400000000000000000000020751520475227200226250ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // EventInterrupt is a generic wakeup event. Its can be used to // to request a redraw. It can carry an arbitrary payload, as well. type EventInterrupt struct { EventTime v any } // Data is used to obtain the opaque event payload. func (ev *EventInterrupt) Data() any { return ev.v } // NewEventInterrupt creates an EventInterrupt with the given payload. func NewEventInterrupt(data any) *EventInterrupt { ev := &EventInterrupt{v: data} ev.SetEventNow() return ev } golang-github-gdamore-tcell.v3-3.4.0+dfsg/key.go000066400000000000000000000457301520475227200213660ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "fmt" "strings" ) // EventKey represents a key press. Usually this is a key press followed // by a key release, but since terminal programs don't have a way to report // key release events, we usually get just one event. If a key is held down // then the terminal may synthesize repeated key presses at some predefined // rate. We have no control over that, nor visibility into it. // // In some cases, we can have a modifier key, such as ModAlt, that can be // generated with a key press. (This usually is represented by having the // high bit set, or in some cases, by sending an ESC prior to the rune.) // // If the value of Key() is KeyRune, then the actual key value will be // available (as a grapheme cluster) with the Str() method. // This will be the case for most keys. // // In most situations, the modifiers will not be set. For example, if the // rune is 'A', this will be reported without the ModShift bit set, since // really can't tell if the Shift key was pressed (it might have been CAPSLOCK, // or a terminal that only can send capitals, or keyboard with separate // capital letters from lower case letters). // // Generally, terminal applications have far less visibility into keyboard // activity than graphical applications. Hence, they should avoid depending // overly much on availability of modifiers, or the availability of any // specific keys. type EventKey struct { EventTime mod ModMask key Key physical Key str string // string for key, usually just one character, but may be composed sequence pressed bool repeat int } // Str returns the string corresponding to the key press, if it makes sense. // The result is only defined if the value of Key() is KeyRune. It will be // either one key (e.g. 'A'), or could be a composed sequence. func (ev *EventKey) Str() string { return ev.str } // Key returns a virtual key code. We use this to identify specific key // codes, such as KeyEnter, etc. Most control and function keys are reported // with unique Key values. Normal alphanumeric and punctuation keys will // generally return KeyRune here; the specific key can be further decoded // using the Str() function. func (ev *EventKey) Key() Key { return ev.key } // Physical returns the physical key that was pressed, when known. // // This is different from Key() and Str(), which describe the logical key result // delivered to the application. For example, on a US keyboard Shift-/ may // produce Str() == "?", while Physical() reports KeySlash. Most applications // should use Key() and Str(); Physical is intended for layout-independent uses // such as keyboard remappers, embedded terminal emulators, and games that care // about key location rather than the printed character. // // For letter keys, compare physical values against the lowercase aliases // KeyA through KeyZ. The legacy KeyCtrlA through KeyCtrlZ constants occupy // the same numeric range as Key('A') through Key('Z'), so Key('A') is not a // physical "A" key identifier. // // If the physical key is unknown, this returns zero. func (ev *EventKey) Physical() Key { return ev.physical } // Pressed returns true for key press events, and false for key release events. // Legacy keyboard reporting only reports presses. func (ev *EventKey) Pressed() bool { return ev.pressed } // Repeat returns the repeat count for this key event. Legacy keyboard // reporting synthesizes repeated key presses as separate events, so this will // normally be 1. func (ev *EventKey) Repeat() int { return ev.repeat } // Modifiers returns the modifiers that were present with the key press. Note // that not all platforms and terminals support this equally well, and some // cases we will not not know for sure. Hence, applications should avoid // using this in most circumstances. func (ev *EventKey) Modifiers() ModMask { return ev.mod } // KeyProtocol identifies the keyboard reporting protocol that the terminal // is currently using. More capable protocols allow disambiguating modifier // combinations, distinguishing key release events, etc. type KeyProtocol int // These are the keyboard protocols that tcell can report. const ( LegacyKeyboard KeyProtocol = iota // basic VT100 style reports KittyKeyboard // kitty supports events, unambiguous keys modulo left/right modifiers Win32Keyboard // win32 supports the full feature set XTermKeyboard // xterm modify other keys, disambiguation only, no release events ) // KeyNames holds the written names of special keys. Useful to echo back a key // name, or to look up a key from a string value. var KeyNames = map[Key]string{ KeyEnter: "Enter", KeyBackspace: "Backspace", KeyTab: "Tab", KeyBacktab: "Backtab", KeyEsc: "Esc", KeyBackspace2: "Backspace2", KeyDelete: "Delete", KeyInsert: "Insert", KeyUp: "Up", KeyDown: "Down", KeyLeft: "Left", KeyRight: "Right", KeyHome: "Home", KeyEnd: "End", KeyUpLeft: "UpLeft", KeyUpRight: "UpRight", KeyDownLeft: "DownLeft", KeyDownRight: "DownRight", KeyCenter: "Center", KeyPgDn: "PgDn", KeyPgUp: "PgUp", KeyClear: "Clear", KeyExit: "Exit", KeyCancel: "Cancel", KeyPause: "Pause", KeyPrint: "Print", KeyF1: "F1", KeyF2: "F2", KeyF3: "F3", KeyF4: "F4", KeyF5: "F5", KeyF6: "F6", KeyF7: "F7", KeyF8: "F8", KeyF9: "F9", KeyF10: "F10", KeyF11: "F11", KeyF12: "F12", KeyF13: "F13", KeyF14: "F14", KeyF15: "F15", KeyF16: "F16", KeyF17: "F17", KeyF18: "F18", KeyF19: "F19", KeyF20: "F20", KeyF21: "F21", KeyF22: "F22", KeyF23: "F23", KeyF24: "F24", KeyF25: "F25", KeyF26: "F26", KeyF27: "F27", KeyF28: "F28", KeyF29: "F29", KeyF30: "F30", KeyF31: "F31", KeyF32: "F32", KeyF33: "F33", KeyF34: "F34", KeyF35: "F35", KeyF36: "F36", KeyF37: "F37", KeyF38: "F38", KeyF39: "F39", KeyF40: "F40", KeyF41: "F41", KeyF42: "F42", KeyF43: "F43", KeyF44: "F44", KeyF45: "F45", KeyF46: "F46", KeyF47: "F47", KeyF48: "F48", KeyF49: "F49", KeyF50: "F50", KeyF51: "F51", KeyF52: "F52", KeyF53: "F53", KeyF54: "F54", KeyF55: "F55", KeyF56: "F56", KeyF57: "F57", KeyF58: "F58", KeyF59: "F59", KeyF60: "F60", KeyF61: "F61", KeyF62: "F62", KeyF63: "F63", KeyF64: "F64", KeyMenu: "Menu", KeyCapsLock: "CapsLock", KeyScrollLock: "ScrollLock", KeyNumLock: "NumLock", KeyShift: "Shift", KeyCtrl: "Ctrl", KeyAlt: "Alt", KeyMeta: "Meta", KeyHyper: "Hyper", KeyCtrlA: "Ctrl-A", KeyCtrlB: "Ctrl-B", KeyCtrlC: "Ctrl-C", KeyCtrlD: "Ctrl-D", KeyCtrlE: "Ctrl-E", KeyCtrlF: "Ctrl-F", KeyCtrlG: "Ctrl-G", KeyCtrlH: "Ctrl-H", KeyCtrlI: "Ctrl-I", KeyCtrlJ: "Ctrl-J", KeyCtrlK: "Ctrl-K", KeyCtrlL: "Ctrl-L", KeyCtrlM: "Ctrl-M", KeyCtrlN: "Ctrl-N", KeyCtrlO: "Ctrl-O", KeyCtrlP: "Ctrl-P", KeyCtrlQ: "Ctrl-Q", KeyCtrlR: "Ctrl-R", KeyCtrlS: "Ctrl-S", KeyCtrlT: "Ctrl-T", KeyCtrlU: "Ctrl-U", KeyCtrlV: "Ctrl-V", KeyCtrlW: "Ctrl-W", KeyCtrlX: "Ctrl-X", KeyCtrlY: "Ctrl-Y", KeyCtrlZ: "Ctrl-Z", } // Name returns a printable value or the key stroke. This can be used // when printing the event, for example. func (ev *EventKey) Name() string { s := "" m := []string{} if ev.mod&modLShift != 0 { m = append(m, "LeftShift") } if ev.mod&modRShift != 0 { m = append(m, "RightShift") } if ev.mod&ModShift != 0 && ev.mod&(modLShift|modRShift) == 0 { m = append(m, "Shift") } if ev.mod&modLAlt != 0 { m = append(m, "LeftAlt") } if ev.mod&modRAlt != 0 { m = append(m, "RightAlt") } if ev.mod&ModAlt != 0 && ev.mod&(modLAlt|modRAlt) == 0 { m = append(m, "Alt") } if ev.mod&modLMeta != 0 { m = append(m, "LeftMeta") } if ev.mod&modRMeta != 0 { m = append(m, "RightMeta") } if ev.mod&ModMeta != 0 && ev.mod&(modLMeta|modRMeta) == 0 { m = append(m, "Meta") } if ev.mod&modLCtrl != 0 { m = append(m, "LeftCtrl") } if ev.mod&modRCtrl != 0 { m = append(m, "RightCtrl") } if ev.mod&ModCtrl != 0 && ev.mod&(modLCtrl|modRCtrl) == 0 { m = append(m, "Ctrl") } if ev.mod&modLHyper != 0 { m = append(m, "LeftHyper") } if ev.mod&modRHyper != 0 { m = append(m, "RightHyper") } if ev.mod&ModHyper != 0 && ev.mod&(modLHyper|modRHyper) == 0 { m = append(m, "Hyper") } ok := false if s, ok = KeyNames[ev.key]; !ok { if ev.key == KeyRune { s = "Rune[" + ev.str + "]" } else { s = fmt.Sprintf("Key[%d,%s]", ev.key, ev.str) } } if len(m) != 0 { switch ev.key { case KeyShift: if ev.mod&(modLShift|modRShift|ModShift) != 0 { return strings.Join(m, "+") } case KeyCtrl: if ev.mod&(modLCtrl|modRCtrl|ModCtrl) != 0 { return strings.Join(m, "+") } case KeyAlt: if ev.mod&(modLAlt|modRAlt|ModAlt) != 0 { return strings.Join(m, "+") } case KeyMeta: if ev.mod&(modLMeta|modRMeta|ModMeta) != 0 { return strings.Join(m, "+") } case KeyHyper: if ev.mod&(modLHyper|modRHyper|ModHyper) != 0 { return strings.Join(m, "+") } } if ev.mod&ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") { s = s[5:] } return fmt.Sprintf("%s+%s", strings.Join(m, "+"), s) } return s } // NewEventKey attempts to create a suitable event. It parses the various // ASCII control sequences if KeyRune is passed for Key, but if the caller // has more precise information it should set that specifically. Callers // that aren't sure about modifier state (most) should just pass ModNone. func NewEventKey(k Key, str string, mod ModMask) *EventKey { return newEventKey(k, str, mod, true, 0, 1, false) } // NewEventKeyEx creates an extended key event with press/release, physical key, // and repeat metadata. It also uses the newer key normalization rules: ASCII // control letters are reported as KeyRune plus ModCtrl instead of legacy // KeyCtrlA through KeyCtrlZ values. func NewEventKeyEx(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) *EventKey { return newEventKey(k, str, mod, pressed, physical, repeat, true) } func newEventKey(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int, advanced bool) *EventKey { ch := rune(0) if len(str) == 1 { ch = []rune(str)[0] } if repeat <= 0 { repeat = 1 } if k == KeyRune { if ch != 0 && (ch < ' ' || ch == 0x7f) { // Turn specials into proper key codes. This is for // control characters and the DEL. k = Key(ch) if mod == ModNone && ch < ' ' { switch k { case KeyBackspace, KeyTab, KeyEsc, KeyEnter: // these keys are directly typeable without CTRL str = "" default: // most likely entered with a CTRL keypress mod = ModCtrl } ch = ch + '\x60' } } // For legacy reasons, if Ctrl is pressed with an ASCII alphabetic, then we // emit it as a KeyCtrlXX symbol. if mod == ModCtrl && !advanced { // We don't do Ctrl-[ or backslash or those specially. if ch >= 'A' && ch <= 'Z' { // upper case k = KeyCtrlA + Key(ch-'A') str = "" } else if ch >= 'a' && ch <= 'z' { // lower case k = KeyCtrlA + Key(ch-'a') str = "" } } // Windows reports ModShift for shifted keys. This is inconsistent // with UNIX, lets harmonize this. if mod == ModShift && str != "" && !advanced { mod = ModNone } } // Backspace2 is just another name for backspace. if k == KeyBackspace2 { k = KeyBackspace } // Advanced key reporting exposes Shift-Tab directly. Backtab is a legacy // alias from terminals that cannot distinguish a physical Backtab key. if k == KeyBacktab && advanced { k = KeyTab mod |= ModShift if physical == 0 || physical == KeyBacktab { physical = KeyTab } } // Shift-Tab should be Backtab. if k == KeyTab && (mod&ModShift) != 0 && !advanced { k = KeyBacktab mod &^= ModShift } ev := &EventKey{key: k, str: str, mod: mod, pressed: pressed, physical: physical, repeat: repeat} ev.SetEventNow() return ev } // ModMask is a mask of modifier keys. Note that it will not always be // possible to report modifier keys. type ModMask int32 // These are the modifiers keys that can be sent either with a key press, // or a mouse event. Note that as of now, due to the confusion associated // with Meta, and the lack of support for it on many/most platforms, the // current implementations never use it. Instead, they use ModAlt, even for // events that could possibly have been distinguished from ModAlt. const ( ModShift ModMask = 1 << iota ModCtrl ModAlt ModMeta ModHyper ModNone ModMask = 0 ) const ( modLShift ModMask = 1 << (iota + 5) modRShift modLCtrl modRCtrl modLAlt modRAlt modLMeta modRMeta modLHyper modRHyper ) // These modifiers identify a specific side when the keyboard protocol reports // one. They include the aggregate modifier bit, so ModLCtrl also satisfies // checks for ModCtrl. const ( ModLShift = ModShift | modLShift ModRShift = ModShift | modRShift ModLCtrl = ModCtrl | modLCtrl ModRCtrl = ModCtrl | modRCtrl ModLAlt = ModAlt | modLAlt ModRAlt = ModAlt | modRAlt ModLMeta = ModMeta | modLMeta ModRMeta = ModMeta | modRMeta ModLHyper = ModHyper | modLHyper ModRHyper = ModHyper | modRHyper ) // These keys are aliases for printable physical keys. They are primarily // useful with EventKey.Physical, which may report a base key location separately // from the generated text. // // These names identify the unshifted base key on a US-style keyboard layout. // They do not identify logical characters produced by modifiers or other // layouts. For example, the physical key named KeySlash may produce "/" or // "?" on a US keyboard depending on Shift, and may produce different text on // other layouts. Applications interested in the logical key sequence should // use EventKey.Key and EventKey.Str instead. const ( KeySpace Key = ' ' Key0 Key = '0' Key1 Key = '1' Key2 Key = '2' Key3 Key = '3' Key4 Key = '4' Key5 Key = '5' Key6 Key = '6' Key7 Key = '7' Key8 Key = '8' Key9 Key = '9' KeyGrave Key = '`' KeyBacktick Key = KeyGrave KeyMinus Key = '-' KeyEqual Key = '=' KeyLBrace Key = '[' KeyLBracket Key = KeyLBrace KeyRBrace Key = ']' KeyRBracket Key = KeyRBrace KeyBackslash Key = '\\' KeySemi Key = ';' KeySemicolon Key = KeySemi KeyQuote Key = '\'' KeyApostrophe Key = KeyQuote KeyComma Key = ',' KeyPeriod Key = '.' KeySlash Key = '/' KeyA Key = 'a' KeyB Key = 'b' KeyC Key = 'c' KeyD Key = 'd' KeyE Key = 'e' KeyF Key = 'f' KeyG Key = 'g' KeyH Key = 'h' KeyI Key = 'i' KeyJ Key = 'j' KeyK Key = 'k' KeyL Key = 'l' KeyM Key = 'm' KeyN Key = 'n' KeyO Key = 'o' KeyP Key = 'p' KeyQ Key = 'q' KeyR Key = 'r' KeyS Key = 's' KeyT Key = 't' KeyU Key = 'u' KeyV Key = 'v' KeyW Key = 'w' KeyX Key = 'x' KeyY Key = 'y' KeyZ Key = 'z' ) // Key is a generic value for representing keys, and especially special // keys (function keys, cursor movement keys, etc.) For normal keys, like // ASCII letters, we use KeyRune, and then expect the application to // inspect the Str() member of the EventKey. type Key int16 // This is the list of named keys. KeyRune is special however, in that it is // a place holder key indicating that a printable character was sent. The // actual value of the rune will be transported in the Rune of the associated // EventKey. const ( KeyRune Key = iota + 256 KeyUp KeyDown KeyRight KeyLeft KeyUpLeft KeyUpRight KeyDownLeft KeyDownRight KeyCenter KeyPgUp KeyPgDn KeyHome KeyEnd KeyInsert KeyDelete KeyHelp KeyExit KeyClear KeyCancel KeyPrint KeyPause // KeyBacktab is used for legacy Shift-Tab reporting. In advanced key // reporting mode, Shift-Tab is reported as KeyTab with ModShift instead. KeyBacktab KeyF1 KeyF2 KeyF3 KeyF4 KeyF5 KeyF6 KeyF7 KeyF8 KeyF9 KeyF10 KeyF11 KeyF12 KeyF13 KeyF14 KeyF15 KeyF16 KeyF17 KeyF18 KeyF19 KeyF20 KeyF21 KeyF22 KeyF23 KeyF24 KeyF25 KeyF26 KeyF27 KeyF28 KeyF29 KeyF30 KeyF31 KeyF32 KeyF33 KeyF34 KeyF35 KeyF36 KeyF37 KeyF38 KeyF39 KeyF40 KeyF41 KeyF42 KeyF43 KeyF44 KeyF45 KeyF46 KeyF47 KeyF48 KeyF49 KeyF50 KeyF51 KeyF52 KeyF53 KeyF54 KeyF55 KeyF56 KeyF57 KeyF58 KeyF59 KeyF60 KeyF61 KeyF62 KeyF63 KeyF64 KeyMenu KeyCapsLock KeyScrollLock KeyNumLock KeyShift KeyCtrl KeyAlt KeyMeta KeyHyper ) const ( // These key codes are used internally, and will never appear to applications. keyPasteStart Key = iota + 16384 keyPasteEnd ) // These are the control keys, they will also be reported with the // rune (lower case) and control modifier. If the shift key // or other modifiers are present then these will *NOT* be reported, // but reported instead as KeyRune. // // Note that these are not reported in advanced key reporting mode. // Instead, for advanced keys, expect KeyRune and a modifier with the // associated rune to be sent. const ( KeyCtrlA Key = iota + 65 KeyCtrlB KeyCtrlC KeyCtrlD KeyCtrlE KeyCtrlF KeyCtrlG KeyCtrlH KeyCtrlI KeyCtrlJ KeyCtrlK KeyCtrlL KeyCtrlM KeyCtrlN KeyCtrlO KeyCtrlP KeyCtrlQ KeyCtrlR KeyCtrlS KeyCtrlT KeyCtrlU KeyCtrlV KeyCtrlW KeyCtrlX KeyCtrlY KeyCtrlZ ) // Special values - these are fixed in an attempt to make it more likely // that aliases will encode the same way. // These are the defined ASCII values for key codes. They generally match // with KeyCtrl values. // // Most of these will not be reported in advanced key reporting mode, as they // are not possible to type directly. Some notable exceptions are KeyESC, KeyBS, // KeyTAB, and KeyCR, which have aliases below. const ( KeyNUL Key = iota KeySOH KeySTX KeyETX KeyEOT KeyENQ KeyACK KeyBEL KeyBS KeyTAB KeyLF KeyVT KeyFF KeyCR KeySO KeySI KeyDLE KeyDC1 KeyDC2 KeyDC3 KeyDC4 KeyNAK KeySYN KeyETB KeyCAN KeyEM KeySUB KeyESC KeyFS KeyGS KeyRS KeyUS KeyDEL Key = 0x7F ) // These keys are aliases for other names. const ( KeyBackspace = KeyBS KeyTab = KeyTAB KeyEsc = KeyESC KeyEscape = KeyESC KeyEnter = KeyCR // NB: This key will be translated to KeyBackspace KeyBackspace2 = KeyDEL ) golang-github-gdamore-tcell.v3-3.4.0+dfsg/key_test.go000066400000000000000000000113671520475227200224240ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import "testing" func TestEventKeyNameSidedModifiers(t *testing.T) { tests := []struct { key Key mod ModMask want string }{ {KeyShift, ModLShift, "LeftShift"}, {KeyShift, ModRShift, "RightShift"}, {KeyCtrl, ModLCtrl, "LeftCtrl"}, {KeyCtrl, ModRCtrl, "RightCtrl"}, {KeyAlt, ModLAlt, "LeftAlt"}, {KeyAlt, ModRAlt, "RightAlt"}, {KeyMeta, ModLMeta, "LeftMeta"}, {KeyMeta, ModRMeta, "RightMeta"}, {KeyHyper, ModLHyper, "LeftHyper"}, {KeyHyper, ModRHyper, "RightHyper"}, {KeyShift, ModShift, "Shift"}, {KeyCtrl, ModCtrl, "Ctrl"}, {KeyAlt, ModAlt, "Alt"}, {KeyMeta, ModMeta, "Meta"}, {KeyHyper, ModHyper, "Hyper"}, } for _, tt := range tests { ev := NewEventKeyEx(tt.key, "", tt.mod, true, tt.key, 1) if got := ev.Name(); got != tt.want { t.Errorf("Name() = %q, want %q", got, tt.want) } } } func TestEventKeyAdvancedNormalization(t *testing.T) { tests := []struct { name string ev *EventKey wantKey Key wantStr string wantMod ModMask wantPressed bool wantPhysical Key wantRepeat int }{ { name: "advanced ctrl letter", ev: NewEventKeyEx(KeyRune, "a", ModCtrl, false, KeyA, 0), wantKey: KeyRune, wantStr: "a", wantMod: ModCtrl, wantPressed: false, wantPhysical: KeyA, wantRepeat: 1, }, { name: "advanced shift tab", ev: NewEventKeyEx(KeyTab, "", ModShift, true, KeyTab, 2), wantKey: KeyTab, wantStr: "", wantMod: ModShift, wantPressed: true, wantPhysical: KeyTab, wantRepeat: 2, }, { name: "advanced backtab normalizes to shift tab", ev: NewEventKeyEx(KeyBacktab, "", ModNone, true, KeyBacktab, 1), wantKey: KeyTab, wantStr: "", wantMod: ModShift, wantPressed: true, wantPhysical: KeyTab, wantRepeat: 1, }, { name: "backspace alias", ev: NewEventKeyEx(KeyBackspace2, "", ModNone, true, KeyBackspace2, 1), wantKey: KeyBackspace, wantStr: "", wantMod: ModNone, wantPressed: true, wantPhysical: KeyBackspace2, wantRepeat: 1, }, { name: "legacy shift printable normalizes", ev: NewEventKey(KeyRune, "A", ModShift), wantKey: KeyRune, wantStr: "A", wantMod: ModNone, wantPressed: true, wantPhysical: 0, wantRepeat: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.ev.Key() != tt.wantKey || tt.ev.Str() != tt.wantStr || tt.ev.Modifiers() != tt.wantMod || tt.ev.Pressed() != tt.wantPressed || tt.ev.Physical() != tt.wantPhysical || tt.ev.Repeat() != tt.wantRepeat { t.Fatalf("unexpected event: key=%v str=%q mod=%v pressed=%v physical=%v repeat=%v", tt.ev.Key(), tt.ev.Str(), tt.ev.Modifiers(), tt.ev.Pressed(), tt.ev.Physical(), tt.ev.Repeat()) } }) } } func TestPhysicalKeyAliases(t *testing.T) { if KeyA != Key('a') || KeyZ != Key('z') || Key0 != Key('0') || Key9 != Key('9') || KeySpace != Key(' ') { t.Fatalf("physical key aliases do not match their rune values") } tests := []struct { name string key Key want Key }{ {"grave", KeyGrave, Key('`')}, {"minus", KeyMinus, Key('-')}, {"equal", KeyEqual, Key('=')}, {"left bracket", KeyLBracket, Key('[')}, {"right bracket", KeyRBracket, Key(']')}, {"backslash", KeyBackslash, Key('\\')}, {"semicolon", KeySemicolon, Key(';')}, {"quote", KeyQuote, Key('\'')}, {"comma", KeyComma, Key(',')}, {"period", KeyPeriod, Key('.')}, {"slash", KeySlash, Key('/')}, } for _, tt := range tests { if tt.key != tt.want { t.Fatalf("%s key alias = %v, want %v", tt.name, tt.key, tt.want) } } } func TestPhysicalKeySlashLogicalText(t *testing.T) { unshifted := NewEventKeyEx(KeyRune, "/", ModNone, true, KeySlash, 1) if unshifted.Str() != "/" || unshifted.Physical() != KeySlash { t.Fatalf("unshifted slash: str=%q physical=%v", unshifted.Str(), unshifted.Physical()) } shifted := NewEventKeyEx(KeyRune, "?", ModShift, true, KeySlash, 1) if shifted.Str() != "?" || shifted.Physical() != KeySlash { t.Fatalf("shifted slash: str=%q physical=%v", shifted.Str(), shifted.Physical()) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/logos/000077500000000000000000000000001520475227200213615ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/logos/patreon.png000066400000000000000000000136511520475227200235450ustar00rootroot00000000000000PNG  IHDR@@͐gAMA a`IDATx U<呐H CqVDDyruWVk*RUPk]-JgX5 Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#> Ѷ#@yϧen/!Wpy5Ob=-mn&Ҿli 2~2ĪIed1{M3}i3ukT [Qon|UI|>zGmmf;p~̞+f<ݦx O|m,| %b>UݴQ<򐤿^ 1V̜y| Ĺ"ƗZW'@Vgϖ_p/)JzB/6M_ZkŮ^%.׉qO#$OsС} +@UH֎ɮo_WL{[bKcO Cq# P $*V>v~7Bsı8l13W5*@wIߣ)f~'$#xFN.OZW.n6ڧ eWKr|is}ӖٌЇ"cZuҺ?%{]^k%9ti 5]}Bַ,K%zIo M[5{4TܑwT>RZ?=}Ujw?a [l~"SnuXN0l͒^_b^)?+fD80Z}VHK ٍݯHv5/_C`]P\]'~5Dbkk~&Ɋygq@`@V}i]%¯e;K`}PWEv"i}_zGx |D>}Ah$u`^_, z?K?), -I{_a`ax Rw ?[?H~d\!WҺ8'_!5GM}?Q@%_yf}I[`ؑd-`.nl'`] \uϕF&@#AjXJOJxI|ɂvY>ykdZقK ÿ(@(C뮔}u}K``i)yR]Ο +@oϺts"*y=jU?:ܹ\#;Q ; RIP@C'-ܩSw@S UC֨E%{bu!X$W׋@T6uGzVV5=;B-@\ljvg$?bKqZǗ53FxZW7?6ug!||p „x@~:l\/ ݛ^Z,=o eXtW *[?&gmf\ٹعU-itֈlX[T26ᗰeY ,k,J:6f<h;Fc^-O+j6Nw[|RR6ٳ&@s8ƍ\M]u!ғΕxocUM_vpdbpf5M콫`vE]:*vĴ;d}uS7sb} 6ChC)E۞Y 锭y5rmXCJou5vUNzJ>.VF*mϲ#$ǟsH,Si(KraQQyѸ?Y<N T^''& y/f}+!1t96?bػ5Y@/=||c[4Y;XBSgJ69}\i&og? 7r܅̱#}ָs3^cjh~_D_?GS[][OUZ}eNorÁ|"-t{ ]Q|5>pÎ*chC*g$.<üU⾷{G/H3O{?)Mwz3Ź;zt#qNo6N6x E8CbbTi"5~e.@z'g3ܑJg?}[<qQ~^̌z| E#/9͒w.d9w>݆;^n/'4ܬM~(}fH ߐ}4>O0`=GfϕKnRyE+,a aEmU֓9$2I^vE 5N=K{H?"eKwqM#n] >X4 ɼKɡ44%5txܗ.$]ZS;yG8ɼ}T0u/'o{$o9E_$m7ݲY4F*8hK3%q1#){=noU$qg8KN:],׋ݴQ',_'͢S7U&K]{n΅bW<$9NGBrP8H}p&m"럖E'ZF'K =eX^ꝉ{!9ŮdK|U+Ķ>/3q!$ |, W2$ wwGdxdyOiV6|U{ڸ0Bw å.Mb<.w@ boy~29lĉ? "{O3m3ܿ3燸.NJ/h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5l h% @5`5lhP5OfQeDh0-ZN@O}j2 Ub@S@@]>@@UX)@d,P%@j"OЧ&c!*PU(| >5 T E SX JT.EOMBUvQ, }j2 Ub@S@@]>@@UX)@d,P%@j"OЧ&c!*PU(| >5 T E SX JT.EOMBUvQ, }j2 Ub@S@@]>@@UX)@d,P%@j"OЧ&c!*PU(| >5 T E SX JT.EOMBUvQ, }j2 Ub@S@@]>@@UX)@d,P%@j"OЧ&c!*PU(| >5 T E SX JT.EOMBUvQ, }j2 Ub@S@@]>@@UX)@d,P%@j"OЧ&c!*PU(| >5 T E SX JT.EOMBUU_,IENDB`golang-github-gdamore-tcell.v3-3.4.0+dfsg/logos/staysail.png000066400000000000000000000420161520475227200237230ustar00rootroot00000000000000PNG  IHDR,,y}usBIT|d pHYs itEXtSoftwarewww.inkscape.org< IDATxwxTי?QwQ1.'8n$ko$ec0`'8Nw,lǸ`6wi4E3z~ `HBv̜f}%f9sCq(y8- J3)H4$"C H?2O=]@sC܁APv)h@'D'~BzoZ:/ +6".,_9 `ԌC ~g=B9ܰu`\H*J/@bB`s%8nIB7H OXPXk=VxPloziLJc@:.Y(ŏZ=BxCQu|?KGmm ;eu*AK~^V<{.;`Ą'Y%$:I@XG"~ېuX\ !0,[*^ FB&*w`9o^8Z@`:7rI8+2-~:IUW'pPBD"욷\R҂@dN2S4xc>_+-6kRB<͊EI/u$N⹽bʩ;5 :f.OԮy{ӋӱxV.}89HMYh9sҊe1 XA0T5JS8KC -]x-Nz\YiZ,+_tK$Z7gDE*eG$̝\㙯 !Jbey! -zoLyۮ(gDiTrl\Y>;["'"K^e>a_X),?dR fg`s ̛^ۖnV"PJvI .7Y2Q'=wxu$شzyǦӰdv>mhO!򅡄9&ByӲQjJlcTK۬'fXDm$Ӎ7D{iZ.+EżhxHrYdiyF\0'i+᪘$-BP/z\xD jwHnTN;Sj2S5X  1W I%:,)?>j΋&B@.6?:s.al[J(} yѬ\Qdji4⋓}VU)9!z뗗ᓯoDCMS6`㋢ @T +tW%JS`zN'$pCp{'j2l4/P'D!b2w[qq1VILEE+v$jE1EaG"0oZ6.[;ى3Zqᜰ3Xk;crM.RT5,ϔ:;#kDϧ*pKqڅRil{ n޺2@,;~ PQū{`GtGg1&NG7sN%u{"^# ɿ0De.aN[0=I+}"}҈&H3QNX]Be%˂i]|3_2c]BݛW}EJ{k)aһBh2+ɋ|~(-,m@"J,:n `GgBJXk=C0$=KhFv&}a%LAsx.M.|WXgd7E(dd萢{fh umUHtҹdR ˴aL Z;ًpچ\n{O3,?ٶ8X6y U3&ɵ| 9?hWOi YߴuH  ':%@|Li3#};cXA6.ï"%[&f /3^ 58(ʍ w`͝W/Ɔeྖ/'%$ fDz 5x]BJ}"筫Q1 22nT+C~{бMT(R*йɯ~tD8%HR1': V:OXQPmħp!!O"VeׇR+3"73:3mA2R*94r2tَj8܈+9yVL@fDBJRGoeWPбTšyӲO):߃"*"$s^?y k26-"'JqVy>-G.N#G쨜g`>? :λw5uɍrT֍j܄՗j/U !hw~0!]k0_PqD(xw0LkGQVhJ)vWTs n NG<$VJq+8a7\u ^݁ y]LXA]>. +F5t̨U&6$޶2)uyn /F'>kFkagzd~y\@$tX3a '9 ,bwR;xh7iMfw^ bn3j:v4j%%Q;=v8]1!]kJӴ {G?&,D[Wa]>!k!OX*|>|Q+0f;<#N͊5N)w #mjaw a$2j)(aWVQvQUs2yŠ WLk#-۰7lnP"a>0D nw[TЪy-hy7--g KA K(_R袼Rh NX;]R{-0V?(%,?Z$ ~ǰ7x~p4-~m2,6rȄUY'%bNbJkl n]@Y#$ŝW/fF2Xzn @i\@ӐI ƖܧpEvI0Ps Pʻ!R+W!!\l=3iE;77ʅHT4>|il h˦6;3ݾWzwfL +1(vQƯ`*ETN߽ϊJp&7IX3ù\^w3u0kM.`F¡-,$,@ܘ]>3gGX4+vd@Ve@nd~LwQ/8 J$٧O( OBa_0',1LߪXD*o8^gn|!"^>;)ǀTqT/UX0)R>PiA +b[zFx&{]S^$?S O_pŴu,Żb%IQwG%_ )Nw tL |]ԦKc{s&!ѳZX- 1Lu {qxS2ҋ )r8u _5e(n:w>i:^=DZo*z:a< ^oB`Te#ͣ +~gp#kw nA[W| g>?š[O1!+WIJÈ[ ޹>@p5lZWƠu8xb9oUJW n2\5e&6<~xKF5 JI@x!q4rܶ 8a(Ũ]V:#tR[Xz"f YOtUSDE7-K&R& k F\^Ůū :&+b;=:;-&CI(]±n)ҠͱcfƠcK!+BIk/(ƚEÈ ]oaf?^lj뗕B!;ͦP1/u@9ҵ|܆3exD[V'YqTl9?o~tlL)Í*;]o]f5EZXOG[w ؎-K\(Xoۋe. N,R *Dȝ-k6ܩcF~n,9|kwe,L+U&k07SW,b?:e<pb#w늘4.F$bYgq(wWΘYMkC Ip6>?StOTn{YR"(EpUgķGWjDKuı4-j+JEĉ^6 B!u R4⩿\4RAD]fzr߈~ }㈅[.[" zګ4xy1NM27:BnZ>-Y,()~?Gc浸8qV +s$?ߟ]BBY)/ZW WTbOv `#~/!;,)Guȅ!6 + :R*hջ"/r0(KG)ʧ QfJ w]knH$hN%, z ߻ewN75h3D{o ;7\[W\2S59xgJ4N%,f:E5PEö!7N>|qCS T?٢) ſ.-?8Q 8?h“Sb<<˦>|q=CuEJ!-g=:na!+f ;0&̔c@<^KL Zp mМw iDI%˭h.R[ET-ږ xt.R*_Q: .AT&GQWpVG x'.^?K[quVn:ܸRʥؼv0l^JPr&7IXD-hT9F}o$],F  NXnA\`@p͆9ĬL^T:hp:IDI`Uy!_W-DfVk9"1go]qVgą+{SB_K!^ӈSLfF 9=D1tJqq=bYZ) Z.JToq(! ٢-l|!jcJh#Q!@kU53Y0a X:a`wz0tctac8j9T  )Jj9r)TJ)**)򯿧QɑW!EQQՆ$іū/4KqVIn4f)~EY̯r{kGG ]}v0[7*IBi`Q{5 ::RtJ*0_tJU0T0}UsVɘ7b:/j6Q%+G+StiA[vtkGgf+_i5ͅ ?Dwx;zԍ-ׇaw./<^x<~ =p{}p|p{|pypO=뱉PRlfczqcVi:R lؼY1$_>1oQPJ_$B.EQt  2RTHիAQtF"D&Ȉߏ~W.<^.^?./^./|^?^8py|p {aÍan6r^o`D/@bja:fflj6s:jw'a Ij9L)(ME^ze҈EL*9K5\n/?Cp{p ݰ;ZtppcB9nW|"^|e>+B?UtPJ1Vz4ښX&`VY:MܩbJ\) B.B>uw/cra ;%c kaŨWskv/:]xNl7#9g'[:w}3%qv : ;z*9u/:r/Pɟ e}Pn@Fd. $>jA}gr1vnjT ب\T[/_iCOqu}ͫgL~|`ǝ=9<OX7&.Ċ&,_2SZ"Լzd.GǺF%&GA`-,TIMrCH%&j3J21,EP§2pS^;qcM4qi/ _G!tnsMya3/EDa# J"#UT Yi(]Ƅ4}t<zcCS/5Bȿ67l|;Jw&6 QVʐAAT Ҍ*ЩkikkP+e1 8:6 }V 6lC.X0hƀʼn^D8TXSC)yI%tj9tZE )jP&h5r(d(Rhէ-AC.@%B4@;P"!q 7( _|8=pg6p Ɛ ǰN'dpa Uyd6O'x}@6 :S,J)$RAZ5r NpT*Z^6nƧ^F9]>/Xc'jI$"=hr xR.K'\q6(Ch1q# k:X7OYHq\dP铓y¤VmQ^8nr /f.cIk8; I璐UjJ\8GZX4'V(܏!=8}$煔2T@K(8.Q4SCJXG@ %9{p.E!p~IPq\"S)ܿ !'ރw)(?qɋ|;!.y % 8F)տ a1oE},FK*#7C'6# :ҍjJu 00h0P+Pgv>+"Ű ?P]6ƐӅa/NNtoGO}CE#Binw N&ʠS?~|)2g5 ~o~o^&dkQGAYdiEQ4 -wpe8܏dN*H4 $)WPNx$5n^W~g{\:d둟S)ۀLݨ%sO)Zd/}ځ$;$IX,/Ro_VO1>l/|ǚcF<#JRQgDaYfh!ä4YVi}ڞO33=eJRdڼn{ˊ =gx(^? qQP禠 %)LհM4^ #v yJ襭 ;/hvSMՑ.h{y':a_V}FcezE9r2tz«:,RYz$?%(-HA~F=vv[mA[ =CgiR10 SLibJEQRi7熣<ڰH3⯶ {3>1H{G_v'LJeQk)nFa)r{1mfh5c͝tؘMb,OrHm9}o8;F qul,±~|ҏ|<mfѽr)ҍjdiCNZdju/Nd Nqm[kxΟ ˰4?|0?,"x=5;}r v6֯(V=P굡]vtkXJJVZV VFi)jH ݨԋ+K.hn~4NՏڂ p_41rsQUQ Vvv9U)PB!b|T--ŊނN4>?Eg'3NZNb'5^?͘PO᫖e .ymotFQ/Z__ﱸVeT,%s`zv\S mȍ+ڻm8>fw>3^mqy H%ax}=NHAPҰh^$&IzϿSBoŵ"Ab,bJP$x>t sMh鰠sMQ`䢠̔-Œy}컟{u7v%& @Q, :L-LE)SMi(-HANrCNzkvφ>;:{l賢ǎ>ߑK0+b(Ko?% Гrt~mQn~9^CJA4-4N"']T :ZZZZlpcx}vzw\]:\ݰڇaraDw߼떜T o1c<^VH`y }+'KLǕ jD(9JyP œUx𶕣 %r`G]O:vo$jx~=]^8=p{|\;-_8Sa~G8Yg.F^ƀc;SU} | u3Ǎ`㪩شz$+^ TXҽ_Edf 8.(L->2õDž_tk"$w=V8n\*lgx}vrkv @8ġKfT#ͨD^ N ZRRJZZJvfmZ%XƇSg;هiAs z͎L$X|LaMZ 4 LNT jdg萓95Mt5R JdhblCN;qՌu㯺qɁn|r`K,`T;2407Ct-sq}GǺ{MxpSnEa'#`@%=U#ˠS{0?Sl= 2$zͱ=^4u~h\IܰaVI:,c T@VyyFeꑗC^&~#?ؕ~ Rtnu({,M" ` Xiԣ4?)#/K|Rx]؟pK$KoeiJXPaoV-GAYzPGal(pV 'V'zC0[w=rx`s04cxMȆD"@CC ֨d(M”gDqF2^o֑|e|@&GW@/4W7)j,_'lL9L?3;kGWݧ矮Tkv`dZUU4?sda,̝5$'ӿ"U-/8:s2a@Aui8FD\&xStJdiCAl=O%4ux+Zhﶢۊ^] fea2\r 4O{bݘ ږϰ b,MPX]%xu"r\2t>|~-hDG=&1HRtJu*fT#']4 2ϪӥIzÍ-@b겢 :Q+el xqo~9@)pWd:a~;(Yq+wTׇA', چ1hƠ}f'Xtz(2JLBJ\ R ZT ):%R*f赡s';a=Uy.ìqyf˃Knݖ#5L@biʅ`jw o9b $n|7qejr\j V ?:^;& JES~qܳ5È ǰ]h벡km6vMSs;ѽ-v7 Tng9x̶|av5nk.+E9?hi~fOD|[_6֡T Ǭ hoЁ~;hﶠTRj붢g`uxO,܁_ @IaG(N֑ %)Yr (1";C.,N X0[oqwqj:z;4s#P[/-0aƒ@z%4 )zUkjKjRAð ېn.N]>m800zqGo-/.0B L@Էa9ND(N$TW`, eBy(r`4NVBM5Wq,|8qqBonnց+!UZJ9^F^ڰ ցDB$, P|/:l`Ký?  +f* ,ֱp\aˬCOXao?K@DcV/Vn k4)$B%'qEFW&Cu6Ss:ֱp\䮖u t ) ֱp\Ͷ^KƆAKAS`5(Po{XBRΖ_Xqchݒ(KlBi;ߵ !pqRȥɞvJBɓk9q8}nd𱛳X72kC `9c \]q 0\5]|~I,f <((Y"F1-%Ar%똸fw[ݝsIDAT۬^`Ċ& vw@ @{zqe{FxšGSH)zֱp$^(a )p:xVzӝׂ'.nbZA.T\G<:DVeT>U[Ap/Lh>:V<{.;p OXRW'oW:M>:DV ]l' m9󯬃ItCU3Hc~O?h~y笃I,wI3s7}ﯖSEmRWOoz17?嗾dHg` c0p=Ǡa9 T;*0u#}߷#N2-ĹB\0srPS:0" i!EVa[m c+c1 Q0J0F f(!{v7Sa4gƇ7t _agnmC׍5 3a`1 Q0J0F f( b%A 3a`1 Q0J0F f( b%A 3a`1 Q0J0F f( b%A 3a`1 Q0J0F f(a4L C`lv%$rΛQ6Ite]8AMA* \lGAzZ\tTa54KgLA!x 2Kqxζykz,z "kJNspdp,z NnGbU4ԉ{O=| l ) ]|w#ڝLu["*]X8!UZ2 ]A2[a5!'jD3md yݓ=Hy.ȴ]K$*[ D M?~Jnݵ9x:)D)3f4]|ѩ>NP+.eZ/, pi FNlHJXdck01?kGB$[ACd[@6n Zr%C~ W7ZCE[EP ҅ަmS0X,^3YFZ@0(/h=V$E*9Qi'k ps\BiΆyoC#;Rr!)Y܏ mǁ"nDOŷ ^3dzתo~0QW0Uź2V!~#(Pe Lz FۣNyUp :4 =ُIyoi]/@} \=ք6.*m0ä́y-0JhOk{ |p l+'ci::c V\q*d_,(^4\GE/ąܥ*~G3nu6#ˑݤz=UЛgKaފ& S9V?2[-&A?TLyg5#^C[ >u n *B.nIDi$cT'>3=֣LMB x`JAPZ+"'GN\ =pwM>2_% =zpp<ٟiZk\Nw@*9Cf\\t{_'L^͒xCF$?T}YW4_1¶--YI[G|?.#Re 늬Ľ9o[՞ɂHK@4 )F") O"7k#>ykMqQ|EPe}%"'0hqE%O!.-po۰9܏'!%w";FnN>Vx$ފ%uug;,EzxY% ?V ,p7Mr ÓX@L)vk'#7W{&NDzixwo I}j+)!M5\PF>?R 3O!B䠳U$i1)Park*DCnUZi8j44DVj} 4TϮX=ۓ&M5^o'| B#7w\pAD9"._= 'h*]sU¯[m" AU7u> iCi8& ldEG#ۘ{\{i2ً7Q еHw!k<4DZțb׽lR#[F]{x1ݳMNQ k]qYIdwH i%qostG}cě 'JJy+u4_As h_D#DO.Jlt 2Р 3ȴ)2*(N7(0.p d_s%!{jGCg 2P4#C 9LCM~sf8i;ֆON#\hȒJh[ȿҹb!GuBF!/n pv6WG&vEwmkyܮ¡a\W}:ʁ ><l>Hlj7qZ:^!ai)f5ךskMk(bTI "ߨA0u^#dF; aY87]Xbs)J)kM,!NՈo; ~ldV)Wړ[x1$v#RWHTw]rTeG"ZHS)o" qeaU 4{ kA.T_]X\z l(V?wM#RA=LϦ+ AY}i"yC=mSOJЛKXVR"󢾇:2\NJZtiBZZq@@3)ԕW2 - /א(V bJLBЩ7LS*4*=_76fHnFdfF[=q~UCtmj4t|t:L t*y#wᾶ1i/Krꮶ,eOcu -F{pدp_ٽ5H|+5Ve.^C&C)̋Si} ȝh6:g)<<ك.O\ߋCTzKA|+/>u!<0Wh?؞YrD =>/5\E|שּmĻ](P9ʊ'x`io~NkY|-*EK;&ixqk[GП#/m0=g x*S^MCfeɨkep7\"$mq[=*cȅlsUp.jY ԫ>OIPOɏfEDʹ>B.K`?ozQf!&Α O'=.k1UIbsB. 16Y2Aď^&KF h56V[$EzeZe!.uR$]ERl AXI #ý yڝJ Oe 91SR{xYڷq0Q<5IRr?K0}a~,b.$D{k^xZVGw[k:5cF0F f( b%A 3a`1 Q0J0F f( b%A 3a`1 Q0J0F f( b%A 3a`1 Q0J0F f( b%A 3a %Sh63&2Q6ș 0(X0J0F f( b%A 3aN0-iEyÝVa[uZal1_?q>;0u\p9}%Xou 96;'ۀ)F&Do8ԗ98IENDB`golang-github-gdamore-tcell.v3-3.4.0+dfsg/logos/tidelift.png000066400000000000000000000044351520475227200237010ustar00rootroot00000000000000PNG  IHDR*s IDATxm8єTK}1\\Jp :U+"L;]",FT^ax?k5Q1Y\Z3~7QcyTt@P]u>%k*}C:@ T:\}1PtyvbtTR`gP*l*ߦ*V5oJQ[g @m 7I, JQ;Lh *hBT ?/MJ@%P T@*J@%P T*J2!*J@%P T* T@%P T*ʄ* T@T \TToJߔJkIq1v<)S T ?XoP@*j2iTiba*P)Ry>\OdI2K@~s:'kv@&Pǵ|tWKYb4TwtJ T@* T*P T*P T@*P TA@*P T@*P T@*P T@%P T@* T@* T*P T*P T@*P T@*P T@*P T@*P T@*P T2A*P T@*P T@*P T@*P T~lh63JkXp@*P9Tm؂ TjX T.c s>;@¡~xLWA"*Pb2Ri8@V|`]<*PGrޏU *P)?TRP%}V[O vAu4#@Peu7J: S# d?V'PJAeQ&RE-_G=TUԷpf|mUPʣ<*PQ,a T- A*P9 SP | goPh) @U,T{͵܃ TA(LyGo@r&[T*ָsm[P4ܣ7=@UTYP*WԱo#*I$J$ T*IR6Nɵ4΁>9}&O Tzҏ gfP:& PRQDjHf Td?9@5n T*/NP\SP<~nPM-TPM1P TC55/R7 5C;TsTу QbGo@؏w,<*> *Go@Ga@ f?\2T}+PiEE\Ts{sTz^\ TCCғ€PiE}~TPy{(0rT TހJ=yaG[J7T nUP9 'P9z*-vTހJw?z\PPiѣ0rTi/6rT*uecOnN{dM.B"o;91F Ye}3AUþ!Zn3m?-ӿsJ(P9 ݏcט{1mnC&IENDB`golang-github-gdamore-tcell.v3-3.4.0+dfsg/logos/ukraine.svg000066400000000000000000000010611520475227200235360ustar00rootroot00000000000000#StandWithUkraine: 🇺🇦 golang-github-gdamore-tcell.v3-3.4.0+dfsg/mouse.go000066400000000000000000000076611520475227200217270ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // EventMouse is a mouse event. It is sent on either mouse up or mouse down // events. It is also sent on mouse motion events - if the terminal supports // it. We make every effort to ensure that mouse release events are delivered. // Hence, click drag can be identified by a motion event with the mouse down, // without any intervening button release. On some terminals only the initiating // press and terminating release event will be delivered. // // Mouse wheel events, when reported, may appear on their own as individual // impulses; that is, there will normally not be a release event delivered // for mouse wheel movements. // // Most terminals cannot report the state of more than one button at a time -- // and some cannot report motion events unless a button is pressed. // // Applications can inspect the time between events to resolve double or // triple clicks. type EventMouse struct { EventTime btn ButtonMask mod ModMask x int y int } // Buttons returns the list of buttons that were pressed or wheel motions. func (ev *EventMouse) Buttons() ButtonMask { return ev.btn } // Modifiers returns a list of keyboard modifiers that were pressed // with the mouse button(s). func (ev *EventMouse) Modifiers() ModMask { return ev.mod } // Position returns the mouse position. The origin 0, 0 is at the upper // left corner. The unit is character cells unless the screen was started // with MousePixelEvents, in which case the unit is terminal pixels. func (ev *EventMouse) Position() (int, int) { return ev.x, ev.y } // NewEventMouse is used to create a new mouse event. Applications // shouldn't need to use this; its mostly for screen implementers. func NewEventMouse(x, y int, btn ButtonMask, mod ModMask) *EventMouse { ev := &EventMouse{x: x, y: y, btn: btn, mod: mod} ev.SetEventNow() return ev } // ButtonMask is a mask of mouse buttons and wheel events. Mouse button presses // are normally delivered as both press and release events. Mouse wheel events // are normally just single impulse events. Windows supports up to eight // separate buttons plus all four wheel directions, but XTerm can only support // mouse buttons 1-3 and wheel up/down. Its not unheard of for terminals // to support only one or two buttons (think Macs). Old terminals, and true // emulations (such as vt100) won't support mice at all, of course. type ButtonMask int16 // These are the actual button values. Note that tcell version 1.x reversed buttons // two and three on *nix based terminals. We use button 1 as the primary, and // button 2 as the secondary, and button 3 (which is often missing) as the middle. const ( Button1 ButtonMask = 1 << iota // Usually the left (primary) mouse button. Button2 // Usually the right (secondary) mouse button. Button3 // Usually the middle mouse button. Button4 // Often a side button (thumb/next). Button5 // Often a side button (thumb/prev). Button6 Button7 Button8 WheelUp // Wheel motion up/away from user. WheelDown // Wheel motion down/towards user. WheelLeft // Wheel motion to left. WheelRight // Wheel motion to right. ButtonNone ButtonMask = 0 // No button or wheel events. ButtonPrimary = Button1 ButtonSecondary = Button2 ButtonMiddle = Button3 ) golang-github-gdamore-tcell.v3-3.4.0+dfsg/mouse_test.go000066400000000000000000000272501520475227200227620ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "testing" "time" "github.com/gdamore/tcell/v3/vt" ) func TestMouseEventFields(t *testing.T) { now := time.Now() ev := NewEventMouse(4, 5, ButtonMiddle, ModShift) if ev.when.Before(now) { t.Errorf("time went backwards? %s", ev.when) } if time.Now().Before(ev.when) { t.Errorf("time also went backwards? %s", ev.when) } if ev.Buttons() != Button3 { t.Errorf("wrong buttons %v", ev.Buttons()) } if ev.Modifiers() != ModShift { t.Errorf("wrong modifiers %v", ev.Modifiers()) } if x, y := ev.Position(); x != 4 || y != 5 { t.Errorf("wrong position %d %d", x, y) } } func getMouseEvent(t *testing.T, s Screen) (*EventMouse, bool) { t.Helper() for { select { case ev := <-s.EventQ(): if me, ok := ev.(*EventMouse); ok { return me, true } t.Logf("Got different event: %T", ev) case <-time.After(time.Second): t.Fatal("timeout waiting for mouse event") return nil, false } } } func getFocusEvent(t *testing.T, s Screen) (*EventFocus, bool) { t.Helper() for { select { case ev := <-s.EventQ(): if fe, ok := ev.(*EventFocus); ok { return fe, true } t.Logf("Got different event: %T", ev) case <-time.After(time.Second): t.Fatal("timeout waiting for focus event") return nil, false } } } func TestMouseEvents(t *testing.T) { term, s := NewMockScreen(t) defer s.Fini() s.EnableMouse(MouseMotionEvents) time.Sleep(time.Millisecond * 10) // simple mouse click term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button1, Down: true, Motion: false, Mod: vt.ModNone, }) term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button1, Down: false, Motion: false, Mod: vt.ModNone, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button1 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("still got buttons %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.WheelUp, Down: true, Mod: vt.ModLAlt, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != WheelUp { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModAlt { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModAlt) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.WheelDown, Down: true, Mod: vt.ModRMeta | vt.ModRCtrl, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != WheelDown { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModCtrl|ModAlt { // NB: ModAlt used for Meta in mouse events t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModAlt|ModCtrl) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.WheelLeft, Down: true, Mod: vt.ModRShift, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != WheelLeft { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.WheelRight, Down: true, Mod: vt.ModLShift, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != WheelRight { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button2, Down: true, Mod: vt.ModLShift, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button2 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } // this will be a chord of buttons 2 and 3 term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button3, Down: true, Mod: vt.ModLShift, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button3|Button2 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button3, Down: false, Mod: vt.ModLShift, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button2 { t.Errorf("wrong buttons: %x", me.Buttons()) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button2, Down: false, Mod: vt.ModLShift, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("wrong buttons: %x", me.Buttons()) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.NoButton, Down: true, Mod: vt.ModLShift, Motion: true, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } // this simulates a certain kind of broken report term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button1, Down: false, Mod: vt.ModLShift, Motion: true, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } // send malformed mouse events (fuzz testing) term.SendRaw([]byte("\x1b[<3M")) term.SendRaw([]byte("\x1b[<3;2m")) select { case ev := <-s.EventQ(): t.Fatalf("Got unexpected event: %T", ev) case <-time.After(time.Millisecond * 50): } // Now try the upper buttons (uncommon) term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button4, Down: true, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button4 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button4, Down: false, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("wrong buttons: %x", me.Buttons()) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button5, Down: true, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button5 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } // chord with 5 term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button6, Down: true, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button6|Button5 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } // and now adding the final chord with button 7 term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button7, Down: true, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button5|Button6|Button7 { t.Errorf("wrong buttons: %x", me.Buttons()) } else if x, y := me.Position(); x != 2 || y != 3 { t.Errorf("wrong position %d,%d != 2,3", x, y) } else if me.Modifiers() != ModShift { t.Errorf("wrong modifiers %x != %x", me.Modifiers(), ModShift) } // and release them out of order term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button6, Down: false, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button5|Button7 { t.Errorf("wrong buttons: %x", me.Buttons()) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button5, Down: false, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != Button7 { t.Errorf("wrong buttons: %x", me.Buttons()) } term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button7, Down: false, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("wrong buttons: %x", me.Buttons()) } // Spurious release event term.MouseEvent(vt.MouseEvent{ Position: vt.Coord{X: 2, Y: 3}, Button: vt.Button7, Down: false, Mod: vt.ModRShift, Motion: false, }) if me, ok := getMouseEvent(t, s); !ok { return } else if me.Buttons() != ButtonNone { t.Errorf("wrong buttons: %x", me.Buttons()) } } func TestFocusEvents(t *testing.T) { term, s := NewMockScreen(t) defer s.Fini() s.EnableFocus() time.Sleep(time.Millisecond * 10) term.FocusEvent(true) if ev, ok := getFocusEvent(t, s); !ok { return } else if !ev.Focused { t.Error("focused was not set") } term.FocusEvent(false) if ev, ok := getFocusEvent(t, s); !ok { return } else if ev.Focused { t.Error("focused was set") } term.FocusEvent(true) if ev, ok := getFocusEvent(t, s); !ok { return } else if !ev.Focused { t.Error("focused was not set") } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/paste.go000066400000000000000000000036761520475227200217150ustar00rootroot00000000000000// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "time" ) // EventPaste is used to mark the start and end of a bracketed paste. // // An event with .Start() true will be sent to mark the start of a bracketed paste, // followed by a number of keys (string data) for the content, ending with the // an event with .End() true. type EventPaste struct { start bool t time.Time } // When returns the time when this EventPaste was created. func (ev *EventPaste) When() time.Time { return ev.t } // Start returns true if this is the start of a paste. func (ev *EventPaste) Start() bool { return ev.start } // End returns true if this is the end of a paste. func (ev *EventPaste) End() bool { return !ev.start } // NewEventPaste returns a new EventPaste. func NewEventPaste(start bool) *EventPaste { return &EventPaste{t: time.Now(), start: start} } // NewEventClipboard returns a new NewEventClipboard with a data payload func NewEventClipboard(data []byte) *EventClipboard { return &EventClipboard{t: time.Now(), data: data} } // EventClipboard represents data from the clipboard, // in response to a GetClipboard request. type EventClipboard struct { t time.Time data []byte } // Data returns the attached binary data. func (ev *EventClipboard) Data() []byte { return ev.data } // When returns the time when this event was created. func (ev *EventClipboard) When() time.Time { return ev.t } golang-github-gdamore-tcell.v3-3.4.0+dfsg/resize.go000066400000000000000000000026641520475227200220760ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import "github.com/gdamore/tcell/v3/tty" // EventResize is sent when the window size changes. type EventResize struct { EventTime ws WindowSize } // NewEventResize creates an EventResize with the new updated window size, // which is given in character cells. func NewEventResize(width, height int) *EventResize { ws := WindowSize{ Width: width, Height: height, } ev := &EventResize{ws: ws} ev.SetEventNow() return ev } // Size returns the new window size as width, height in character cells. func (ev *EventResize) Size() (int, int) { return ev.ws.Width, ev.ws.Height } // PixelSize returns the new window size as width, height in pixels. The size // will be 0,0 if the screen doesn't support this feature func (ev *EventResize) PixelSize() (int, int) { return ev.ws.PixelWidth, ev.ws.PixelHeight } type WindowSize = tty.WindowSize golang-github-gdamore-tcell.v3-3.4.0+dfsg/resize_test.go000066400000000000000000000030311520475227200231220ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "testing" "time" "github.com/gdamore/tcell/v3/vt" ) func TestPixelSize(t *testing.T) { // simulate 8x16 font, VGA (640x480) ev := NewEventResize(80, 25) ev.ws.PixelHeight = ev.ws.Height * 16 ev.ws.PixelWidth = ev.ws.Width * 8 if w, h := ev.PixelSize(); w != 640 || h != 400 { t.Errorf("pixelsize wrong: %d %d", w, h) } } // TestEventResize creates a screen and does some resizing to prove it works. func TestEventResize(t *testing.T) { mt, ms := NewMockScreen(t) defer ms.Fini() // drain the eventQ loop: for { select { case <-ms.EventQ(): default: break loop } } mt.SetSize(vt.Coord{X: 50, Y: 132}) select { case ev := <-ms.EventQ(): if re, ok := ev.(*EventResize); ok { if y, x := re.Size(); y != 50 || x != 132 { t.Errorf("wrong size: %d %d", y, x) } } else { t.Errorf("wrong event type %T", ev) } case <-time.After(time.Millisecond * 100): t.Errorf("never got resize signal") } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/runes.go000066400000000000000000000065051520475227200217270ustar00rootroot00000000000000// Copyright 2015 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // The names of these constants are chosen to match Terminfo names, // modulo case, and changing the prefix from ACS_ to Rune. These are // the runes we provide extra special handling for, with ASCII fallbacks // for terminals that lack them. const ( RuneSterling = '£' RuneDArrow = '↓' RuneLArrow = '←' RuneRArrow = '→' RuneUArrow = '↑' RuneBullet = '·' RuneBoard = '░' RuneCkBoard = '▒' RuneDegree = '°' RuneDiamond = '◆' RuneGEqual = '≥' RunePi = 'π' RuneHLine = '─' RuneLantern = '§' RunePlus = '┼' RuneLEqual = '≤' RuneLLCorner = '└' RuneLRCorner = '┘' RuneNEqual = '≠' RunePlMinus = '±' RuneS1 = '⎺' RuneS3 = '⎻' RuneS7 = '⎼' RuneS9 = '⎽' RuneBlock = '█' RuneTTee = '┬' RuneRTee = '┤' RuneLTee = '├' RuneBTee = '┴' RuneULCorner = '┌' RuneURCorner = '┐' RuneVLine = '│' ) // RuneFallbacks is the default map of fallback strings that will be // used to replace a rune when no other more appropriate transformation // is available, and the rune cannot be displayed directly. // // New entries may be added to this map over time, as it becomes clear // that such is desirable. Characters that represent either letters or // numbers should not be added to this list unless it is certain that // the meaning will still convey unambiguously. // // As an example, it would be appropriate to add an ASCII mapping for // the full width form of the letter 'A', but it would not be appropriate // to do so a glyph representing the country China. // // Programs that desire richer fallbacks may register additional ones, // or change or even remove these mappings with Screen.RegisterRuneFallback // Screen.UnregisterRuneFallback methods. // // Note that Unicode is presumed to be able to display all glyphs. // This is a pretty poor assumption, but there is no easy way to // figure out which glyphs are supported in a given font. Hence, // some care in selecting the characters you support in your application // is still appropriate. var RuneFallbacks = map[rune]string{ RuneSterling: "f", RuneDArrow: "v", RuneLArrow: "<", RuneRArrow: ">", RuneUArrow: "^", RuneBullet: "o", RuneBoard: "#", RuneCkBoard: ":", RuneDegree: "\\", RuneDiamond: "+", RuneGEqual: ">", RunePi: "*", RuneHLine: "-", RuneLantern: "#", RunePlus: "+", RuneLEqual: "<", RuneLLCorner: "+", RuneLRCorner: "+", RuneNEqual: "!", RunePlMinus: "#", RuneS1: "~", RuneS3: "-", RuneS7: "-", RuneS9: "_", RuneBlock: "#", RuneTTee: "+", RuneRTee: "+", RuneLTee: "+", RuneBTee: "+", RuneULCorner: "+", RuneURCorner: "+", RuneVLine: "|", } golang-github-gdamore-tcell.v3-3.4.0+dfsg/screen.go000066400000000000000000000402641520475227200220520ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "sync" "github.com/gdamore/tcell/v3/color" ) // Screen represents the physical (or emulated) screen. // This can be a terminal window or a physical console. Platforms implement // this differently. type Screen interface { // Init initializes the screen for use. Init() error // Fini finalizes the screen also releasing resources. Fini() // Clear logically erases the screen. // This is effectively a short-cut for Fill(' ', StyleDefault). Clear() // Fill fills the screen with the given character and style. // The effect of filling the screen is not visible until Show // is called (or Sync). Fill(rune, Style) // Put writes the first grapheme of the given string with th // given style at the given coordinates. (Only the first grapheme // occupying either one or two cells is stored.) It returns the // remainder of the string, and the width displayed. Put(x int, y int, str string, style Style) (string, int) // PutStr writes a string starting at the given position, using the // default style. The content is clipped to the screen dimensions. PutStr(x int, y int, str string) // PutStrStyled writes a string starting at the given position, using // the given style. The content is clipped to the screen dimensions. PutStrStyled(x int, y int, str string, style Style) // Get the contents at the given location. If the // coordinates are out of range, then the values will be 0, nil, // StyleDefault. Note that the contents returned are logical contents // and may not actually be what is displayed, but rather are what will // be displayed if Show() or Sync() is called. The width is the width // in screen cells; most often this will be 1, but some East Asian // characters and emoji require two cells. Get(x, y int) (str string, style Style, width int) // SetContent sets the contents of the given cell location. If // the coordinates are out of range, then the operation is ignored. // // The first rune is the primary non-zero width rune. The array // that follows is a possible list of combining characters to append, // and will usually be nil (no combining characters.) // // The results are not displayed until Show() or Sync() is called. // // Note that wide (East Asian full width and emoji) runes occupy two cells, // and attempts to place character at next cell to the right will have // undefined effects. Wide runes that are printed in the // last column will be replaced with a single width space on output. SetContent(x int, y int, primary rune, combining []rune, style Style) // SetStyle sets the default style to use when clearing the screen // or when StyleDefault is specified. If it is also StyleDefault, // then whatever system/terminal default is relevant will be used. SetStyle(style Style) // ShowCursor is used to display the cursor at a given location. // If the coordinates -1, -1 are given or are otherwise outside the // dimensions of the screen, the cursor will be hidden. ShowCursor(x int, y int) // HideCursor is used to hide the cursor. It's an alias for // ShowCursor(-1, -1).sim HideCursor() // SetCursorStyle is used to set the cursor style. If the style // is not supported (or cursor styles are not supported at all), // then this will have no effect. Color will be changed if supplied, // and the terminal supports doing so. SetCursorStyle(CursorStyle, ...color.Color) // Size returns the screen size as width, height. This changes in // response to a call to Clear or Flush. Size() (width, height int) // EventQ returns the channel of events, and is usable just like // any other channel. Events can be injected by writing to // the channel, and they can be read by reading from it. The // channel will remain open until the screen is completely shut down // with Fini(). Consequently, applications must not write to this // channel after Fini() is called. EventQ() chan Event // EnableMouse enables the mouse. (If your terminal supports it.) // If no flags are specified, then all events are reported, if the // terminal supports them. EnableMouse(...MouseFlags) // DisableMouse disables the mouse. DisableMouse() // EnablePaste enables bracketed paste mode, if supported. EnablePaste() // DisablePaste disables bracketed paste mode. DisablePaste() // EnableFocus enables reporting of focus events, if your terminal supports it. EnableFocus() // DisableFocus disables reporting of focus events. DisableFocus() // Colors returns the number of colors. All colors are assumed to // use the ANSI color map. If a terminal is monochrome, it will // return 0. Colors() int // Show makes all the content changes made using SetContent() visible // on the display. // // It does so in the most efficient and least visually disruptive // manner possible. Show() // Sync works like Show(), but it updates every visible cell on the // physical display, assuming that it is not synchronized with any // internal model. This may be both expensive and visually jarring, // so it should only be used when believed to actually be necessary. // // Typically, this is called as a result of a user-requested redraw // (e.g. to clear up on-screen corruption caused by some other program), // or during a resize event. Sync() // CharacterSet returns information about the character set. // This isn't the full locale, but it does give us the input/output // character set. Note that this is just for diagnostic purposes, // we normally translate input/output to/from UTF-8, regardless of // what the user's environment is. CharacterSet() string // RegisterRuneFallback adds a fallback for runes that are not // part of the character set -- for example one could register // o as a fallback for ø. This should be done cautiously for // characters that might be displayed ordinarily in language // specific text -- characters that could change the meaning of // written text would be dangerous. The intention here is to // facilitate fallback characters in pseudo-graphical applications. // // If the terminal has fallbacks already in place via an alternate // character set, those are used in preference. Also, standard // fallbacks for graphical characters in the alternate character set // terminfo string are registered implicitly. // // The display string should be the same width as original rune. // This makes it possible to register two character replacements // for full width East Asian characters, for example. // // It is recommended that replacement strings consist only of // 7-bit ASCII, since other characters may not display everywhere. RegisterRuneFallback(r rune, subst string) // UnregisterRuneFallback unmaps a replacement. It will unmap // the implicit ASCII replacements for alternate characters as well. // When an unmapped char needs to be displayed, but no suitable // glyph is available, '?' is emitted instead. It is not possible // to "disable" the use of alternate characters that are supported // by your terminal except by changing the terminal database. UnregisterRuneFallback(r rune) // Resize does nothing, since it's generally not possible to // ask a screen to resize, but it allows the Screen to implement // the View interface. Resize(int, int, int, int) // Suspend pauses input and output processing. It also restores the // terminal settings to what they were when the application started. // This can be used to, for example, run a sub-shell. Suspend() error // Resume resumes after Suspend(). Resume() error // Beep attempts to sound an OS-dependent audible alert and returns an error // when unsuccessful. Beep() error // SetSize attempts to resize the window. It also invalidates the cells and // calls the resize function. Note that if the window size is changed, it will // not be restored upon application exit. // // Many terminals cannot support this. Perversely, the "modern" Windows Terminal // does not support application-initiated resizing, whereas the legacy terminal does. // Also, some emulators can support this but may have it disabled by default. SetSize(int, int) // LockRegion sets or unsets a lock on a region of cells. A lock on a // cell prevents the cell from being redrawn. LockRegion(x, y, width, height int, lock bool) // Tty returns the underlying Tty. If the screen is not a terminal, the // returned bool will be false Tty() (Tty, bool) // SetTitle sets a window title on the screen. // Terminals may be configured to ignore this, or unable to. // Tcell may attempt to save and restore the window title on entry and exit, but // the results may vary. Use of unicode characters may not be supported. SetTitle(string) // SetClipboard is used to post arbitrary data to the system clipboard. // This need not be UTF-8 string data. It's up to the recipient to decode the // data meaningfully. Terminals may prevent this for security reasons. // An empty byte or nil can be used to clear the clipboard. SetClipboard([]byte) // GetClipboard is used to request the clipboard contents. It may be ignored. // If the terminal is willing, it will be post the clipboard contents using an // EventPaste with the clipboard content as the Data() field. Terminals may // prevent this for security reasons. GetClipboard() // HasClipboard is true if the screen claims to support the clipboard. // Note that GetClipboard may still not work, but SetClipboard should be functional. // Note that many terminals that support the clipboard don't actually report that they // do, so a false indication is not necessarily conclusive. HasClipboard() bool // ShowNotification is used to show a desktop notification, when the terminal // supports it. Right now only terminals supporting OSC 777 support this. ShowNotification(title string, body string) // KeyboardProtocol returns the keyboard protocol currently in use. KeyboardProtocol() KeyProtocol // Terminal returns the terminal name and version if known. If either of these // are unknown, then empty strings are returned in their place. This is intended // to facilitate debug, and also applications that wish to enable very specific // behaviors for the terminal Terminal() (string, string) } var overrideScreen chan Screen var overrideOnce sync.Once // NewScreen returns a default Screen suitable for the user's terminal environment. // Any options are passed through to NewTerminfoScreen. func NewScreen(opts ...TerminfoScreenOption) (Screen, error) { // Allow an application (presumably test code) to inject a replacement default // screen. This could also be used to create shims for things like nesting screens. select { case s := <-overrideScreen: return s, nil default: } if s, e := NewTerminfoScreen(opts...); s != nil { return s, nil } else { return nil, e } } // ShimScreen allows an application to override the screen that will // be returned by NewScreen. Typically this is used for testing, // where the test code calls this once before running an example. // It could also be used to intercept a regular Screen. func ShimScreen(s Screen) { overrideOnce.Do(func() { overrideScreen = make(chan Screen, 8) // normally would only be one anyway }) overrideScreen <- s } // MouseFlags are options to modify the handling of mouse events. // Actual events can be ORed together. type MouseFlags int const ( MouseButtonEvents = MouseFlags(1) // Click events only MouseDragEvents = MouseFlags(2) // Click-drag events (includes button events) MouseMotionEvents = MouseFlags(4) // All mouse events (includes click and drag events) // MousePixelEvents requests that mouse coordinates be reported in // terminal pixels rather than character cells (xterm SGR-Pixel mode, // CSI ?1016h). It is a modifier on the other mouse flags: at least one // of MouseButtonEvents, MouseDragEvents, or MouseMotionEvents must also // be set for any events to be delivered. When this mode is active, // EventMouse.Position() returns coordinates in pixels; the application // is responsible for mapping those to its own grid (e.g. via the // terminal's reported cell-pixel size). MousePixelEvents = MouseFlags(8) ) // CursorStyle represents a given cursor style, which can include the shape and // whether the cursor blinks or is solid. Support for changing this is not universal. type CursorStyle int const ( CursorStyleDefault = CursorStyle(iota) // The default CursorStyleBlinkingBlock CursorStyleSteadyBlock CursorStyleBlinkingUnderline CursorStyleSteadyUnderline CursorStyleBlinkingBar CursorStyleSteadyBar ) // screenImpl is a subset of Screen that can be used with baseScreen to formulate // a complete implementation of Screen. See Screen for doc comments about methods. type screenImpl interface { Init() error Fini() SetStyle(style Style) ShowCursor(x int, y int) HideCursor() SetCursor(CursorStyle, color.Color) Size() (width, height int) EnableMouse(...MouseFlags) DisableMouse() EnablePaste() DisablePaste() EnableFocus() DisableFocus() Colors() int Show() Sync() CharacterSet() string RegisterRuneFallback(r rune, subst string) UnregisterRuneFallback(r rune) Resize(int, int, int, int) Suspend() error Resume() error Beep() error SetSize(int, int) SetTitle(string) Tty() (Tty, bool) SetClipboard([]byte) GetClipboard() HasClipboard() bool ShowNotification(string, string) KeyboardProtocol() KeyProtocol Terminal() (string, string) // Following methods are not part of the Screen api, but are used for interaction with // the common layer code. // Locker locks the underlying data structures so that we can access them // in a thread-safe way. sync.Locker // GetCells returns a pointer to the underlying CellBuffer that the implementation uses. // Various methods will write to these for performance, but will use the lock to do so. GetCells() *CellBuffer // StopQ is closed when the screen is shut down via Fini. It remains open if the screen // is merely suspended. StopQ() <-chan struct{} // EventQ delivers events. Events are posted to this by the screen in response to // key presses, resizes, etc. Application code receives events from this via the // Screen.PollEvent, Screen.ChannelEvents APIs. EventQ() chan Event } type baseScreen struct { screenImpl } func (b *baseScreen) Put(x int, y int, str string, style Style) (remain string, width int) { cells := b.GetCells() b.Lock() defer b.Unlock() return cells.Put(x, y, str, style) } func (b *baseScreen) PutStrStyled(x int, y int, str string, style Style) { cells := b.GetCells() b.Lock() defer b.Unlock() cols, rows := cells.Size() if cells.sanitizeContent { str = stripOSCControlsIfNeeded(str) } width := 0 for str != "" && x < cols && y < rows { str, width = cells.put(x, y, str, style) if width == 0 { break } x += width } } func (b *baseScreen) PutStr(x, y int, str string) { b.PutStrStyled(x, y, str, StyleDefault) } func (b *baseScreen) Clear() { b.Fill(' ', StyleDefault) } func (b *baseScreen) Fill(r rune, style Style) { cb := b.GetCells() b.Lock() cb.Fill(r, style) b.Unlock() } func (b *baseScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) { b.Put(x, y, string(append([]rune{mainc}, combc...)), style) } func (b *baseScreen) Get(x, y int) (string, Style, int) { cells := b.GetCells() b.Lock() defer b.Unlock() return cells.Get(x, y) } func (b *baseScreen) LockRegion(x, y, width, height int, lock bool) { cells := b.GetCells() b.Lock() for j := y; j < (y + height); j += 1 { for i := x; i < (x + width); i += 1 { switch lock { case true: cells.LockCell(i, j) case false: cells.UnlockCell(i, j) } } } b.Unlock() } func (b *baseScreen) SetCursorStyle(cs CursorStyle, ccs ...color.Color) { if len(ccs) > 0 { b.SetCursor(cs, ccs[0]) } else { b.SetCursor(cs, ColorNone) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/style.go000066400000000000000000000207351520475227200217340ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "strings" "unicode/utf8" "github.com/gdamore/tcell/v3/color" ) // Style represents a complete text style, including both foreground color, // background color, and additional attributes such as "bold" or "underline". // // Note that not all terminals can display all colors or attributes, and // many might have specific incompatibilities between specific attributes // and color combinations. // // To use Style, just declare a variable of its type. type Style struct { fg color.Color bg color.Color ulColor color.Color attrs AttrMask ulStyle UnderlineStyle url *urlInfo } type urlInfo struct { url string id string } // stripOSCControls removes control bytes that can terminate OSC payloads early. func stripOSCControls(s string) string { var b strings.Builder b.Grow(len(s)) for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]) if r == utf8.RuneError && size == 1 { c := s[i] if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) { i++ continue } _ = b.WriteByte(c) i++ continue } if r <= 0x1f || r == 0x7f || (r >= 0x80 && r <= 0x9f) { i += size continue } b.WriteString(s[i : i+size]) i += size } return b.String() } // stripOSCControlsIfNeeded returns the original string when it contains no // control bytes and only allocates when stripping is required. func stripOSCControlsIfNeeded(s string) string { for i := 0; i < len(s); i++ { c := s[i] if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) { return stripOSCControls(s) } } return s } // StyleDefault represents a default style, based upon the context. // It is the zero value. var StyleDefault Style // styleInvalid is just an arbitrary invalid style used internally. var styleInvalid = Style{attrs: AttrInvalid} // Foreground returns a new style based on s, with the foreground color set // as requested. ColorDefault can be used to select the global default. func (s Style) Foreground(c color.Color) Style { s2 := s s2.fg = c return s2 } // Background returns a new style based on s, with the background color set // as requested. ColorDefault can be used to select the global default. func (s Style) Background(c color.Color) Style { s2 := s s2.bg = c return s2 } func (s Style) setAttrs(attrs AttrMask, on bool) Style { s2 := s if on { s2.attrs |= attrs } else { s2.attrs &^= attrs } return s2 } // Normal returns the style with all attributes disabled. // Colors are preserved, as are hyperlinks. (Underline color // will also be preserved, but no underline is currently shown. // Apart from color, the underline style is reset as well.) func (s Style) Normal() Style { return Style{ fg: s.fg, bg: s.bg, ulColor: s.ulColor, url: s.url, } } // Bold returns a new style based on s, with the bold attribute set // as requested. func (s Style) Bold(on bool) Style { return s.setAttrs(AttrBold, on) } // Blink returns a new style based on s, with the blink attribute set // as requested. func (s Style) Blink(on bool) Style { return s.setAttrs(AttrBlink, on) } // Dim returns a new style based on s, with the dim attribute set // as requested. func (s Style) Dim(on bool) Style { return s.setAttrs(AttrDim, on) } // Italic returns a new style based on s, with the italic attribute set // as requested. func (s Style) Italic(on bool) Style { return s.setAttrs(AttrItalic, on) } // Reverse returns a new style based on s, with the reverse attribute set // as requested. (Reverse usually changes the foreground and background // colors.) func (s Style) Reverse(on bool) Style { return s.setAttrs(AttrReverse, on) } // StrikeThrough sets strike-through mode. func (s Style) StrikeThrough(on bool) Style { return s.setAttrs(AttrStrikeThrough, on) } // Underline style. Modern terminals have the option of rendering the // underline using different styles, and even different colors. type UnderlineStyle uint8 const ( UnderlineStyleNone = UnderlineStyle(iota) UnderlineStyleSolid UnderlineStyleDouble UnderlineStyleCurly UnderlineStyleDotted UnderlineStyleDashed ) // Underline returns a new style based on s, with the underline attribute set // as requested. The parameters can be: // // bool: on / off - enables just a simple underline // UnderlineStyle: sets a specific style (should not coexist with the bool) // Color: the color to use func (s Style) Underline(params ...any) Style { s2 := s for _, param := range params { switch v := param.(type) { case bool: if v { s2.ulStyle = UnderlineStyleSolid } else { s2.ulStyle = UnderlineStyleNone } case UnderlineStyle: s2.ulStyle = v case Color: s2.ulColor = v default: panic("Bad type for underline") } } return s2 } // GetForeground returns the foreground (text) color. func (s Style) GetForeground() color.Color { return s.fg } // GetBackground returns the background color. func (s Style) GetBackground() color.Color { return s.bg } // GetUnderlineStyle returns the underline style for the style. func (s Style) GetUnderlineStyle() UnderlineStyle { return s.ulStyle } // GetUnderlineColor returns the underline color for the style. func (s Style) GetUnderlineColor() color.Color { return s.ulColor } // Attributes returns a new style based on s, with its attributes set as // specified. // // Deprecated: Use direct functions instead. func (s Style) Attributes(attrs AttrMask) Style { s2 := s s2.attrs = attrs return s2 } // GetAttributes gets the attributes for a style. // Deprecated: Use individual properties instead. func (s Style) GetAttributes() AttrMask { return s.attrs } // Url returns a style with the Url set. If the provided Url is not empty, // and the terminal supports it, text will typically be marked up as a clickable // link to that Url. If the Url is empty, then this mode is turned off. func (s Style) Url(url string) Style { s2 := s s2.url = &urlInfo{url: stripOSCControlsIfNeeded(url)} if s.url != nil { s2.url.id = s.url.id } if s2.url.url == "" && s2.url.id == "" { s2.url = nil } return s2 } // UrlId returns a style with the UrlId set. If the provided UrlId is not empty, // any marked up Url with this style will be given the UrlId also. If the // terminal supports it, any text with the same UrlId will be grouped as if it // were one Url, even if it spans multiple lines. func (s Style) UrlId(id string) Style { s2 := s s2.url = &urlInfo{} if id = stripOSCControlsIfNeeded(id); id != "" { s2.url.id = "id=" + id } if s.url != nil { s2.url.url = s.url.url } if s2.url.url == "" && s2.url.id == "" { s2.url = nil } return s2 } // GetUrl returns the URL (id and actual URL) associated with the style. // This is a hyper link that will be used for cells marked up with this style. func (s Style) GetUrl() (id string, url string) { if s.url != nil { return strings.TrimPrefix(s.url.id, "id="), s.url.url } return "", "" } // HasBold returns true if the style indicates bold text. // Note that on some terminals bold text is simply brighter. func (s Style) HasBold() bool { return s.attrs&AttrBold != 0 } // HasBlink returns true if the style indicates blinking text. func (s Style) HasBlink() bool { return s.attrs&AttrBlink != 0 } // HasReverse returns true if the style indicates reverse video text. func (s Style) HasReverse() bool { return s.attrs&AttrReverse != 0 } // HasItalic returns true if the style indicates italicized text. func (s Style) HasItalic() bool { return s.attrs&AttrItalic != 0 } // HasDim returns true if the style indicates dim or faint text. func (s Style) HasDim() bool { return s.attrs&AttrDim != 0 } // HasStrikeThrough returns true if the style indicates crossed-out text. func (s Style) HasStrikeThrough() bool { return s.attrs&AttrStrikeThrough != 0 } // HasUnderline returns true if any underline style is set. // Note that more detail is available via the GetUnderlineStyle // and GetUnderlineColor methods. func (s Style) HasUnderline() bool { return s.ulStyle != UnderlineStyleNone } golang-github-gdamore-tcell.v3-3.4.0+dfsg/style_test.go000066400000000000000000000143571520475227200227760ustar00rootroot00000000000000// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "testing" "unicode/utf8" "github.com/gdamore/tcell/v3/color" ) func TestStyle(t *testing.T) { _, s := NewMockScreen(t) defer s.Fini() style := StyleDefault fg, bg, attr := style.fg, style.bg, style.attrs if fg != ColorDefault || bg != ColorDefault || attr != AttrNone { t.Errorf("Bad default style (%v, %v, %v)", fg, bg, attr) } s2 := style. Background(ColorRed). Foreground(ColorBlue). Blink(true) fg, bg, attr = s2.fg, s2.bg, s2.attrs if fg != ColorBlue || bg != ColorRed || attr != AttrBlink { t.Errorf("Bad custom style (%v, %v, %v)", fg, bg, attr) } if !s2.HasBlink() { t.Errorf("blink is false") } if s2.HasItalic() { t.Errorf("italic is true") } if s2.HasDim() { t.Errorf("dim is true") } if s2.HasBold() { t.Errorf("bold is true") } if s2.HasUnderline() { t.Errorf("underline is true") } if s2.HasReverse() { t.Errorf("reverse is true") } if s2.HasStrikeThrough() { t.Errorf("strike-through is true") } if id, url := s2.GetUrl(); id != "" || url != "" { t.Errorf("url not empty: %q %q", id, url) } if s2.GetAttributes() != AttrBlink { t.Errorf("wrong attrs %x", s2.GetAttributes()) } if s2.GetBackground() != color.Red || s2.GetForeground() != color.Blue { t.Errorf("wrong colors %s %s", s2.GetForeground().String(), s2.GetBackground().String()) } us := s2.Url("http://example.com") if id, url := us.GetUrl(); id != "" || url != "http://example.com" { t.Errorf("url wrong: %q %q", id, url) } us = us.Url("http://example.com").UrlId("someId") if id, url := us.GetUrl(); id != "someId" || url != "http://example.com" { t.Errorf("url wrong: %q %q", id, url) } us = us.Underline(UnderlineStyleDotted, color.Pink) if us.GetUnderlineColor() != color.Pink { t.Errorf("underline color wrong: %q", us.GetUnderlineColor().String()) } if us.GetUnderlineStyle() != UnderlineStyleDotted { t.Errorf("wrong underline style: %v", us.GetUnderlineStyle()) } us = us.Url("") if id, url := us.GetUrl(); id != "someId" || url != "" { t.Errorf("url clear should preserve id state: %q %q", id, url) } us = us.Url("http://example.net") if id, url := us.GetUrl(); id != "someId" || url != "http://example.net" { t.Errorf("url id not preserved across clear: %q %q", id, url) } us = us.UrlId("") if id, url := us.GetUrl(); id != "" || url != "http://example.net" { t.Errorf("url id clear did not preserve url: %q %q", id, url) } orphan := StyleDefault.UrlId("orphan") if id, url := orphan.GetUrl(); id != "orphan" || url != "" { t.Errorf("url id without url should be retained: %q %q", id, url) } orphan = orphan.Url("http://example.org") if id, url := orphan.GetUrl(); id != "orphan" || url != "http://example.org" { t.Errorf("url id set before url was not preserved: %q %q", id, url) } if id, url := StyleDefault.Url("").GetUrl(); id != "" || url != "" { t.Errorf("empty url should leave no hyperlink state: %q %q", id, url) } if id, url := StyleDefault.UrlId("").GetUrl(); id != "" || url != "" { t.Errorf("empty url id should leave no hyperlink state: %q %q", id, url) } us = us.Normal().Reverse(true).Italic(false) if us.GetAttributes() != AttrReverse { t.Errorf("wrong attributes: %v", us.GetAttributes()) } } func TestStyleUrlStripsOSCControls(t *testing.T) { s := StyleDefault. Url("http://exa\x07mple.com/\x1b\\path"). UrlId("id\x00\x1f\x7f\x80\x9fend") id, url := s.GetUrl() combined := id + url for i := 0; i < len(combined); i++ { c := combined[i] if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) { t.Fatalf("control characters survived sanitization: id=%q url=%q", id, url) } } if id != "idend" { t.Fatalf("unexpected sanitized id: %q", id) } if url != "http://example.com/\\path" { t.Fatalf("unexpected sanitized url: %q", url) } s = StyleDefault. Url("http://example.com/\u3042\u009b"). UrlId("id\u3042\u009bend") id, url = s.GetUrl() combined = id + url if !utf8.ValidString(combined) { t.Fatalf("UTF-8 was corrupted during sanitization: id=%q url=%q", id, url) } for _, r := range combined { if r <= 0x1f || r == 0x7f || (r >= 0x80 && r <= 0x9f) { t.Fatalf("control characters survived UTF-8 sanitization: id=%q url=%q", id, url) } } if id != "id\u3042end" { t.Fatalf("unexpected sanitized UTF-8 id: %q", id) } if url != "http://example.com/\u3042" { t.Fatalf("unexpected sanitized UTF-8 url: %q", url) } } func TestStripOSCControlsIfNeeded(t *testing.T) { if got := stripOSCControlsIfNeeded("hello world"); got != "hello world" { t.Fatalf("clean string changed: %q", got) } if got := stripOSCControlsIfNeeded("he\x07llo\x1bworld"); got != "helloworld" { t.Fatalf("dirty string not stripped correctly: %q", got) } } func TestStripOSCControlsInvalidUTF8(t *testing.T) { got := stripOSCControls(string([]byte{0xff, 0x1b, 0xfe, 'A'})) if got != string([]byte{0xff, 0xfe, 'A'}) { t.Fatalf("unexpected invalid-UTF8 sanitization result: %q", got) } } func TestStyleHelperMethods(t *testing.T) { s := StyleDefault. Dim(true). StrikeThrough(true). Underline(true) if !s.HasDim() { t.Fatalf("Dim(true) should set dim") } if !s.HasStrikeThrough() { t.Fatalf("StrikeThrough(true) should set strike-through") } if !s.HasUnderline() { t.Fatalf("Underline(true) should set underline") } s = s.Underline(false) if s.HasUnderline() { t.Fatalf("Underline(false) should clear underline") } s = s.Attributes(AttrBold | AttrItalic) if got := s.GetAttributes(); got != AttrBold|AttrItalic { t.Fatalf("unexpected attributes: %v", got) } } func TestUnderlineBadTypePanics(t *testing.T) { defer func() { if recover() == nil { t.Fatal("Underline should panic on unsupported parameter type") } }() _ = StyleDefault.Underline("bad") } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tscreen.go000066400000000000000000001254301520475227200222350ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (!js && !wasm) || (js && wasm) // +build !js,!wasm js,wasm package tcell import ( "bytes" "encoding/base64" "errors" "fmt" "io" "maps" "os" "runtime" "slices" "strconv" "strings" "sync" "time" "unicode/utf8" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/vt" "golang.org/x/text/transform" ) // NewTerminfoScreen returns a Screen that uses the stock TTY interface // and POSIX terminal control, combined with a terminfo description taken from // the $TERM environment variable. It returns an error if the terminal // is not supported for any reason. // // For terminals that do not support dynamic resize events, the $LINES // $COLUMNS environment variables can be set to the actual window size, // otherwise defaults taken from the terminal database are used. func NewTerminfoScreen(opts ...TerminfoScreenOption) (Screen, error) { return NewTerminfoScreenFromTty(nil, opts...) } type TerminfoScreenOption interface { apply(*tScreen) } // OptColors forces the number of colors, overriding the value // of the color count that would be detected by the environment. // If the value is 0, then color is forced off. Other reasonable values // are 8, 16, 88, 256, or 1<<24. The latter case intrinsically enables // 24-bit color as well. type OptColors int func (o OptColors) apply(t *tScreen) { t.ncolor = min(int(o), 256) t.truecolor = o > 256 t.noColor = o == 0 } // OptTerm overrides the detection of $TERM. type OptTerm string func (o OptTerm) apply(t *tScreen) { t.term = string(o) } // OptAltScreen controls whether the alternate screen buffer is used. // The default is true. The TCELL_ALTSCREEN=disable environment override // is still honored. type OptAltScreen bool func (o OptAltScreen) apply(t *tScreen) { t.altScreen = bool(o) } // OptSanitizeContent enables stripping control characters from content passed // to Put and PutStr. This is safer, but a little slower than leaving content // unsanitized. type OptSanitizeContent bool func (o OptSanitizeContent) apply(t *tScreen) { t.cells.sanitizeContent = bool(o) } // OptAdvancedKeys enables richer key reporting where supported. In this mode // key events may include release state, repeat counts, and physical keys, and // ASCII control letters are reported as KeyRune with ModCtrl instead of // KeyCtrlA through KeyCtrlZ. Shift-Tab is reported as KeyTab with ModShift, // rather than KeyBacktab. type OptAdvancedKeys bool func (o OptAdvancedKeys) apply(t *tScreen) { t.advancedKeys = bool(o) } // OptKeyboardProtocol forces the keyboard reporting protocol instead of using // startup negotiation. The zero value forces legacy keyboard reporting. type OptKeyboardProtocol KeyProtocol func (o OptKeyboardProtocol) apply(t *tScreen) { t.forceKeyboardProtocol(KeyProtocol(o)) } // OptNegotiation controls whether terminal capabilities are negotiated during // startup. The default is true. type OptNegotiation bool func (o OptNegotiation) apply(t *tScreen) { t.negotiate = bool(o) } // OptControlStringLimit sets the maximum inbound control-string payload size // accepted from the terminal before the parser drops the sequence. This limits // OSC and XDA strings, including OSC 52 clipboard strings; OSC 52 is the // protocol used for writing clipboard data through the terminal. The default is // 64 KiB; a value of 0 disables the limit. type OptControlStringLimit int func (o OptControlStringLimit) apply(t *tScreen) { t.controlStringLimit = max(int(o), 0) } // Some terminal escapes that are basically universal. // We would really like to be able to use private mode queries for some of // these but generally we've found that support for queries is not always present, // even when the private modes can be controlled. It appears that *all* terminals // will happily swallow the escapes that they do not recognize, with the small annoyance // in "st" where it prints error messages to its stderr (which is usually not visible // to the user unless they started it from another terminal session). But apart from // the complaint to stderr from "st", everything else is fine. const ( enableAutoMargin = "\x1b[?7h" // dec private mode 7 setCursorPosition = "\x1b[%[1]d;%[2]dH" sgr0 = "\x1b[m" // attrOff bold = "\x1b[1m" dim = "\x1b[2m" italic = "\x1b[3m" underline = "\x1b[4m" blink = "\x1b[5m" reverse = "\x1b[7m" strikeThrough = "\x1b[9m" clear = "\x1b[H\x1b[J" doubleUnder = "\x1b[4:2m" curlyUnder = "\x1b[4:3m" dottedUnder = "\x1b[4:4m" dashedUnder = "\x1b[4:5m" underColor = "\x1b[58:5:%dm" underRGB = "\x1b[58:2::%d:%d:%dm" underFg = "\x1b[59m" enableAltChars = "\x1b(B\x1b)0" // set G0 as US-ASCII, G1 as DEC line drawing startAltChars = "\x0e" // aka Shift-Out endAltChars = "\x0f" // aka Shift-In setFg8 = "\x1b[3%dm" // for colors less than 8 setFg256 = "\x1b[38;5;%dm" // for colors less than 256 setFgRgb = "\x1b[38;2;%d;%d;%dm" // for RGB setBg8 = "\x1b[4%dm" // color colors less than 8 setBg256 = "\x1b[48;5;%dm" // for colors less than 256 setBgRgb = "\x1b[48;2;%d;%d;%dm" // for RGB setFgBgRgb = "\x1b[38;2;%d;%d;%d;48;2;%d;%d;%dm" // for RGB, in one shot enterCA = "\x1b[?1049h" // alternate screen exitCA = "\x1b[?1049l" // alternate screen enterKeypad = "\x1b[?1h\x1b=" // Note mode 1 might not be supported everywhere exitKeypad = "\x1b[?1l\x1b>" // Also mode 1 requestWindowSize = "\x1b[18t" // For modern terminals requestPrimaryDA = "\x1b[c" // Request primary device attributes requestExtAttr = "\x1b[>q" // Request extended attribute (emulator name and version) setClipboard = "\x1b]52;c;%s\x1b\\" // Clipboard content is base64 notifyDesktop9 = "\x1b]9;%[2]s\x1b\\" // Args are title, body (but OSC 9 only has body) notifyDesktop777 = "\x1b]777;notify;%s;%s\x1b\\" // Most commonly supported queryKittyKbd = "\x1b[?u" // Query for Kitty keyboard support enableKittyKbd = "\x1b[=1u" // Technically this pushes enableKittyKbdAdv = "\x1b[=15u" // disambiguation, events, alternate keys, all keys disableKittyKbd = "\x1b[=0u" // Technically this means pop previous mode queryXTermKbd = "\x1b[?4m" // Query for XTerm modify other keys support enableXTermKbd = "\x1b[>4;2m" // Enable modify other keys protocol disableXTermKbd = "\x1b[>4;0m" // Disable modify other keys protocol ) // NewTerminfoScreenFromTty returns a Screen using a custom Tty implementation. // If the passed in tty is nil, then a reasonable default (typically /dev/tty) // is presumed, at least on UNIX hosts. (Windows hosts will typically fail this // call altogether.) func NewTerminfoScreenFromTty(tty Tty, opts ...TerminfoScreenOption) (Screen, error) { t := &tScreen{ tty: tty, altScreen: true, negotiate: true, controlStringLimit: defaultControlStringLimit, } t.prepareCursorStyles() t.prepareExtendedOSC() t.buildAcsMap() t.resizeQ = make(chan bool, 1) t.fallback = make(map[rune]string) maps.Copy(t.fallback, RuneFallbacks) for _, o := range opts { o.apply(t) } return &baseScreen{screenImpl: t}, nil } // tScreen represents a screen backed by a terminfo implementation. type tScreen struct { tty Tty h int w int fini bool cells CellBuffer buffering bool // true if we are collecting writes to buf instead of sending directly to out buf bytes.Buffer curstyle Style style Style resizeQ chan bool quit chan struct{} keyQ chan []byte cx int cy int cls bool // clear screen cursorx int cursory int acs map[rune]string charset string encoder transform.Transformer decoder transform.Transformer fallback map[rune]string ncolor int colors map[color.Color]color.Color palette []color.Color truecolor bool noColor bool legacy bool hasClipboard bool // true if OSC 52 reported via DA1 finiOnce sync.Once enterUrl string exitUrl string setWinSize string cursorStyles map[CursorStyle]string cursorStyle CursorStyle cursorColor color.Color cursorRGB string cursorFg string stopQ chan struct{} eventQ chan Event initQ chan Event initted bool running bool startTime time.Time wg sync.WaitGroup mouseFlags MouseFlags pasteEnabled bool focusEnabled bool setTitle string saveTitle string restoreTitle string title string setClipboard string notifyDesktop string termName string termVers string term string // value from $TERM altScreen bool inlineResize bool haveMouse bool haveMouseSgr bool haveKittyKbd bool haveWin32Kbd bool haveXTermKbd bool forcedKbd KeyProtocol forceKbd bool negotiate bool mouseDisabled bool advancedKeys bool controlStringLimit int input *inputParser sync.Mutex } func (t *tScreen) useAltScreen() bool { return t.altScreen && os.Getenv("TCELL_ALTSCREEN") != "disable" } func validKeyboardProtocol(p KeyProtocol) bool { switch p { case LegacyKeyboard, KittyKeyboard, Win32Keyboard, XTermKeyboard: return true default: return false } } func parseKeyboardProtocol(s string) (KeyProtocol, bool) { switch s { case "legacy": return LegacyKeyboard, true case "kitty": return KittyKeyboard, true case "win32": return Win32Keyboard, true case "xterm": return XTermKeyboard, true default: return LegacyKeyboard, false } } func (t *tScreen) forceKeyboardProtocol(p KeyProtocol) bool { if !validKeyboardProtocol(p) { return false } t.forcedKbd = p t.forceKbd = true return true } func (t *tScreen) applyKeyboardProtocolOverride() { if !t.forceKbd { return } t.haveKittyKbd = t.forcedKbd == KittyKeyboard t.haveWin32Kbd = t.forcedKbd == Win32Keyboard t.haveXTermKbd = t.forcedKbd == XTermKeyboard } func (t *tScreen) applyEnvironmentOverrides() { switch os.Getenv("TCELL_KEYBOARD_PROTOCOL") { case "auto": t.forceKbd = false case "": default: if p, ok := parseKeyboardProtocol(os.Getenv("TCELL_KEYBOARD_PROTOCOL")); ok { t.forceKeyboardProtocol(p) } } switch os.Getenv("TCELL_NEGOTIATE") { case "auto": t.negotiate = true case "disable": t.negotiate = false } t.mouseDisabled = os.Getenv("TCELL_MOUSE") == "disable" } func (t *tScreen) Init() error { if e := t.initialize(); e != nil { return e } t.startTime = time.Now() t.keyQ = make(chan []byte, 10) t.charset = getCharset() if enc := GetEncoding(t.charset); enc != nil { t.encoder = enc.NewEncoder() t.decoder = enc.NewDecoder() } else { return ErrNoCharset } // environment overrides w := 80 h := 24 if i, _ := strconv.Atoi(os.Getenv("LINES")); i != 0 { h = i } if i, _ := strconv.Atoi(os.Getenv("COLUMNS")); i != 0 { w = i } if t.term == "" { t.term = os.Getenv("TERM") } nterm := t.term if t.ncolor == 0 && !t.noColor { cterm := os.Getenv("COLORTERM") // On Windows, enable 24-bit color by default (all terminals there are 24-bit capable) if runtime.GOOS == "windows" { t.truecolor = true t.ncolor = 256 } else if slices.Contains([]string{"truecolor", "direct", "24bit"}, cterm) || strings.HasSuffix(nterm, "-direct") || strings.HasSuffix(nterm, "-truecolor") { t.truecolor = true t.ncolor = 256 // base 8-bit palette } else if strings.HasSuffix(nterm, "-256color") || strings.Contains(cterm, "256") { t.ncolor = 256 } else if strings.HasSuffix(nterm, "-88color") { t.ncolor = 88 } else if strings.HasSuffix(nterm, "-16color") { t.ncolor = 16 } else if strings.Contains(nterm, "color") || cterm != "" { t.ncolor = 8 } else if strings.Contains(nterm, "mono") || strings.HasSuffix(nterm, "-m") { // monochrome variants t.ncolor = 0 } else if strings.Contains(nterm, "ansi") || slices.Contains([]string{"dtterm", "xterm", "aixterm", "linux"}, nterm) { t.ncolor = 8 } else if strings.HasPrefix(nterm, "vt") || nterm == "sun" { // legacy DEC VT 100/220 etc. family. (technically the VT525 can do ANSI, but they should set to ansi) t.ncolor = 0 } else { // best guess - this covers all the modern variants like ghostty, t.ncolor = 256 } if os.Getenv("NO_COLOR") != "" { t.truecolor = false t.ncolor = 0 t.noColor = true } // A user who wants to have his themes honored can set this environment variable. if os.Getenv("TCELL_TRUECOLOR") == "disable" { t.truecolor = false } } if strings.HasPrefix(nterm, "vt") || strings.Contains(nterm, "ansi") || nterm == "linux" || nterm == "sun" || nterm == "sun-color" { // these terminals are "legacy" and not expected to support most OSC functions t.legacy = true } t.applyEnvironmentOverrides() t.initted = false t.quit = make(chan struct{}) t.initQ = make(chan Event, 32) t.eventQ = make(chan Event, 128) t.input = newInputParser(t.filterEvents()) t.input.advanced = t.advancedKeys t.input.controlStringMax = t.controlStringLimit t.Lock() t.cx = -1 t.cy = -1 t.style = StyleDefault t.cells.Resize(w, h) t.cursorx = -1 t.cursory = -1 t.resize() t.Unlock() if err := t.engage(); err != nil { return err } // clip to reasonable limits nColors := min(t.ncolor, 256) t.colors = make(map[color.Color]color.Color, nColors) t.palette = make([]color.Color, nColors) for i := range nColors { t.palette[i] = color.PaletteColor(i) // identity map for our builtin colors t.colors[color.PaletteColor(i)] = color.PaletteColor(i) } return nil } func (t *tScreen) processInitQ() { // NB: called with lock held if t.initted { return } expire := time.After(time.Second) for { select { case <-expire: t.initted = true return case ev := <-t.initQ: switch ev := ev.(type) { case *eventPrimaryAttributes: if ev.Color && t.ncolor == 0 && !t.noColor { t.ncolor = 8 } if ev.Clipboard && t.setClipboard == "" { t.setClipboard = setClipboard } t.hasClipboard = ev.Clipboard t.initted = true return case *eventTermName: // terminal specific overrides t.termName = ev.Name t.termVers = ev.Version switch ev.Name { case "iTerm2": // Some terminals can use OSC 9. Unfortunately we can only discover // them using this means. It appears that pretty much all of them // except iTerm2 also support more standard OSC 777, and it seems like // only Kitty has its OSC 99 thing, but it also does OSC 777 well. t.notifyDesktop = notifyDesktop9 } case *eventPrivateMode: switch ev.Mode { case vt.PmResizeReports: t.inlineResize = ev.Status.Changeable() case vt.PmMouseSgr: t.haveMouseSgr = ev.Status.Changeable() case vt.PmMouseButton: t.haveMouse = ev.Status.Changeable() case vt.PmWin32Input: t.haveWin32Kbd = ev.Status.Changeable() } case *eventKittyKbdMode: t.haveKittyKbd = true case *eventXTermKbdMode: t.haveXTermKbd = true } } } } func (t *tScreen) filterEvents() chan Event { inQ := make(chan Event, 128) go func() { for { var ev Event select { case ev = <-inQ: case <-t.quit: return } switch ev.(type) { case *eventTermName, *eventPrimaryAttributes, *eventPrivateMode, *eventKittyKbdMode, *eventXTermKbdMode: select { case t.initQ <- ev: default: } default: t.eventQ <- ev } } }() return inQ } func (t *tScreen) prepareExtendedOSC() { if t.legacy { return } // OSC 8 is for enter/exit URL. t.enterUrl = "\x1b]8;%[2]s;%[1]s\x1b\\" t.exitUrl = "\x1b]8;;\x1b\\" // CSI .. t is for window operations. t.setWinSize = "\x1b[8;%[2]d;%[1]dt" t.saveTitle = "\x1b[22;2t" t.restoreTitle = "\x1b[23;2t" // this also tries to request that UTF-8 is allowed in the title t.setTitle = "\x1b[>2t\x1b]2;%s\x1b\\" // OSC 52 is for saving to the clipboard. // this string takes a base64 string and sends it to the clipboard. // it will also be able to retrieve the clipboard using "?" as the // sent string, when we support that. t.setClipboard = setClipboard // OSC 777 is the desktop notification supported by a variety of // newer terminals. (There was also OSC 9 and OSC 99, but they // are not as widely deployed, and OSC 9 is not unique.) t.notifyDesktop = notifyDesktop777 } func (t *tScreen) prepareCursorStyles() { t.cursorStyles = map[CursorStyle]string{ CursorStyleDefault: "\x1b[0 q", CursorStyleBlinkingBlock: "\x1b[1 q", CursorStyleSteadyBlock: "\x1b[2 q", CursorStyleBlinkingUnderline: "\x1b[3 q", CursorStyleSteadyUnderline: "\x1b[4 q", CursorStyleBlinkingBar: "\x1b[5 q", CursorStyleSteadyBar: "\x1b[6 q", } if t.legacy { return } if t.cursorRGB == "" { t.cursorRGB = "\x1b]12;#%02x%02x%02x\007" t.cursorFg = "\x1b]112\007" } } func (t *tScreen) Fini() { // Ensure that enough time passes for terminals to finish sending // their initial response (gnome-terminal sends terminal dimensions // asynchronously later than the response to primary DA for some reason.) if time.Since(t.startTime) < 50*time.Millisecond { time.Sleep(time.Millisecond * 50) } t.finiOnce.Do(t.finish) } func (t *tScreen) finish() { t.Lock() t.fini = true t.Unlock() close(t.quit) t.finalize() } func (t *tScreen) SetStyle(style Style) { t.Lock() if !t.fini { t.style = style } t.Unlock() } func (t *tScreen) encodeStr(s string) []byte { var dstBuf [128]byte var buf []byte nb := dstBuf[:] dst := 0 var err error if enc := t.encoder; enc != nil { enc.Reset() dst, _, err = enc.Transform(nb, []byte(s), true) } if err != nil || dst == 0 || nb[0] == '\x1a' { // Combining characters are elided r, _ := utf8.DecodeRuneInString(s) if len(buf) == 0 { if acs, ok := t.acs[r]; ok { buf = append(buf, []byte(acs)...) } else if fb, ok := t.fallback[r]; ok { buf = append(buf, []byte(fb)...) } else { buf = append(buf, '?') } } } else { buf = append(buf, nb[:dst]...) } return buf } // resolvePalette looks up a color to obtain the palette entry for it. func (t *tScreen) resolvePalette(c Color) Color { if v, ok := t.colors[c]; ok { return v } v := color.Find(c, t.palette) t.colors[c] = v return v } // sendFgBg sends the foreground and background. It is assumed that sgr0 // was already emitted prior to calling this (so colors are already in default). func (t *tScreen) sendFgBg(fg Color, bg Color, attr AttrMask) AttrMask { if t.Colors() == 0 { // foreground vs background, we calculate luminance // and possibly do a reverse video if !fg.Valid() { return attr } v, ok := t.colors[fg] if !ok { v = color.Find(fg, []Color{ColorBlack, ColorWhite}) t.colors[fg] = v } switch v { case ColorWhite: return attr case ColorBlack: return attr ^ AttrReverse } } if t.truecolor { if fg.IsRGB() && bg.IsRGB() { r1, g1, b1 := fg.RGB() r2, g2, b2 := bg.RGB() t.Printf(setFgBgRgb, r1, g1, b1, r2, g2, b2) return attr } if fg.IsRGB() { r, g, b := fg.RGB() t.Printf(setFgRgb, r, g, b) fg = ColorDefault } if bg.IsRGB() { r, g, b := bg.RGB() t.Printf(setBgRgb, r, g, b) bg = ColorDefault } } if fg.Valid() { fg = t.resolvePalette(fg) fgc := fg & 0xffffff if fgc < 8 { t.Printf(setFg8, fgc) } else if fgc < 256 { t.Printf(setFg256, fgc) } } if bg.Valid() { bg = t.resolvePalette(bg) bgc := bg & 0xffffff if bgc < 8 { t.Printf(setBg8, bgc) } else if bgc < 256 { t.Printf(setBg256, bgc) } } return attr } // emitAttrs dumps prints the attributes, aside from underline that is special // The assumption is that sgr0 was already printed ahead of this. func (t *tScreen) emitAttrs(attrs AttrMask) { if attrs&AttrBold != 0 { t.Print(bold) } if attrs&AttrReverse != 0 { t.Print(reverse) } if attrs&AttrBlink != 0 { t.Print(blink) } if attrs&AttrDim != 0 { t.Print(dim) } if attrs&AttrItalic != 0 { t.Print(italic) } if attrs&AttrStrikeThrough != 0 { t.Print(strikeThrough) } } // emitUl dumps prints the underline, which may be colored. // The assumption is that sgr0 was already printed ahead of this. func (t *tScreen) emitUnderline(us UnderlineStyle, uc Color) { if us != UnderlineStyleNone { // NB: under color should have been reset by sgr0 if uc.IsRGB() { r, g, b := uc.RGB() uc = t.resolvePalette(uc) t.Printf(underColor, uc&0xff) t.Printf(underRGB, r, g, b) } else if uc.Valid() { t.Printf(underColor, uc&0xff) } t.Print(underline) // to ensure everyone gets at least a basic underline switch us { case UnderlineStyleDouble: t.Print(doubleUnder) case UnderlineStyleCurly: t.Print(curlyUnder) case UnderlineStyleDotted: t.Print(dottedUnder) case UnderlineStyleDashed: t.Print(dashedUnder) } } } // emitUrl either emits a url (OSC 8), or if the string is empty // then the OSC 8 to exit the URL. It should only be called if we // either have a new URL, or need to exit an old one, as it always emits // the OSC 8 sequence (if OSC 8 is supported). func (t *tScreen) emitUrl(u urlInfo) { if t.enterUrl != "" { if u.url != "" { t.Printf(t.enterUrl, u.url, u.id) } else { t.Print(t.exitUrl) } } } // urlNeedsEmission reports whether a hyperlink transition has any wire effect. // Url ids can be staged before the Url itself, and id-only transitions have no // OSC 8 representation of their own. func urlNeedsEmission(oldUrl, newUrl urlInfo) bool { return oldUrl != newUrl && (oldUrl.url != "" || newUrl.url != "") } func (t *tScreen) drawCell(x, y int) int { str, style, width := t.cells.Get(x, y) if !t.cells.Dirty(x, y) { return width } if t.cy != y || t.cx != x { t.Printf(setCursorPosition, y+1, x+1) t.cx = x t.cy = y } if style == StyleDefault { style = t.style } if style != t.curstyle { fg, bg, attrs := style.fg, style.bg, style.attrs t.Print(sgr0) attrs = t.sendFgBg(fg, bg, attrs) t.emitAttrs(attrs) t.emitUnderline(style.ulStyle, style.ulColor) var newUrl urlInfo var oldUrl urlInfo if t.curstyle.url != nil { oldUrl = *t.curstyle.url } if style.url != nil { newUrl = *style.url } // URL string can be long, so don't send it unless we really need to. if urlNeedsEmission(oldUrl, newUrl) { t.emitUrl(newUrl) } t.curstyle = style } // now emit runes - taking care to not overrun width with a // wide character, and to ensure that we emit exactly one regular // character followed up by any residual combing characters if width < 1 { width = 1 } buf := t.encodeStr(str) str = string(buf) if width > 1 && str == "?" { // No FullWidth character support str = "? " t.cx = -1 } if x > t.w-width { // too wide to fit; emit a single space instead width = 1 str = " " } if width > 1 && x+width < t.w { // Clobber over any content in the next cell. // This fixes a problem with some terminals where overwriting two // adjacent single cells with a wide rune would leave an image // of the second cell. This is a workaround for buggy terminals. t.Print(" \b\b") } t.Print(str) t.cx += width t.cells.SetDirty(x, y, false) if width > 1 && len([]rune(str)) > 1 { t.cx = -1 } return width } func (t *tScreen) ShowCursor(x, y int) { t.Lock() if t.fini { t.Unlock() return } t.cursorx = x t.cursory = y t.Unlock() } func (t *tScreen) SetCursor(cs CursorStyle, cc Color) { t.Lock() if t.fini { t.Unlock() return } t.cursorStyle = cs t.cursorColor = cc t.Unlock() } func (t *tScreen) HideCursor() { t.ShowCursor(-1, -1) } func (t *tScreen) showCursor() { x, y := t.cursorx, t.cursory w, h := t.cells.Size() if x < 0 || y < 0 || x >= w || y >= h { t.hideCursor() return } t.Printf(setCursorPosition, y+1, x+1) t.Print(vt.PmShowCursor.Enable()) if t.cursorStyles != nil { if esc, ok := t.cursorStyles[t.cursorStyle]; ok { t.Print(esc) } } if t.cursorRGB != "" { if t.cursorColor == ColorReset { t.Print(t.cursorFg) } else if t.cursorColor.Valid() { r, g, b := t.cursorColor.RGB() t.Printf(t.cursorRGB, r, g, b) } } t.cx = x t.cy = y } func (t *tScreen) Write(b []byte) (int, error) { if t.buffering { return t.buf.Write(b) } return t.tty.Write(b) } func (t *tScreen) Print(s string) { _, _ = io.WriteString(t, s) } func (t *tScreen) Printf(f string, args ...any) { _, _ = fmt.Fprintf(t, f, args...) } func (t *tScreen) Show() { t.Lock() if !t.fini { t.resize() t.draw() } t.Unlock() } func (t *tScreen) clearScreen() { t.Print(sgr0) t.Print(t.exitUrl) _ = t.sendFgBg(t.style.fg, t.style.bg, AttrNone) t.Print(clear) t.cls = false } func (t *tScreen) startBuffering() { t.Print(vt.PmSyncOutput.Enable()) } func (t *tScreen) endBuffering() { t.Print(vt.PmSyncOutput.Disable()) } func (t *tScreen) hideCursor() { // just in case we cannot hide it, move it to the end t.cx, t.cy = t.cells.Size() t.Printf(setCursorPosition, t.cy+1, t.cx+1) // then hide it t.Print(vt.PmShowCursor.Disable()) } func (t *tScreen) draw() { // clobber cursor position, because we're going to change it all t.cx = -1 t.cy = -1 // make no style assumptions t.curstyle = styleInvalid t.buf.Reset() t.buffering = true t.startBuffering() defer func() { t.buffering = false t.endBuffering() }() // hide the cursor while we move stuff around t.hideCursor() if t.cls { t.clearScreen() } for y := 0; y < t.h; y++ { for x := 0; x < t.w; x++ { width := t.drawCell(x, y) if width > 1 { if x+1 < t.w { // this is necessary so that if we ever // go back to drawing that cell, we // actually will *draw* it. t.cells.SetDirty(x+1, y, true) } } x += width - 1 } } if t.curstyle.url != nil && t.curstyle.url.url != "" { t.emitUrl(urlInfo{}) } // restore the cursor t.showCursor() _, _ = t.buf.WriteTo(t.tty) } func (t *tScreen) EnableMouse(flags ...MouseFlags) { var f MouseFlags flagsPresent := false for _, flag := range flags { f |= flag flagsPresent = true } if !flagsPresent { f = MouseMotionEvents | MouseDragEvents | MouseButtonEvents } t.Lock() if t.fini { t.Unlock() return } t.mouseFlags = f t.enableMouse(f) t.Unlock() } func (t *tScreen) enableMouse(f MouseFlags) { // Rather than using terminfo to find mouse escape sequences, we rely on the fact that // pretty much *every* terminal that supports mouse tracking follows the // XTerm standards (the modern ones). It is expected that all terminals understand // the same DEC private modes. Note that the SGR mode is required for the mouse sequences // to be understood. // We rely on dec private mode queries for this. // If your terminal doesn't support these, then ask them to fix it. // Note that as of macOS 26, macOS Terminal does not support them, // so we enable the mouse unconditionally unless we get a report // that says we have mouse, but not SGR mouse. This is suboptimal, but // a concession forced by the sorry state of terminal emulators. if t.mouseDisabled { f = 0 } if f != 0 && t.haveMouse && !t.haveMouseSgr { return } // start by disabling all tracking. t.Print(vt.PmMouseButton.Disable()) t.Print(vt.PmMouseDrag.Disable()) t.Print(vt.PmMouseMotion.Disable()) t.Print(vt.PmMouseSgr.Disable()) t.Print(vt.PmMouseSgrPixel.Disable()) pixel := f&MousePixelEvents != 0 t.input.SetPixelMouse(pixel) if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 { t.Print(vt.PmMouseButton.Enable()) } if f&MouseDragEvents != 0 { t.Print(vt.PmMouseDrag.Enable()) } if f&MouseMotionEvents != 0 { t.Print(vt.PmMouseMotion.Enable()) } if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 { if pixel { t.Print(vt.PmMouseSgrPixel.Enable()) } else { t.Print(vt.PmMouseSgr.Enable()) } } } func (t *tScreen) DisableMouse() { t.Lock() if t.fini { t.Unlock() return } t.mouseFlags = 0 t.enableMouse(0) t.Unlock() } func (t *tScreen) EnablePaste() { t.Lock() if t.fini { t.Unlock() return } t.pasteEnabled = true t.enablePasting(true) t.Unlock() } func (t *tScreen) DisablePaste() { t.Lock() if t.fini { t.Unlock() return } t.pasteEnabled = false t.enablePasting(false) t.Unlock() } func (t *tScreen) enablePasting(on bool) { var s string if on { s = vt.PmBracketedPaste.Enable() } else { s = vt.PmBracketedPaste.Disable() } if s != "" { t.Print(s) } } func (t *tScreen) EnableFocus() { t.Lock() if t.fini { t.Unlock() return } t.focusEnabled = true t.enableFocusReporting() t.Unlock() } func (t *tScreen) DisableFocus() { t.Lock() if t.fini { t.Unlock() return } t.focusEnabled = false t.disableFocusReporting() t.Unlock() } func (t *tScreen) enableFocusReporting() { t.Print(vt.PmFocusReports.Enable()) } func (t *tScreen) disableFocusReporting() { t.Print(vt.PmFocusReports.Disable()) } func (t *tScreen) Size() (int, int) { t.Lock() w, h := t.w, t.h t.Unlock() return w, h } func (t *tScreen) resize() { ws, err := t.tty.WindowSize() if err != nil { return } if ws.Width == t.w && ws.Height == t.h { return } t.cx = -1 t.cy = -1 t.cells.Resize(ws.Width, ws.Height) t.cells.Invalidate() t.h = ws.Height t.w = ws.Width t.input.SetSize(ws.Width, ws.Height) } func (t *tScreen) Colors() int { // this doesn't change, no need for lock if t.truecolor { return 1 << 24 } return t.ncolor } // vtACSNames is a map of bytes defined by terminfo that are used in // the terminals Alternate Character Set to represent other glyphs. // For example, the upper left corner of the box drawing set can be // displayed by printing "l" while in the alternate character set. // It's not quite that simple, since the "l" is the terminfo name, // and it may be necessary to use a different character based on // the terminal implementation (or the terminal may lack support for // this altogether). These values are from the DEC VT100, and all // modern terminal emulators support this as charset 0. var vtACSNames = map[byte]rune{ '`': RuneDiamond, 'a': RuneCkBoard, 'f': RuneDegree, 'g': RunePlMinus, 'h': RuneBoard, 'i': RuneLantern, 'j': RuneLRCorner, 'k': RuneURCorner, 'l': RuneULCorner, 'm': RuneLLCorner, 'n': RunePlus, 'o': RuneS1, 'p': RuneS3, 'q': RuneHLine, 'r': RuneS7, 's': RuneS9, 't': RuneLTee, 'u': RuneRTee, 'v': RuneBTee, 'w': RuneTTee, 'x': RuneVLine, 'y': RuneLEqual, 'z': RuneGEqual, '{': RunePi, '|': RuneNEqual, '}': RuneSterling, '~': RuneBullet, } // buildAcsMap builds a map of characters that we translate from Unicode to // alternate character encodings. To do this, we use the standard VT100 ACS // maps. This is only done if the terminal lacks support for Unicode; we // always prefer to emit Unicode glyphs when we are able. func (t *tScreen) buildAcsMap() { const acsstr = "``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~" t.acs = make(map[rune]string) for b, r := range vtACSNames { t.acs[r] = startAltChars + string(b) + endAltChars } } func (t *tScreen) scanInput(buf *bytes.Buffer) { // The end of the buffer isn't necessarily the end of the input, because // large inputs are chunked. Set atEOF to false so the UTF-8 validating decoder // returns ErrShortSrc instead of ErrInvalidUTF8 for incomplete multi-byte codepoints. const atEOF = false for buf.Len() > 0 { utf := make([]byte, min(8, max(buf.Len()*2, 128))) nOut, nIn, e := t.decoder.Transform(utf, buf.Bytes(), atEOF) _ = buf.Next(nIn) t.input.ScanUTF8(utf[:nOut]) if e == transform.ErrShortSrc { return } } } func (t *tScreen) mainLoop(stopQ chan struct{}) { defer t.wg.Done() buf := &bytes.Buffer{} var ta <-chan time.Time for { select { case <-stopQ: return case <-t.quit: return case <-t.resizeQ: go func() { t.Lock() t.cx = -1 t.cy = -1 t.resize() t.cells.Invalidate() t.draw() t.Unlock() }() continue case chunk := <-t.keyQ: buf.Write(chunk) t.scanInput(buf) if t.input.Waiting() { ta = time.After(time.Millisecond * 100) } else { ta = nil } case <-ta: t.input.Scan() } } } func (t *tScreen) inputLoop(stopQ chan struct{}) { defer t.wg.Done() for { select { case <-stopQ: return default: } chunk := make([]byte, 128) n, e := t.tty.Read(chunk) switch e { case nil: default: t.Lock() running := t.running t.Unlock() if running { select { case t.eventQ <- NewEventError(e): case <-t.quit: } } return } if n > 0 { t.keyQ <- chunk[:n] } } } func (t *tScreen) Sync() { t.Lock() t.cx = -1 t.cy = -1 if !t.fini { t.resize() t.cls = true t.cells.Invalidate() t.draw() } t.Unlock() } func (t *tScreen) CharacterSet() string { return t.charset } func (t *tScreen) RegisterRuneFallback(orig rune, fallback string) { t.Lock() t.fallback[orig] = fallback t.Unlock() } func (t *tScreen) UnregisterRuneFallback(orig rune) { t.Lock() delete(t.fallback, orig) t.Unlock() } func (t *tScreen) SetSize(w, h int) { t.Lock() defer t.Unlock() if t.fini { return } if t.setWinSize != "" { t.Printf(t.setWinSize, w, h) } t.cells.Invalidate() t.resize() } func (t *tScreen) Resize(int, int, int, int) {} func (t *tScreen) Suspend() error { t.Lock() if t.fini { t.Unlock() return nil } finish := t.disengageStart() t.Unlock() if finish { t.disengageFinish() } return nil } func (t *tScreen) Resume() error { t.Lock() defer t.Unlock() if t.fini { return nil } return t.engageLocked() } func (t *tScreen) Tty() (Tty, bool) { return t.tty, true } func (t *tScreen) applyKnownTerminalProfile(goos, termProgram string) bool { switch termProgram { case "Apple_Terminal": // macOS Terminal.app cannot handle the startup queries, but it does // support modern mouse reporting. t.haveMouse = true t.haveMouseSgr = true t.termName = "Terminal.app" t.termVers = os.Getenv("TERM_PROGRAM_VERSION") return true case "WezTerm": // The WezTerm keyboard protocol to use is in theory driven by its // own configuration, but we have found this unreliable because it // does not mask unsupported capabilities. Furthermore, on Windows // builds the kitty protocol implementation is broken, while on other // builds win32-input-mode is broken. This is a best effort to make // WezTerm work reasonably; our stronger advice is to choose another // terminal program altogether. This workaround will probably not // apply to ssh sessions, as TERM_PROGRAM is not normally propagated. if goos == "windows" { t.haveWin32Kbd = true } else { t.haveKittyKbd = true t.haveWin32Kbd = false } t.haveMouse = true t.haveMouseSgr = true t.initted = true t.termName = "WezTerm" t.termVers = os.Getenv("TERM_PROGRAM_VERSION") return true } return false } func useVTWindowSizeQuery(goos string) bool { return goos != "windows" } func useXTermKeyboardQuery(goos string) bool { return goos != "windows" } // engage is used to place the terminal in raw mode and establish screen size, etc. // Think of this is as tcell "engaging" the clutch, as it's going to be driving the // terminal interface. func (t *tScreen) engage() error { t.Lock() defer t.Unlock() return t.engageLocked() } // engageLocked is engage's implementation when t's lock is already held. func (t *tScreen) engageLocked() error { if t.tty == nil { return ErrNoScreen } if t.running { return errors.New("already engaged") } if err := t.tty.Start(); err != nil { return err } stopQ := make(chan struct{}) t.stopQ = stopQ t.wg.Add(2) go t.inputLoop(stopQ) go t.mainLoop(stopQ) if !t.initted { // macOS Terminal.app is brain damaged // https://garrett.damore.org/2025/12/macos-terminal-still-missing-mark-apple.html // Eventually they'll hopefully fix this. As the environment variable // does not convey by default via ssh, remote sessions might see spurious characters // emitted during startup. See the blog post for alternatives. if !t.applyKnownTerminalProfile(runtime.GOOS, os.Getenv("TERM_PROGRAM")) && t.negotiate { if useVTWindowSizeQuery(runtime.GOOS) { t.Print(requestWindowSize) } t.Print(vt.PmResizeReports.Query()) t.Print(vt.PmMouseButton.Query()) t.Print(vt.PmMouseSgr.Query()) if !t.forceKbd { t.Print(vt.PmWin32Input.Query()) t.Print(queryKittyKbd) if useXTermKeyboardQuery(runtime.GOOS) { // XTerm's modifyOtherKeys mode is mainly useful for XTerm // itself, and we do not use it on Windows. t.Print(queryXTermKbd) } } t.Print(requestExtAttr) } if !t.negotiate { t.initted = true } else if !t.initted { t.Print(requestPrimaryDA) // NB: MUST BE LAST } } t.processInitQ() t.applyKeyboardProtocolOverride() if t.useAltScreen() { // Technically this may not be right, but every terminal we know about // (even Wyse 60) uses this to enter the alternate screen buffer, and // possibly save and restore the window title and/or icon. // (In theory there could be terminals that don't support X,Y cursor // positions without a setup command, but we don't support them.) t.Print(enterCA) t.Print(t.saveTitle) } if t.haveWin32Kbd { t.Print(vt.PmWin32Input.Enable()) } else if t.haveKittyKbd { if t.advancedKeys { t.Print(enableKittyKbdAdv) } else { t.Print(enableKittyKbd) } } else if t.haveXTermKbd { t.Print(enableXTermKbd) } t.running = true if ws, err := t.tty.WindowSize(); err == nil && ws.Width != 0 && ws.Height != 0 { t.cells.Resize(ws.Width, ws.Height) } t.enableMouse(t.mouseFlags) t.enablePasting(t.pasteEnabled) if t.focusEnabled { t.enableFocusReporting() } t.Print(enterKeypad) t.Print(enableAltChars) t.Print(vt.PmShowCursor.Disable()) t.Print(vt.PmAutoMargin.Disable()) t.Print(clear) if t.title != "" && t.setTitle != "" { t.Printf(t.setTitle, t.title) } if t.negotiate && useVTWindowSizeQuery(runtime.GOOS) { t.Print(requestWindowSize) } if t.inlineResize { t.Print(vt.PmResizeReports.Enable()) } else { t.tty.NotifyResize(t.resizeQ) } return nil } // disengage is used to release the terminal back to support from the caller. // Think of this as tcell disengaging the clutch, so that another application // can take over the terminal interface. This restores the TTY mode that was // present when the application was first started. func (t *tScreen) disengage() { t.Lock() finish := t.disengageStart() t.Unlock() if finish { t.disengageFinish() } } // disengageStart begins a disengage operation while t's lock is already held. // It returns true when disengageFinish must be called after releasing the lock. func (t *tScreen) disengageStart() bool { if !t.running { return false } t.running = false if t.inlineResize { t.Print(vt.PmResizeReports.Disable()) } else { t.tty.NotifyResize(nil) } stopQ := t.stopQ close(stopQ) _ = t.tty.Drain() return true } // disengageFinish completes a disengage operation after disengageStart has // released the running loops. func (t *tScreen) disengageFinish() { // wait for everything to shut down t.wg.Wait() // shutdown the screen and disable special modes (e.g. mouse and bracketed paste) t.cells.Resize(0, 0) t.Print(vt.PmShowCursor.Enable()) if t.cursorStyles != nil && t.cursorStyle != CursorStyleDefault { t.Print(t.cursorStyles[CursorStyleDefault]) } if t.cursorFg != "" && t.cursorColor.Valid() { t.Print(t.cursorFg) } t.Print(exitKeypad) t.Print(sgr0) t.Print(vt.PmAutoMargin.Enable()) if t.haveWin32Kbd { t.Print(vt.PmWin32Input.Disable()) } if t.haveKittyKbd { t.Print(disableKittyKbd) } if t.haveXTermKbd { t.Print(disableXTermKbd) } // Hack for Windows. if runtime.GOOS == "windows" { t.Print(vt.PmWin32Input.Disable()) } // t.Print(t.disableCsiU) if t.useAltScreen() { t.Print(t.restoreTitle) t.Print(clear) t.Print(exitCA) } t.enableMouse(0) t.enablePasting(false) t.disableFocusReporting() _ = t.tty.Stop() } // Beep emits a beep to the terminal. func (t *tScreen) Beep() error { t.Lock() defer t.Unlock() if t.fini { return nil } t.Print(string(byte(7))) return nil } // finalize is used to at application shutdown, and restores the terminal // to it's initial state. It should not be called more than once. func (t *tScreen) finalize() { t.disengage() _ = t.tty.Close() close(t.eventQ) } func (t *tScreen) StopQ() <-chan struct{} { return t.quit } func (t *tScreen) EventQ() chan Event { return t.eventQ } func (t *tScreen) GetCells() *CellBuffer { return &t.cells } func (t *tScreen) SetTitle(title string) { t.Lock() if t.fini { t.Unlock() return } t.title = stripOSCControlsIfNeeded(title) if t.setTitle != "" && t.running { t.Printf(t.setTitle, t.title) } t.Unlock() } func (t *tScreen) SetClipboard(data []byte) { // Post binary data to the system clipboard. It might be UTF-8, it might not be. t.Lock() if t.fini { t.Unlock() return } if t.setClipboard != "" { encoded := base64.StdEncoding.EncodeToString(data) t.Printf(t.setClipboard, encoded) } t.Unlock() } func (t *tScreen) GetClipboard() { t.Lock() if t.fini { t.Unlock() return } if t.setClipboard != "" { t.Printf(t.setClipboard, "?") } t.Unlock() } func (t *tScreen) HasClipboard() bool { return t.hasClipboard } func (t *tScreen) ShowNotification(title string, body string) { t.Lock() if t.fini { t.Unlock() return } t.Printf(t.notifyDesktop, stripOSCControlsIfNeeded(title), stripOSCControlsIfNeeded(body)) t.Unlock() } func (t *tScreen) Terminal() (string, string) { t.Lock() defer t.Unlock() return t.termName, t.termVers } func (t *tScreen) KeyboardProtocol() KeyProtocol { t.Lock() defer t.Unlock() if t.haveWin32Kbd { return Win32Keyboard } if t.haveKittyKbd { return KittyKeyboard } if t.haveXTermKbd { return XTermKeyboard } return LegacyKeyboard } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tscreen_plan9.go000066400000000000000000000015371520475227200233410ustar00rootroot00000000000000//go:build plan9 // +build plan9 // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell // initialize on Plan 9: if no TTY was provided, use the Plan 9 TTY. func (t *tScreen) initialize() error { if t.tty == nil { tty, err := NewDevTty() if err != nil { return err } t.tty = tty } return nil } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tscreen_test.go000066400000000000000000000732451520475227200233020ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "bytes" "runtime" "strings" "testing" "time" "github.com/gdamore/tcell/v3/tty" "github.com/gdamore/tcell/v3/vt" ) // This just offers some very basic tests that do not require a full mock. // drainInput just does a very primitive sleep to allow input to drain. // We need this because otherwise the application will close too soon before // consuming characters from input, including sequences that are returned in // response to queries. func drainInput() { time.Sleep(time.Millisecond * 30) } // TestInitScreen just tries to initialize the default screen. // It requires a working tty. func TestInitScreen(t *testing.T) { s, err := NewTerminfoScreen() if err != nil { t.Skip("failed to get screen", err) } if err := s.Init(); err != nil { t.Skip("failed to initialize screen", err) } defer s.Fini() if s.CharacterSet() != "UTF-8" { t.Fatalf("Character Set (%v) not UTF-8", s.CharacterSet()) } drainInput() } type spyTty struct { vt.MockTerm writes bytes.Buffer } func (t *spyTty) Write(b []byte) (int, error) { _, _ = t.writes.Write(b) return t.MockTerm.Write(b) } func (t *spyTty) Output() string { return t.writes.String() } func TestOptAltScreenDisable(t *testing.T) { t.Setenv("TCELL_ALTSCREEN", "") tty := &spyTty{MockTerm: vt.NewMockTerm()} s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } s.Fini() out := tty.Output() if strings.Contains(out, enterCA) { t.Fatalf("alternate screen enter escape was emitted") } if strings.Contains(out, exitCA) { t.Fatalf("alternate screen exit escape was emitted") } } func TestOptAltScreenDefault(t *testing.T) { t.Setenv("TCELL_ALTSCREEN", "") tty := &spyTty{MockTerm: vt.NewMockTerm()} s, err := NewTerminfoScreenFromTty(tty) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } s.Fini() out := tty.Output() if !strings.Contains(out, enterCA) { t.Fatalf("alternate screen enter escape was not emitted") } if !strings.Contains(out, exitCA) { t.Fatalf("alternate screen exit escape was not emitted") } } func TestFiniPreventsSetStyleMutation(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } before := StyleDefault.Foreground(ColorRed) after := StyleDefault.Foreground(ColorBlue) scr.SetStyle(before) scr.Fini() scr.SetStyle(after) ts.Lock() defer ts.Unlock() if !ts.fini { t.Fatal("screen was not marked finished") } if ts.style != before { t.Fatal("SetStyle mutated the screen after Fini") } } func TestFiniPreventsFurtherTerminalMutation(t *testing.T) { tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})} scr, err := NewTerminfoScreenFromTty(tty) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } scr.ShowCursor(1, 1) scr.SetCursorStyle(CursorStyleSteadyBlock, ColorRed) scr.EnableMouse() scr.EnablePaste() scr.EnableFocus() scr.Fini() beforeOutput := tty.Output() ts.Lock() beforeCursorX, beforeCursorY := ts.cursorx, ts.cursory beforeCursorStyle, beforeCursorColor := ts.cursorStyle, ts.cursorColor beforeMouseFlags := ts.mouseFlags beforePasteEnabled, beforeFocusEnabled := ts.pasteEnabled, ts.focusEnabled ts.Unlock() scr.ShowCursor(2, 1) scr.SetCursorStyle(CursorStyleBlinkingBar, ColorBlue) scr.EnableMouse(MouseMotionEvents) scr.DisableMouse() scr.EnablePaste() scr.DisablePaste() scr.EnableFocus() scr.DisableFocus() scr.SetSize(20, 10) if err := scr.Suspend(); err != nil { t.Fatalf("Suspend after Fini failed: %v", err) } if err := scr.Resume(); err != nil { t.Fatalf("Resume after Fini failed: %v", err) } if err := scr.Beep(); err != nil { t.Fatalf("Beep after Fini failed: %v", err) } scr.SetTitle("after") scr.SetClipboard([]byte("after")) scr.GetClipboard() scr.ShowNotification("after", "after") if got := tty.Output(); got != beforeOutput { t.Fatal("terminal output changed after Fini") } ts.Lock() defer ts.Unlock() if ts.cursorx != beforeCursorX || ts.cursory != beforeCursorY { t.Fatal("cursor position changed after Fini") } if ts.cursorStyle != beforeCursorStyle || ts.cursorColor != beforeCursorColor { t.Fatal("cursor style changed after Fini") } if ts.mouseFlags != beforeMouseFlags { t.Fatal("mouse flags changed after Fini") } if ts.pasteEnabled != beforePasteEnabled { t.Fatal("paste state changed after Fini") } if ts.focusEnabled != beforeFocusEnabled { t.Fatal("focus state changed after Fini") } } func TestOptAdvancedKeys(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt, OptAdvancedKeys(true)) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if !ts.advancedKeys { t.Fatal("advanced keys option was not applied") } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() if !ts.input.advanced { t.Fatal("advanced keys option was not propagated to input parser") } } func TestOptKeyboardProtocol(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt, OptKeyboardProtocol(KittyKeyboard)) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if !ts.forceKbd || ts.forcedKbd != KittyKeyboard { t.Fatalf("forced keyboard protocol = (%v, %v), want (true, %v)", ts.forceKbd, ts.forcedKbd, KittyKeyboard) } } func TestOptNegotiation(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt, OptNegotiation(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if ts.negotiate { t.Fatal("negotiation option was not applied") } } func TestSetSizeTakesScreenLock(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) ts := &tScreen{tty: mt, w: 8, h: 2} done := make(chan struct{}) ts.Lock() go func() { ts.SetSize(8, 2) close(done) }() select { case <-done: ts.Unlock() t.Fatal("SetSize returned while the screen lock was held") case <-time.After(10 * time.Millisecond): } ts.Unlock() select { case <-done: case <-time.After(time.Second): t.Fatal("SetSize did not return after the screen lock was released") } } func TestOptControlStringLimit(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt, OptControlStringLimit(4096)) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if ts.controlStringLimit != 4096 { t.Fatalf("control string limit = %d, want %d", ts.controlStringLimit, 4096) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() if ts.input.controlStringMax != 4096 { t.Fatalf("input parser control string limit = %d, want %d", ts.input.controlStringMax, 4096) } } func TestOptControlStringLimitUnlimited(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt, OptControlStringLimit(0)) if err != nil { t.Fatalf("failed to get screen: %v", err) } bs, ok := scr.(*baseScreen) if !ok { t.Fatalf("expected *baseScreen, got %T", scr) } ts, ok := bs.screenImpl.(*tScreen) if !ok { t.Fatalf("expected *tScreen, got %T", bs.screenImpl) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() payload := bytes.Repeat([]byte{'x'}, defaultControlStringLimit+1) ts.input.ScanUTF8(append([]byte("\x1b]"), payload...)) if ts.input.state != istOsc { t.Fatalf("parser state = %v, want %v", ts.input.state, istOsc) } if !bytes.Equal(ts.input.strBuf, payload) { t.Fatalf("string buffer length = %d, want %d", len(ts.input.strBuf), len(payload)) } } func TestOptSanitizeContent(t *testing.T) { t.Run("disabled by default", func(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() scr.PutStr(0, 0, "\x1bA\x07B") if got, _, _ := scr.Get(0, 0); !strings.Contains(got, "\x1b") { t.Fatalf("expected control bytes to remain when sanitizer is disabled, got %q", got) } if got, _, _ := scr.Get(1, 0); !strings.Contains(got, "\x07") { t.Fatalf("expected control bytes to remain when sanitizer is disabled, got %q", got) } }) t.Run("enabled", func(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2}) scr, err := NewTerminfoScreenFromTty(mt, OptSanitizeContent(true)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() scr.PutStr(0, 0, "\x1bA\x07B") if got, _, _ := scr.Get(0, 0); got != "A" { t.Fatalf("unexpected sanitized cell content at 0,0: %q", got) } if got, _, _ := scr.Get(1, 0); got != "B" { t.Fatalf("unexpected sanitized cell content at 1,0: %q", got) } }) } func TestNewScreenSanitizeContentOption(t *testing.T) { scr, err := NewScreen(OptSanitizeContent(true)) if err != nil { t.Skipf("failed to get screen: %v", err) } if err := scr.Init(); err != nil { t.Skipf("failed to initialize screen: %v", err) } defer scr.Fini() scr.PutStr(0, 0, "\x1bA\x07B") if got, _, _ := scr.Get(0, 0); got != "A" { t.Fatalf("unexpected sanitized cell content at 0,0: %q", got) } if got, _, _ := scr.Get(1, 0); got != "B" { t.Fatalf("unexpected sanitized cell content at 1,0: %q", got) } } func TestNewScreenShimScreen(t *testing.T) { _, scr := NewMockScreen(t) ShimScreen(scr) got, err := NewScreen() if err != nil { t.Fatalf("failed to get screen: %v", err) } if got != scr { t.Fatalf("unexpected shimmed screen: got %T, want %T", got, scr) } } func TestUnlockRegionRedrawsUntouchedBlankCell(t *testing.T) { mt, scr := NewMockScreen(t, vt.MockOptSize{X: 1, Y: 1}) defer scr.Fini() mt.Backend().Put(vt.Coord{X: 0, Y: 0}, vt.Cell{C: "X", S: vt.BaseStyle, W: 1}) scr.LockRegion(0, 0, 1, 1, true) scr.LockRegion(0, 0, 1, 1, false) scr.Show() if got := mt.GetCell(vt.Coord{X: 0, Y: 0}); got.C != " " || got.W != 1 { t.Fatalf("unlocking an untouched blank cell should repaint a space, got %#v", got) } } func TestOSC8ControlsAreStrippedFromOutput(t *testing.T) { tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() style := StyleDefault. Url("http://exa\x07mple.com/\x1b\\path"). UrlId("id\x00\x1f\x7f\x80\x9fend") s.PutStrStyled(0, 0, "X", style) s.Show() out := tty.Output() const prefix = "\x1b]8;id=idend;" _, link, ok := strings.Cut(out, prefix) if !ok { t.Fatalf("missing OSC 8 link open sequence in output: %q", out) } link, _, ok = strings.Cut(link, "\x1b\\") if !ok { t.Fatalf("missing OSC 8 terminator in output: %q", out) } if link != "http://example.com/\\path" { t.Fatalf("unexpected emitted URL payload: %q", link) } if _, afterLink, ok := strings.Cut(out, "X"); !ok || !strings.Contains(afterLink, "\x1b]8;;\x1b\\") { t.Fatalf("missing OSC 8 link close sequence after linked content: %q", out) } for i := 0; i < len(link); i++ { c := link[i] if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) { t.Fatalf("control characters survived in emitted URL payload: %q", link) } } } func TestOSC8IdWithoutUrlDoesNotEmitClose(t *testing.T) { tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() tty.writes.Reset() s.PutStrStyled(0, 0, "X", StyleDefault.UrlId("orphan")) s.Show() if out := tty.Output(); strings.Contains(out, "\x1b]8;;\x1b\\") { t.Fatalf("unexpected OSC 8 close sequence for id-only style: %q", out) } } func TestKeyboardProtocol(t *testing.T) { tests := []struct { name string setup func(*tScreen) want KeyProtocol }{ { name: "Legacy", want: LegacyKeyboard, }, { name: "Xterm", setup: func(s *tScreen) { s.haveXTermKbd = true }, want: XTermKeyboard, }, { name: "Kitty", setup: func(s *tScreen) { s.haveKittyKbd = true }, want: KittyKeyboard, }, { name: "Win32", setup: func(s *tScreen) { s.haveWin32Kbd = true }, want: Win32Keyboard, }, { name: "KittyBeforeXterm", setup: func(s *tScreen) { s.haveKittyKbd = true s.haveXTermKbd = true }, want: KittyKeyboard, }, { name: "Win32BeforeKittyAndXterm", setup: func(s *tScreen) { s.haveWin32Kbd = true s.haveKittyKbd = true s.haveXTermKbd = true }, want: Win32Keyboard, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &tScreen{} if tt.setup != nil { tt.setup(s) } if got := s.KeyboardProtocol(); got != tt.want { t.Fatalf("KeyboardProtocol() = %v, want %v", got, tt.want) } }) } } func TestApplyKnownTerminalProfile(t *testing.T) { tests := []struct { name string goos string termProgram string wantKnown bool wantInitted bool wantMouse bool wantMouseSgr bool wantKittyKbd bool wantWin32Kbd bool }{ { name: "AppleTerminal", goos: "darwin", termProgram: "Apple_Terminal", wantKnown: true, wantMouse: true, wantMouseSgr: true, }, { name: "LocalWindowsWezTerm", goos: "windows", termProgram: "WezTerm", wantKnown: true, wantInitted: true, wantMouse: true, wantMouseSgr: true, wantWin32Kbd: true, }, { name: "LocalUnixWezTerm", goos: "linux", termProgram: "WezTerm", wantKnown: true, wantInitted: true, wantMouse: true, wantMouseSgr: true, wantKittyKbd: true, }, { name: "UnknownTerminal", goos: "linux", termProgram: "Other", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &tScreen{} if got := s.applyKnownTerminalProfile(tt.goos, tt.termProgram); got != tt.wantKnown { t.Fatalf("applyKnownTerminalProfile() = %v, want %v", got, tt.wantKnown) } if s.initted != tt.wantInitted { t.Fatalf("initted = %v, want %v", s.initted, tt.wantInitted) } if s.haveMouse != tt.wantMouse { t.Fatalf("haveMouse = %v, want %v", s.haveMouse, tt.wantMouse) } if s.haveMouseSgr != tt.wantMouseSgr { t.Fatalf("haveMouseSgr = %v, want %v", s.haveMouseSgr, tt.wantMouseSgr) } if s.haveKittyKbd != tt.wantKittyKbd { t.Fatalf("haveKittyKbd = %v, want %v", s.haveKittyKbd, tt.wantKittyKbd) } if s.haveWin32Kbd != tt.wantWin32Kbd { t.Fatalf("haveWin32Kbd = %v, want %v", s.haveWin32Kbd, tt.wantWin32Kbd) } }) } } func TestAppleTerminalProfileSkipsStartupQueries(t *testing.T) { t.Setenv("TERM_PROGRAM", "Apple_Terminal") t.Setenv("TERM_PROGRAM_VERSION", "999") tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() out := tty.Output() for _, seq := range []string{ vt.PmResizeReports.Query(), vt.PmMouseButton.Query(), vt.PmMouseSgr.Query(), vt.PmWin32Input.Query(), queryKittyKbd, queryXTermKbd, requestExtAttr, } { if strings.Contains(out, seq) { t.Fatalf("Apple Terminal profile emitted startup query %q", seq) } } if !strings.Contains(out, requestPrimaryDA) { t.Fatal("Apple Terminal profile did not emit primary DA") } name, version := s.Terminal() if name != "Terminal.app" || version != "999" { t.Fatalf("Terminal() = %q, %q, want %q, %q", name, version, "Terminal.app", "999") } } func TestKeyboardProbePolicy(t *testing.T) { tests := []struct { name string goos string wantWindowSizeQuery bool wantXTermQuery bool }{ { name: "Unix", goos: "linux", wantWindowSizeQuery: true, wantXTermQuery: true, }, { name: "Windows", goos: "windows", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := useVTWindowSizeQuery(tt.goos); got != tt.wantWindowSizeQuery { t.Fatalf("useVTWindowSizeQuery() = %v, want %v", got, tt.wantWindowSizeQuery) } if got := useXTermKeyboardQuery(tt.goos); got != tt.wantXTermQuery { t.Fatalf("useXTermKeyboardQuery() = %v, want %v", got, tt.wantXTermQuery) } }) } } func TestKeyboardProtocolHelpers(t *testing.T) { tests := []struct { name string value string want KeyProtocol wantValid bool }{ {name: "legacy", value: "legacy", want: LegacyKeyboard, wantValid: true}, {name: "kitty", value: "kitty", want: KittyKeyboard, wantValid: true}, {name: "win32", value: "win32", want: Win32Keyboard, wantValid: true}, {name: "xterm", value: "xterm", want: XTermKeyboard, wantValid: true}, {name: "invalid", value: "bogus"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := parseKeyboardProtocol(tt.value) if got != tt.want || ok != tt.wantValid { t.Fatalf("parseKeyboardProtocol(%q) = (%v, %v), want (%v, %v)", tt.value, got, ok, tt.want, tt.wantValid) } }) } if validKeyboardProtocol(KeyProtocol(99)) { t.Fatal("invalid keyboard protocol was accepted") } } func TestForceKeyboardProtocol(t *testing.T) { s := &tScreen{ haveKittyKbd: true, haveWin32Kbd: true, haveXTermKbd: true, } if s.forceKeyboardProtocol(KeyProtocol(99)) { t.Fatal("invalid keyboard protocol was forced") } if s.forceKbd { t.Fatal("invalid keyboard protocol changed override state") } if !s.forceKeyboardProtocol(XTermKeyboard) { t.Fatal("valid keyboard protocol was not forced") } s.applyKeyboardProtocolOverride() if s.haveKittyKbd || s.haveWin32Kbd || !s.haveXTermKbd { t.Fatalf("forced XTerm protocol state = kitty:%v win32:%v xterm:%v", s.haveKittyKbd, s.haveWin32Kbd, s.haveXTermKbd) } } func TestApplyEnvironmentOverrides(t *testing.T) { tests := []struct { name string keyboardProtocol string negotiate string mouse string startForceKbd bool startForcedKbd KeyProtocol startNegotiate bool wantForceKbd bool wantForcedKbd KeyProtocol wantNegotiate bool wantMouseOff bool }{ { name: "defaults", startNegotiate: true, wantNegotiate: true, wantForcedKbd: LegacyKeyboard, }, { name: "force all", keyboardProtocol: "win32", negotiate: "disable", mouse: "disable", startNegotiate: true, wantForceKbd: true, wantForcedKbd: Win32Keyboard, wantMouseOff: true, }, { name: "auto resets options", keyboardProtocol: "auto", negotiate: "auto", startForceKbd: true, startForcedKbd: KittyKeyboard, wantNegotiate: true, wantForcedKbd: KittyKeyboard, }, { name: "invalid leaves options", keyboardProtocol: "bogus", negotiate: "bogus", startForceKbd: true, startForcedKbd: XTermKeyboard, startNegotiate: false, wantForceKbd: true, wantForcedKbd: XTermKeyboard, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("TCELL_KEYBOARD_PROTOCOL", tt.keyboardProtocol) t.Setenv("TCELL_NEGOTIATE", tt.negotiate) t.Setenv("TCELL_MOUSE", tt.mouse) s := &tScreen{ forceKbd: tt.startForceKbd, forcedKbd: tt.startForcedKbd, negotiate: tt.startNegotiate, } s.applyEnvironmentOverrides() if s.forceKbd != tt.wantForceKbd { t.Fatalf("forceKbd = %v, want %v", s.forceKbd, tt.wantForceKbd) } if s.forcedKbd != tt.wantForcedKbd { t.Fatalf("forcedKbd = %v, want %v", s.forcedKbd, tt.wantForcedKbd) } if s.negotiate != tt.wantNegotiate { t.Fatalf("negotiate = %v, want %v", s.negotiate, tt.wantNegotiate) } if s.mouseDisabled != tt.wantMouseOff { t.Fatalf("mouseDisabled = %v, want %v", s.mouseDisabled, tt.wantMouseOff) } }) } } func TestForcedKeyboardProtocolSkipsKeyboardQueries(t *testing.T) { t.Setenv("TERM_PROGRAM", "") tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptKeyboardProtocol(KittyKeyboard), OptAdvancedKeys(true), OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() out := tty.Output() for _, seq := range []string{vt.PmWin32Input.Query(), queryKittyKbd, queryXTermKbd} { if strings.Contains(out, seq) { t.Fatalf("forced keyboard protocol emitted competing query %q", seq) } } for _, seq := range []string{vt.PmResizeReports.Query(), vt.PmMouseButton.Query(), vt.PmMouseSgr.Query(), requestExtAttr} { if !strings.Contains(out, seq) { t.Fatalf("forced keyboard protocol suppressed unrelated query %q", seq) } } if !strings.Contains(out, enableKittyKbdAdv) { t.Fatalf("forced Kitty protocol did not emit advanced enable sequence") } } func TestNegotiationDisabledSkipsStartupQueries(t *testing.T) { t.Setenv("TERM_PROGRAM", "") tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() out := tty.Output() for _, seq := range []string{ requestWindowSize, vt.PmResizeReports.Query(), vt.PmMouseButton.Query(), vt.PmMouseSgr.Query(), vt.PmWin32Input.Query(), queryKittyKbd, queryXTermKbd, requestExtAttr, requestPrimaryDA, } { if strings.Contains(out, seq) { t.Fatalf("disabled negotiation emitted startup query %q", seq) } } } func TestMouseDisabledPreventsEnablement(t *testing.T) { t.Setenv("TCELL_MOUSE", "disable") tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() s.EnableMouse() out := tty.Output() for _, seq := range []string{vt.PmMouseButton.Enable(), vt.PmMouseDrag.Enable(), vt.PmMouseMotion.Enable(), vt.PmMouseSgr.Enable()} { if strings.Contains(out, seq) { t.Fatalf("disabled mouse emitted enable sequence %q", seq) } } } func TestMouseWithoutSgrPreventsEnablement(t *testing.T) { tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})} s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false)) if err != nil { t.Fatalf("failed to get screen: %v", err) } if err := s.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer s.Fini() ts := s.(*baseScreen).screenImpl.(*tScreen) ts.haveMouse = true ts.haveMouseSgr = false s.EnableMouse() out := tty.Output() for _, seq := range []string{vt.PmMouseButton.Enable(), vt.PmMouseDrag.Enable(), vt.PmMouseMotion.Enable(), vt.PmMouseSgr.Enable()} { if strings.Contains(out, seq) { t.Fatalf("mouse without SGR support emitted enable sequence %q", seq) } } } func TestProcessInitQKeyboardProtocol(t *testing.T) { tests := []struct { name string evs []Event want KeyProtocol }{ { name: "Legacy", want: LegacyKeyboard, }, { name: "Xterm", evs: []Event{&eventXTermKbdMode{Mode: XtermKbdModeExt}}, want: XTermKeyboard, }, { name: "Kitty", evs: []Event{&eventKittyKbdMode{Mode: KittyKbdModeBase}}, want: KittyKeyboard, }, { name: "Win32", evs: []Event{&eventPrivateMode{Mode: vt.PmWin32Input, Status: vt.ModeOff}}, want: Win32Keyboard, }, { name: "KittyPreferredOverXterm", evs: []Event{ &eventXTermKbdMode{Mode: XtermKbdModeExt}, &eventKittyKbdMode{Mode: KittyKbdModeBase}, }, want: KittyKeyboard, }, { name: "Win32PreferredOverKittyAndXterm", evs: []Event{ &eventXTermKbdMode{Mode: XtermKbdModeExt}, &eventKittyKbdMode{Mode: KittyKbdModeBase}, &eventPrivateMode{Mode: vt.PmWin32Input, Status: vt.ModeOff}, }, want: Win32Keyboard, }, { name: "UnchangeableWin32IsIgnored", evs: []Event{&eventPrivateMode{Mode: vt.PmWin32Input, Status: vt.ModeNA}}, want: LegacyKeyboard, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &tScreen{ initQ: make(chan Event, len(tt.evs)+1), } for _, ev := range tt.evs { s.initQ <- ev } s.initQ <- &eventPrimaryAttributes{} s.Lock() s.processInitQ() s.Unlock() if got := s.KeyboardProtocol(); got != tt.want { t.Fatalf("KeyboardProtocol() = %v, want %v", got, tt.want) } }) } } // TestInitScreenStdio just tries to initialize the default screen using standard I/O. // It requires a working tty. func TestInitScreenStdio(t *testing.T) { tty, err := tty.NewStdIoTty() if err != nil { t.Skip("maybe stdin is not a tty?") return } s, err := NewTerminfoScreenFromTty(tty) if err := s.Init(); err != nil { t.Skip("failed to initialize screen", err) tty.Close() return } defer s.Fini() if s.CharacterSet() != "UTF-8" { t.Fatalf("Character Set (%v) not UTF-8", s.CharacterSet()) } drainInput() } func TestNotDevNull(t *testing.T) { tty, err := tty.NewDevTtyFromDev("/dev/null") if err == nil { tty.Close() t.Error("open /dev/null as tty should not have passed") } } func TestNoColorEnv(t *testing.T) { t.Setenv("NO_COLOR", "1") s, err := NewTerminfoScreen() if err != nil { t.Skip("failed to get screen") } if err := s.Init(); err != nil { t.Skip("failed to initialize screen", err) } defer s.Fini() if s.Colors() != 0 { t.Errorf("screen should not have color but had %d", s.Colors()) } drainInput() } func NewMockScreen(t *testing.T, opts ...vt.MockOpt) (vt.MockTerm, Screen) { t.Helper() if runtime.GOOS == "js" { t.Skip("not supported on webasm") return nil, nil } mt := vt.NewMockTerm(opts...) scr, err := NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to get terminal: %v", err) return nil, nil } if err = scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) return nil, nil } return mt, scr } func TestSetTitleStripsOSCControls(t *testing.T) { mt := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}) scr, err := NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to get terminal: %v", err) } scr.SetTitle("good\x07title\x1b\\end") if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() if got := mt.GetTitle(); got != "goodtitle\\end" { t.Fatalf("title not sanitized: %q", got) } } func TestShowNotificationStripsOSCControls(t *testing.T) { mt := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})} scr, err := NewTerminfoScreenFromTty(mt) if err != nil { t.Fatalf("failed to get terminal: %v", err) } if err := scr.Init(); err != nil { t.Fatalf("failed to initialize screen: %v", err) } defer scr.Fini() before := mt.Output() scr.ShowNotification("tit\x07le", "bo\x1b\\dy") delta := mt.Output()[len(before):] if strings.Contains(delta, "tit\x07le") || strings.Contains(delta, "bo\x1b\\dy") { t.Fatalf("notification payload still contains control characters: %q", delta) } if !strings.Contains(delta, "title") || !strings.Contains(delta, "bo\\dy") { t.Fatalf("notification payload missing sanitized strings: %q", delta) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tscreen_unix.go000066400000000000000000000021761520475227200233010ustar00rootroot00000000000000// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package tcell // initialize is used at application startup, and sets up the initial values // including file descriptors used for terminals and saving the initial state // so that it can be restored when the application terminates. func (t *tScreen) initialize() error { var err error if t.tty == nil { t.tty, err = NewDevTty() if err != nil { return err } } return nil } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tscreen_win.go000066400000000000000000000017721520475227200231140ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build windows // +build windows package tcell // initialize is used at application startup, and sets up the initial values // including file descriptors used for terminals and saving the initial state // so that it can be restored when the application terminates. func (t *tScreen) initialize() error { var err error if t.tty == nil { t.tty, err = NewDevTty() if err != nil { return err } } return nil } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty.go000066400000000000000000000021261520475227200214060ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import "github.com/gdamore/tcell/v3/tty" type Tty = tty.Tty // NewDevTty obtains a default tty from the console or TTY (e.g. /dev/tty) for the process. func NewDevTty() (Tty, error) { return tty.NewDevTty() } // NewDevTtyFromDev obtains a tty from the given device path. Not supported on Windows. func NewDevTtyFromDev(dev string) (Tty, error) { return tty.NewDevTtyFromDev(dev) } // NewStdIoTty obtains a tty from stdin and stdout. func NewStdIoTty() (Tty, error) { return tty.NewStdIoTty() } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/000077500000000000000000000000001520475227200210565ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/nonblock_bsd.go000066400000000000000000000031751520475227200240500ustar00rootroot00000000000000// Copyright 2021 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || netbsd || openbsd // +build darwin dragonfly freebsd netbsd openbsd package tty import ( "syscall" "golang.org/x/sys/unix" ) // BSD systems use TIOC style ioctls. // tcSetBufParams is used by the tty driver on UNIX systems to configure the // buffering parameters (minimum character count and minimum wait time in msec.) // This also waits for output to drain first. func tcSetBufParams(fd int, vMin uint8, vTime uint8) error { _ = syscall.SetNonblock(fd, true) tio, err := unix.IoctlGetTermios(fd, unix.TIOCGETA) if err != nil { return err } tio.Cc[unix.VMIN] = vMin tio.Cc[unix.VTIME] = vTime if err = unix.IoctlSetTermios(fd, unix.TIOCSETAW, tio); err != nil { return err } return nil } // tcFlushInput discards any queued input before the caller starts reading from // the tty. This avoids stale bytes, such as delayed mouse reports, from being // delivered to the next foreground application. func tcFlushInput(fd int) error { return unix.IoctlSetPointerInt(fd, unix.TIOCFLUSH, unix.TCIFLUSH) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/nonblock_unix.go000066400000000000000000000030411520475227200242530ustar00rootroot00000000000000// Copyright 2021 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build linux || aix || zos || solaris // +build linux aix zos solaris package tty import ( "syscall" "golang.org/x/sys/unix" ) // tcSetBufParams is used by the tty driver on UNIX systems to configure the // buffering parameters (minimum character count and minimum wait time in msec.) // This also waits for output to drain first. func tcSetBufParams(fd int, vMin uint8, vTime uint8) error { _ = syscall.SetNonblock(fd, true) tio, err := unix.IoctlGetTermios(fd, unix.TCGETS) if err != nil { return err } tio.Cc[unix.VMIN] = vMin tio.Cc[unix.VTIME] = vTime if err = unix.IoctlSetTermios(fd, unix.TCSETSW, tio); err != nil { return err } return nil } // tcFlushInput discards any queued input before the caller starts reading from // the tty. This avoids stale bytes, such as delayed mouse reports, from being // delivered to the next foreground application. func tcFlushInput(fd int) error { return unix.IoctlSetInt(fd, unix.TCFLSH, unix.TCIFLUSH) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/stdin_unix.go000066400000000000000000000067531520475227200236040ustar00rootroot00000000000000// Copyright 2021 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package tty import ( "errors" "fmt" "os" "os/signal" "strconv" "syscall" "time" "golang.org/x/sys/unix" "golang.org/x/term" ) // stdIoTty is an implementation of the Tty API based upon stdin/stdout. type stdIoTty struct { fd int in *os.File out *os.File saved *term.State sig chan os.Signal started bool } func (tty *stdIoTty) Read(b []byte) (int, error) { return tty.in.Read(b) } func (tty *stdIoTty) Write(b []byte) (int, error) { return tty.out.Write(b) } func (tty *stdIoTty) Close() error { return nil } func (tty *stdIoTty) Start() error { if tty.started { return nil } var err error tty.in = os.Stdin tty.out = os.Stdout tty.fd = int(tty.in.Fd()) if !term.IsTerminal(tty.fd) { return errors.New("device is not a terminal") } _ = tty.in.SetReadDeadline(time.Time{}) saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime if err != nil { return err } if err = tcFlushInput(tty.fd); err != nil { _ = term.Restore(tty.fd, saved) return err } tty.saved = saved tty.started = true return nil } func (tty *stdIoTty) Drain() error { _ = tty.in.SetReadDeadline(time.Now()) if err := tcSetBufParams(tty.fd, 0, 0); err != nil { return err } return nil } func (tty *stdIoTty) Stop() error { if err := term.Restore(tty.fd, tty.saved); err != nil { return err } _ = tty.in.SetReadDeadline(time.Now()) tty.NotifyResize(nil) tty.started = false return nil } func (tty *stdIoTty) WindowSize() (WindowSize, error) { size := WindowSize{} ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ) if err != nil { return size, err } w := int(ws.Col) h := int(ws.Row) if w == 0 { w, _ = strconv.Atoi(os.Getenv("COLUMNS")) } if w == 0 { w = 80 // default } if h == 0 { h, _ = strconv.Atoi(os.Getenv("LINES")) } if h == 0 { h = 25 // default } size.Width = w size.Height = h size.PixelWidth = int(ws.Xpixel) size.PixelHeight = int(ws.Ypixel) return size, nil } func (tty *stdIoTty) NotifyResize(resizeQ chan<- bool) { sigQ := tty.sig tty.sig = nil if sigQ != nil { signal.Stop(sigQ) close(sigQ) } if resizeQ == nil { return } sigQ = make(chan os.Signal, 1) signal.Notify(sigQ, syscall.SIGWINCH) tty.sig = sigQ go func() { for range sigQ { select { case resizeQ <- true: default: // queue full, so nvm. } } }() } // NewStdioTty opens a tty using standard input/output. func NewStdIoTty() (Tty, error) { tty := &stdIoTty{ sig: make(chan os.Signal), in: os.Stdin, out: os.Stdout, } var err error tty.fd = int(tty.in.Fd()) if !term.IsTerminal(tty.fd) { return nil, errors.New("not a terminal") } if tty.saved, err = term.GetState(tty.fd); err != nil { return nil, fmt.Errorf("failed to get state: %w", err) } return tty, nil } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/tty.go000066400000000000000000000072431520475227200222330ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tty import "io" // Tty is an abstraction of a tty (traditionally "teletype"). This allows applications to // provide for alternate backends, as there are situations where the traditional /dev/tty // does not work, or where more flexible handling is required. This interface is for use // with the terminfo-style based API. It extends the io.ReadWriter API. It is reasonable // that the implementation might choose to use different underlying files for the Reader // and Writer sides of this API, as part of it's internal implementation. // // Note that the consumer of these interfaces will provide mutual exclusion guarantees // for the methods. Implementations need only be concerned about locking for any // asynchronous functions that they use (e.g. signal handlers.) The exception to this // is that Read and Write may be called concurrently to each other (but only after // a successful Start), and Stop may be called while an outstanding Read or Write // call is pending. (Stop should interrupt any blocking read.) type Tty interface { // Start is used to activate the Tty for use. Upon return the terminal should be // in raw mode, non-blocking, etc. The implementation should take care of saving // any state that is required so that it may be restored when Stop is called. // Start must be idempotent. Start() error // Stop is used to stop using this Tty instance. This may be a suspend, so that other // terminal based applications can run in the foreground. Implementations should // restore any state collected at Start(), and return to ordinary blocking mode, etc. // Drain is called first to drain the input. Once this is called, no more Read // or Write calls will be made until Start is called again. Stop() error // Drain is called before Stop, and ensures that the reader will wake up appropriately // if it was blocked. This workaround is required for /dev/tty on certain UNIX systems // to ensure that Read() does not block forever. This typically arranges for the tty driver // to send data immediately (e.g. VMIN and VTIME both set zero) and sets a deadline on input. // Implementations may reasonably make this a no-op. There will still be control sequences // emitted between the time this is called, and when Stop is called. Drain() error // NotifyResize is used to post a signal that will be written to (non-blocking) if the // system detects that a resize event happened. If the channel is null, then the caller // does not desire such notifications (or no longer desires them.) // The standard UNIX implementation links this to a handler for SIGWINCH. // // If window resize events are delivered inline as part of Read, then the implementation may stub this. // If the caller determines that the underlying terminal can deliver notifications without OS support // (i.e. the terminal supports in-band resize notifications), then it may not call this function at all. NotifyResize(chan<- bool) // WindowSize is called to determine the terminal dimensions. This might be determined // by an ioctl or other means. WindowSize() (WindowSize, error) io.ReadWriteCloser } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/tty_plan9.go000066400000000000000000000145461520475227200233420ustar00rootroot00000000000000//go:build plan9 // +build plan9 // Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tty import ( "bufio" "errors" "fmt" "io" "os" "strconv" "strings" "sync" "sync/atomic" ) // p9Tty implements tcell.Tty using Plan 9's /dev/cons and /dev/consctl. // Raw mode is enabled by writing "rawon" to /dev/consctl while the fd stays open. // Resize notifications are read from /dev/wctl: the first read returns geometry, // subsequent reads block until the window changes (rio(4)). // // References: // - kbdfs(8): cons/consctl rawon|rawoff semantics // - rio(4): wctl geometry and blocking-on-change behavior // - vt(1): VT100 emulator typically used for TUI programs on Plan 9 // // Limitations: // - We assume VT100-level capabilities (often no colors, no mouse). // - Window size is conservative: we return 80x24 unless overridden. // Set LINES/COLUMNS (or TCELL_LINES/TCELL_COLS) to refine. // - Mouse and bracketed paste are not wired; terminfo/xterm queries // are not attempted because vt(1) may not support them. type p9Tty struct { cons *os.File // /dev/cons (read+write) consctl *os.File // /dev/consctl (write "rawon"/"rawoff") wctl *os.File // /dev/wctl (resize notifications) closed atomic.Bool started bool onResize atomic.Value // resize channel wg sync.WaitGroup stopCh chan struct{} } func NewDevTty() (Tty, error) { // tcell signature return newPlan9TTY() } func NewStdIoTty() (Tty, error) { // also required by tcell // On Plan 9 there is no POSIX tty discipline on stdin/stdout; // use /dev/cons explicitly for robustness. return newPlan9TTY() } func NewDevTtyFromDev(_ string) (Tty, error) { // required by tcell // Plan 9 does not have multiple "ttys" in the POSIX sense; // always bind to /dev/cons and /dev/consctl. return newPlan9TTY() } func newPlan9TTY() (Tty, error) { cons, err := os.OpenFile("/dev/cons", os.O_RDWR, 0) if err != nil { return nil, fmt.Errorf("open /dev/cons: %w", err) } consctl, err := os.OpenFile("/dev/consctl", os.O_WRONLY, 0) if err != nil { _ = cons.Close() return nil, fmt.Errorf("open /dev/consctl: %w", err) } // /dev/wctl may not exist (console without rio); best-effort. wctl, _ := os.OpenFile("/dev/wctl", os.O_RDWR, 0) t := &p9Tty{ cons: cons, consctl: consctl, wctl: wctl, stopCh: make(chan struct{}), } return t, nil } func (t *p9Tty) Start() error { if t.started { return nil } if t.closed.Load() { return errors.New("tty closed") } // Recreate stop channel if absent or closed (supports resume). if t.stopCh == nil || isClosed(t.stopCh) { t.stopCh = make(chan struct{}) } // Put console into raw mode; remains active while consctl is open. if _, err := t.consctl.Write([]byte("rawon")); err != nil { return fmt.Errorf("enable raw mode: %w", err) } // Reopen /dev/wctl on resume; best-effort (system console may lack it). if t.wctl == nil { if f, err := os.OpenFile("/dev/wctl", os.O_RDWR, 0); err == nil { t.wctl = f } } if t.wctl != nil { t.wg.Add(1) go t.watchResize() } t.started = true return nil } func (t *p9Tty) Drain() error { // Per tcell docs, this may reasonably be a no-op on non-POSIX ttys. // Read deadlines are not available on plan9 os.File; we rely on Stop(). return nil } func (t *p9Tty) Stop() error { // Signal watcher to stop (if not already). if t.stopCh != nil && !isClosed(t.stopCh) { close(t.stopCh) } // Exit raw mode first. _, _ = t.consctl.Write([]byte("rawoff")) // Closing wctl unblocks watchResize; nil it so Start() can reopen later. if t.wctl != nil { _ = t.wctl.Close() t.wctl = nil } // Ensure watcher goroutine has exited before returning. t.wg.Wait() t.started = false return nil } func (t *p9Tty) Close() error { if t.closed.Swap(true) { return nil } if t.stopCh != nil && !isClosed(t.stopCh) { close(t.stopCh) } _, _ = t.consctl.Write([]byte("rawoff")) _ = t.cons.Close() _ = t.consctl.Close() if t.wctl != nil { _ = t.wctl.Close() t.wctl = nil } t.wg.Wait() return nil } func (t *p9Tty) Read(p []byte) (int, error) { return t.cons.Read(p) } func (t *p9Tty) Write(p []byte) (int, error) { return t.cons.Write(p) } func (t *p9Tty) NotifyResize(resizeQ chan<- bool) { t.onResize.Store(resizeQ) } func (t *p9Tty) WindowSize() (WindowSize, error) { // Strategy: // 1) honor explicit overrides (TCELL_LINES/TCELL_COLS, LINES/COLUMNS), // 2) otherwise return conservative 80x24. // Reading /dev/wctl gives pixel geometry, but char cell metrics are // not generally available to non-draw clients; vt(1) is fixed-cell. lines, cols := envInt("TCELL_LINES"), envInt("TCELL_COLS") if lines == 0 { lines = envInt("LINES") } if cols == 0 { cols = envInt("COLUMNS") } if lines <= 0 { lines = 24 } if cols <= 0 { cols = 80 } return WindowSize{Width: cols, Height: lines}, nil } // watchResize blocks on /dev/wctl reads; each read returns when the window // changes size/position/state, per rio(4). We ignore the parsed geometry and // just notify tcell to re-query WindowSize(). func (t *p9Tty) watchResize() { defer t.wg.Done() r := bufio.NewReader(t.wctl) for { select { case <-t.stopCh: return default: } // Each read delivers something like: // " minx miny maxx maxy visible current\n" // We don't need to parse here; just signal. _, err := r.ReadString('\n') if err != nil { if errors.Is(err, io.EOF) { return } // transient errors: continue } if rq, ok := t.onResize.Load().(chan<- bool); ok && rq != nil { select { case rq <- true: default: } } } } func envInt(name string) int { if s := strings.TrimSpace(os.Getenv(name)); s != "" { if v, err := strconv.Atoi(s); err == nil { return v } } return 0 } // helper: safe check if a channel is closed func isClosed(ch <-chan struct{}) bool { select { case <-ch: return true default: return false } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/tty_unix.go000066400000000000000000000123411520475227200232710ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package tty import ( "errors" "fmt" "os" "os/signal" "strconv" "syscall" "time" "golang.org/x/sys/unix" "golang.org/x/term" ) // Use -1 to differentiate from stdin (fd=0). const uninitializedTtyFd = -1 // devTty is an implementation of the Tty API based upon /dev/tty. type devTty struct { fd int f *os.File saved *term.State sig chan os.Signal dev string started bool } func (tty *devTty) Read(b []byte) (int, error) { return tty.f.Read(b) } func (tty *devTty) Write(b []byte) (int, error) { return tty.f.Write(b) } func (tty *devTty) Close() error { return tty.f.Close() } func (tty *devTty) Start() error { if tty.started { return nil } // We open another copy of /dev/tty. This is a workaround for unusual behavior // observed in macOS, apparently caused when a subshell (for example) closes our // own tty device (when it exits for example). Getting a fresh new one seems to // resolve the problem. (We believe this is a bug in the macOS tty driver that // fails to account for dup() references to the same file before applying close() // related behaviors to the tty.) (Note that when using stdin/stdout instead of // /dev/tty this problem is not observed.) var err error if tty.f, err = os.OpenFile(tty.dev, os.O_RDWR, 0); err != nil { return err } tty.fd = int(tty.f.Fd()) if !term.IsTerminal(tty.fd) { tty.f.Close() return errors.New("device is not a terminal") } _ = tty.f.SetReadDeadline(time.Time{}) saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime if err != nil { tty.f.Close() return err } if err = tcFlushInput(tty.fd); err != nil { _ = term.Restore(tty.fd, saved) tty.f.Close() return err } tty.saved = saved tty.started = true return nil } func (tty *devTty) Drain() error { _ = tty.f.SetReadDeadline(time.Now()) if err := tcSetBufParams(tty.fd, 0, 0); err != nil { return err } return nil } func (tty *devTty) Stop() error { // unconditionally set this, because we cannot recover // if we fail anyway, so this gives the best hope of // picking up the pieces in such a circumstance tty.started = false if err := term.Restore(tty.fd, tty.saved); err != nil { return err } _ = tty.f.SetReadDeadline(time.Now()) tty.NotifyResize(nil) // close our tty device -- we'll get another one if we Start again later. _ = tty.f.Close() tty.fd = uninitializedTtyFd return nil } func (tty *devTty) WindowSize() (WindowSize, error) { size := WindowSize{} fd := tty.fd if tty.fd == uninitializedTtyFd { // If WindowSize is called when the tty isn't yet running, the fd for /dev/tty won't be initialized, // so open the file just long enough to retrieve the window size. f, err := os.OpenFile(tty.dev, os.O_RDWR, 0) if err != nil { return size, err } defer func() { _ = f.Close() }() fd = int(f.Fd()) } ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) if err != nil { return size, err } w := int(ws.Col) h := int(ws.Row) if w == 0 { w, _ = strconv.Atoi(os.Getenv("COLUMNS")) } if w == 0 { w = 80 // default } if h == 0 { h, _ = strconv.Atoi(os.Getenv("LINES")) } if h == 0 { h = 25 // default } size.Width = w size.Height = h size.PixelWidth = int(ws.Xpixel) size.PixelHeight = int(ws.Ypixel) return size, nil } func (tty *devTty) NotifyResize(resizeQ chan<- bool) { sigQ := tty.sig tty.sig = nil if sigQ != nil { signal.Stop(sigQ) close(sigQ) } if resizeQ == nil { return } sigQ = make(chan os.Signal, 1) signal.Notify(sigQ, syscall.SIGWINCH) tty.sig = sigQ go func() { for range sigQ { select { case resizeQ <- true: default: // queue full, so nvm. } } }() } // NewDevTty opens a /dev/tty based Tty. func NewDevTty() (Tty, error) { return NewDevTtyFromDev("/dev/tty") } // NewDevTtyFromDev opens a tty device given a path. This can be useful to bind to other nodes. func NewDevTtyFromDev(dev string) (Tty, error) { tty := &devTty{ fd: uninitializedTtyFd, dev: dev, sig: make(chan os.Signal), } // Only open the file long enough to check that the device // represents a TTY. We will reopen it in start. We do collect // the terminal state so we can restore it later though. if f, err := os.OpenFile(dev, os.O_RDWR, 0); err != nil { return nil, err } else { defer func() { _ = f.Close() }() fd := int(f.Fd()) if !term.IsTerminal(fd) { return nil, errors.New("not a terminal") } if tty.saved, err = term.GetState(fd); err != nil { return nil, fmt.Errorf("failed to get state: %w", err) } } return tty, nil } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/tty_wasm.go000066400000000000000000000010751520475227200232570ustar00rootroot00000000000000//go:build wasm || js // +build wasm js package tty import "errors" // NewDevTty obtains a default tty from the console or TTY (e.g. /dev/tty) for the process. func NewDevTty() (Tty, error) { return nil, errors.New("No tty device on wasm") } // NewDevTtyFromDev obtains a tty from the given device path. Not supported on Windows. func NewDevTtyFromDev(dev string) (Tty, error) { return nil, errors.New("No tty device on wasm") } // NewStdIoTty obtains a tty from stdin and stdout. func NewStdIoTty() (Tty, error) { return nil, errors.New("No tty device on wasm") } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/tty_win.go000066400000000000000000000204331520475227200231040ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build windows // +build windows package tty import ( "encoding/binary" "errors" "sync" "syscall" "time" "unicode/utf16" "unsafe" ) var ( k32 = syscall.NewLazyDLL("kernel32.dll") ) var ( procReadConsoleInput = k32.NewProc("ReadConsoleInputW") procGetNumberOfConsoleInputEvents = k32.NewProc("GetNumberOfConsoleInputEvents") procFlushConsoleInputBuffer = k32.NewProc("FlushConsoleInputBuffer") procWaitForMultipleObjects = k32.NewProc("WaitForMultipleObjects") procSetConsoleMode = k32.NewProc("SetConsoleMode") procGetConsoleMode = k32.NewProc("GetConsoleMode") procGetConsoleScreenBufferInfo = k32.NewProc("GetConsoleScreenBufferInfo") procCreateEvent = k32.NewProc("CreateEventW") procSetEvent = k32.NewProc("SetEvent") ) const ( keyEvent uint16 = 1 mouseEvent uint16 = 2 resizeEvent uint16 = 4 menuEvent uint16 = 8 // don't use focusEvent uint16 = 16 ) const ( w32Infinite = ^uintptr(0) w32WaitObject0 = uintptr(0) ) const ( // Input modes modeExtendFlg = uint32(0x0080) modeMouseEn = uint32(0x0010) modeResizeEn = uint32(0x0008) modeVtInput = uint32(0x0200) // modeCooked = uint32(0x0001) // Output modes modeCookedOut = uint32(0x0001) modeVtOutput = uint32(0x0004) modeNoAutoNL = uint32(0x0008) modeUnderline = uint32(0x0010) // ENABLE_LVB_GRID_WORLDWIDE, needed for underlines // modeWrapEOL = uint32(0x0002) ) type coord struct { x int16 y int16 } type rect struct { left int16 top int16 right int16 bottom int16 } type consoleInfo struct { size coord pos coord attrs uint16 win rect maxsz coord } type inputRecord struct { typ uint16 _ uint16 data [16]byte } type winTty struct { buf chan byte out syscall.Handle in syscall.Handle cancelFlag syscall.Handle running bool stopQ chan struct{} resizeQ chan<- bool cols uint16 rows uint16 pair []uint16 // for surrogate pairs (UTF-16) oimode uint32 // original input mode oomode uint32 // original output mode oscreen consoleInfo wg sync.WaitGroup surrogate rune sync.Mutex } func (w *winTty) Read(b []byte) (int, error) { // first character read blocks var num int select { case c := <-w.buf: b[0] = c num++ case <-w.stopQ: // stopping, so make sure we eat everything, which might require // very short sleeps to ensure all buffered data is consumed. } // second character read is non-blocking for ; num < len(b); num++ { select { case c := <-w.buf: b[num] = c case <-time.After(time.Millisecond * 10): return num, nil } } return num, nil } func (w *winTty) Write(b []byte) (int, error) { esc := utf16.Encode([]rune(string(b))) if len(esc) > 0 { err := syscall.WriteConsole(w.out, &esc[0], uint32(len(esc)), nil, nil) if err != nil { return 0, err } } return len(b), nil } func (w *winTty) Close() error { _ = syscall.Close(w.in) _ = syscall.Close(w.out) return nil } func (w *winTty) Drain() error { close(w.stopQ) time.Sleep(time.Millisecond * 10) _, _, _ = procSetEvent.Call(uintptr(w.cancelFlag)) return nil } func (w *winTty) getConsoleInput() error { // cancelFlag comes first as WaitForMultipleObjects returns the lowest index // in the event that both events are signaled. waitObjects := []syscall.Handle{w.cancelFlag, w.in} // As arrays are contiguous in memory, a pointer to the first object is the // same as a pointer to the array itself. pWaitObjects := unsafe.Pointer(&waitObjects[0]) rv, _, er := procWaitForMultipleObjects.Call( uintptr(len(waitObjects)), uintptr(pWaitObjects), uintptr(0), w32Infinite) // WaitForMultipleObjects returns WAIT_OBJECT_0 + the index. switch rv { case w32WaitObject0: // w.cancelFlag return errors.New("cancelled") case w32WaitObject0 + 1: // w.in var nrec int32 rv, _, er := procGetNumberOfConsoleInputEvents.Call( uintptr(w.in), uintptr(unsafe.Pointer(&nrec))) if rv == 0 { return er } rec := make([]inputRecord, max(nrec, 1)) rv, _, er = procReadConsoleInput.Call( uintptr(w.in), uintptr(unsafe.Pointer(&rec[0])), uintptr(nrec), uintptr(unsafe.Pointer(&nrec))) if rv == 0 { return er } loop: for i := range nrec { ir := rec[i] switch ir.typ { case keyEvent: // we normally only expect to see ascii, but paste data may come in as UTF-16. wc := rune(binary.LittleEndian.Uint16(ir.data[10:])) for _, decoded := range decodeUTF16Rune(&w.surrogate, wc) { for _, chr := range []byte(string(decoded)) { // We normally expect only to see ASCII (win32-input-mode), // but apparently pasted data can arrive in UTF-16 here. select { case w.buf <- chr: case <-w.stopQ: break loop } } } case resizeEvent: w.Lock() w.cols = binary.LittleEndian.Uint16(ir.data[0:]) w.rows = binary.LittleEndian.Uint16(ir.data[2:]) if w.resizeQ != nil { select { case w.resizeQ <- true: default: } } w.Unlock() default: } } return nil default: return er } } func (w *winTty) scanInput() { defer w.wg.Done() for { if e := w.getConsoleInput(); e != nil { return } } } func (w *winTty) Start() error { if w.running { return nil } _, _, _ = procFlushConsoleInputBuffer.Call(uintptr(w.in)) w.stopQ = make(chan struct{}) cf, _, err := procCreateEvent.Call( uintptr(0), uintptr(1), uintptr(0), uintptr(0)) if cf == uintptr(0) { return err } w.running = true w.cancelFlag = syscall.Handle(cf) _, _, _ = procSetConsoleMode.Call(uintptr(w.in), uintptr(modeVtInput|modeResizeEn|modeExtendFlg)) _, _, _ = procSetConsoleMode.Call(uintptr(w.out), uintptr(modeVtOutput|modeNoAutoNL|modeCookedOut|modeUnderline)) w.wg.Add(1) go w.scanInput() return nil } func (w *winTty) Stop() error { w.wg.Wait() _, _, _ = procSetConsoleMode.Call(uintptr(w.in), uintptr(w.oimode)) _, _, _ = procSetConsoleMode.Call(uintptr(w.out), uintptr(w.oomode)) _, _, _ = procFlushConsoleInputBuffer.Call(uintptr(w.in)) w.running = false return nil } func (tty *winTty) NotifyResize(resizeQ chan<- bool) { tty.Lock() tty.resizeQ = resizeQ tty.Unlock() } func (w *winTty) WindowSize() (WindowSize, error) { w.Lock() defer w.Unlock() return WindowSize{Width: int(w.cols), Height: int(w.rows)}, nil } func NewDevTty() (Tty, error) { w := &winTty{} var err error w.in, err = syscall.Open("CONIN$", syscall.O_RDWR, 0) if err != nil { return nil, err } w.out, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0) if err != nil { _ = syscall.Close(w.in) return nil, err } w.buf = make(chan byte, 128) _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oscreen))) _, _, _ = procGetConsoleMode.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oomode))) _, _, _ = procGetConsoleMode.Call(uintptr(w.in), uintptr(unsafe.Pointer(&w.oimode))) w.rows = uint16(w.oscreen.size.y) w.cols = uint16(w.oscreen.size.x) return w, nil } func NewDevTtyFromDev(dev string) (Tty, error) { return nil, errors.New("No tty device on Windows") } func NewStdIoTty() (Tty, error) { w := &winTty{} w.in = syscall.Stdin w.out = syscall.Stdout w.buf = make(chan byte, 128) _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oscreen))) if r, _, err := procGetConsoleMode.Call(uintptr(w.out), uintptr(unsafe.Pointer(&w.oomode))); r == 0 || err != nil { return nil, errors.New("output is not a terminal") } if r, _, err := procGetConsoleMode.Call(uintptr(w.in), uintptr(unsafe.Pointer(&w.oimode))); r == 0 || err != nil { return nil, errors.New("input is not a terminal") } w.rows = uint16(w.oscreen.size.y) w.cols = uint16(w.oscreen.size.x) return w, nil } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/utf16.go000066400000000000000000000024501520475227200223530ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tty import ( "unicode/utf16" "unicode/utf8" ) // decodeUTF16Rune decodes one UTF-16 code unit at a time while preserving // malformed input as replacement characters instead of silently discarding it. func decodeUTF16Rune(surrogate *rune, wc rune) []rune { switch { case wc >= 0xD800 && wc <= 0xDBFF: if *surrogate != 0 { *surrogate = wc return []rune{utf8.RuneError} } *surrogate = wc return nil case wc >= 0xDC00 && wc <= 0xDFFF: if *surrogate == 0 { return []rune{utf8.RuneError} } decoded := utf16.DecodeRune(*surrogate, wc) *surrogate = 0 return []rune{decoded} default: if *surrogate != 0 { *surrogate = 0 return []rune{utf8.RuneError, wc} } return []rune{wc} } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/utf16_test.go000066400000000000000000000030311520475227200234060ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tty import ( "reflect" "testing" ) func TestDecodeUTF16Rune(t *testing.T) { tests := []struct { name string in []rune want []rune }{ { name: "bmp rune", in: []rune{'A'}, want: []rune{'A'}, }, { name: "valid surrogate pair", in: []rune{0xD83D, 0xDE00}, want: []rune{'😀'}, }, { name: "orphaned high before bmp", in: []rune{0xD83D, ' '}, want: []rune{'�', ' '}, }, { name: "orphaned high before high", in: []rune{0xD83D, 0xD83D, 0xDE00}, want: []rune{'�', '😀'}, }, { name: "orphaned low", in: []rune{0xDE00}, want: []rune{'�'}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var surrogate rune var got []rune for _, wc := range tt.in { got = append(got, decodeUTF16Rune(&surrogate, wc)...) } if !reflect.DeepEqual(got, tt.want) { t.Fatalf("decodeUTF16Rune(%U) = %U, want %U", tt.in, got, tt.want) } }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/tty/winsize.go000066400000000000000000000022301520475227200230720ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tty // WindowSize represents the dimensions of the window of a terminal. type WindowSize struct { Width int // Width in characters Height int // Height in characters PixelWidth int // Width in pixels (zero if not available or known) PixelHeight int // Height in pixels (zero if not available or known) } // CellDimensions returns the dimensions of a single cell, in pixels func (ws WindowSize) CellDimensions() (int, int) { if ws.PixelWidth == 0 || ws.PixelHeight == 0 { return 0, 0 } return (ws.PixelWidth / ws.Width), (ws.PixelHeight / ws.Height) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/000077500000000000000000000000001520475227200206675ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/attr.go000066400000000000000000000030501520475227200221660ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt // Attr is a synthetic combination of display attributes for cells, apart // from color which is handled separately. type Attr uint16 const ( Plain = Attr(0) // basic, plain style Bold = Attr(1 << 0) // maybe double strike, or just brighter Blink = Attr(1 << 1) // NB: many terminals do not support it Reverse = Attr(1 << 2) // foreground and background reversed Dim = Attr(1 << 3) // fainter, may also be lower alpha Italic = Attr(1 << 4) // italicized StrikeThrough = Attr(1 << 5) // crossed-out Underline = Attr(1 << 6) // any underline style Overline = Attr(1 << 7) // rarely supported // Underline styles, always mixed with underline, only one can be selected PlainUnderline = Underline DoubleUnderline = Underline | 1<<13 CurlyUnderline = Underline | 2<<13 DottedUnderline = Underline | 3<<13 DashedUnderline = Underline | 4<<13 UnderlineMask = Underline | 7<<13 // bits 13, 14, 15 ) golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/backend.go000066400000000000000000000131651520475227200226130ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt // Backend describes the backend of a terminal. // This can be used to create a real emulator, while allowing the processor // front end to handle the common details of parsing escape sequences, the state // machine, and so forth. Backends support a limited set of common functionality, // including a cursor. They only need to support writing at the cursor. type Backend interface { // GetPrivateMode returns the status of a given private mode. GetPrivateMode(PrivateMode) ModeStatus // SetPrivateMode sets a private mode to the given status. // If either value is invalid, this should simply ignore the operation. SetPrivateMode(PrivateMode, ModeStatus) error // GetSize returns the size of the terminal in characters. // The X and Y are counts, so the bottom right cell should be at coordinate (X-1, Y-1). GetSize() Coord // Colors returns the number of colors this terminal can support. For direct color, // return 1<<24. The XTerm palette is assumed. Monochrome terminals should return 0. Colors() int // Put content at the given location. The string in the cell might be a grapheme cluster, and the width can be // 0, 1, or 2. The backend should try to optimize by keeping style and last coordinates if needed. // A graphical backend could use this and be completely stateless. Put(Coord, Cell) // GetPosition returns the cursor position. GetPosition() Coord // SetPosition sets the cursor position. If the position is out of bounds, // it should be clipped to the window size. SetPosition(Coord) // Reset resets the terminal to default state. Reset() // RaiseResize is called by the emulation layer when it has completed its own internal resizing. // The backend is responsible for sending a signal (if needed) to child processes as part of this // function. (The emulation layer knows nothing of child processes.) RaiseResize() // Buffering is called by the emulator to indicate that the backend should buffer contents because // multiple updates are taking place. This should be treated in addition to mode 2026, if the backend // supports it. (Mode 2026 should only be supported by the backend if it actually supports true // double buffering.) Buffering(bool) // SetCursor is used to set the current cursor style. If the backend does not support changing // the cursor shape, it should implement at least hidden, steady, and blinking (typically as a block). SetCursor(CursorStyle) } // Beeper can be implemented by a backend to indicate it can ring the bell or beep. // This is typically done in response to a 0x07 bell. type Beeper interface { Beep() } // Resizer adds notifications when the window size changes. type Resizer interface { // NotifyResize registers a channel to be posted to if the window size changes. NotifyResize(chan<- bool) } // Titler adds support for setting the window title. (Typically this is OSC 2.) // Note that for security reasons we only support setting this. // We don't bother with icon titles, since few terminal emulators support it, and it // would be hard for us to do this in any portable fashion. type Titler interface { // SetWindowTitle only changes the window title. SetWindowTitle(string) } // MouseReporting determines what mouse events the backend reports. type MouseReporting int const ( MouseDisabled = MouseReporting(iota) // No mouse reports at all. MouseButtons // Report button events only. MouseDrag // Report drag events. MouseMotion // Report motion events (movement). ) // Mouser adds support configuring mouse reporting. // We also assume that a mouse reporter can report focus events. type Mouser interface { SetMouse(MouseReporting) } // Blitter implements a cell-level blit, where a rectangular range of cells is copied from one // location to another. The source and destination may overlap. The old locations will remain // unchanged except of course or cells overwritten by the blit. The content will also be clipped // to the visible dimensions. type Blitter interface { Blit(src, dst, dim Coord) } // Clipboard implements a clipboard or copy buffer for copy/paste activity. // The backend may prevent sending clipboard data by returning an empty string // for the clipboard. Frequently this is done for security reasons. type Clipboard interface { // SetClipboard sets the contents of the clipboard. SetClipboard([]byte) // GetClipboard gets the contents of the clipboard. // It will return nil if the operation is not supported. // An empty clipboard will be []byte{} GetClipboard() []byte } // AdvancedKeyboard provides raw keyboard events, which gives // access to key presses, and releases, physical keys, and so forth. // These keyboards should provide a mapping facility to obtain associated // Unicode text via a layout, as well. type AdvancedKeyboard interface { // IsAdvancedKeyboard returns true if the emulator supports full keyboard // reporting. This must include key press and release events, and mapping // of physical keys (thus permitting key disambiguation). IsAdvancedKeyboard() bool } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/cache_sweep_test.go000066400000000000000000000052131520475227200245240ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "fmt" "testing" "unicode/utf8" ) type benchRuneCache struct { entries []benchRuneCacheEntry n int } type benchRuneCacheEntry struct { r rune s string } func newBenchRuneCache(size int) *benchRuneCache { return &benchRuneCache{entries: make([]benchRuneCacheEntry, size)} } func (c *benchRuneCache) stringFor(r rune) string { if r < utf8.RuneSelf { return asciiRuneStrings[r] } for i := 0; i < c.n; i++ { if c.entries[i].r == r { return c.entries[i].s } } s := string(r) if c.n < len(c.entries) { c.n++ } copy(c.entries[1:c.n], c.entries[:c.n-1]) c.entries[0] = benchRuneCacheEntry{r: r, s: s} return s } var sweepMixedRunes32 = []rune{ '\u0416', '\u0414', '\u042E', '\u042F', '\u041F', '\u041B', '\u0424', '\u042B', '\u042D', '\u0411', '\u0413', '\u0428', '\u00E9', '\u00F6', '\u00FC', '\u00F1', '\u00E7', '\u00F8', '\u00E5', '\u00DF', '\u0142', '\u0111', '\u0127', '\u0131', '\U0001F600', '\U0001F680', '\U0001F9EA', '\U0001F525', '\U0001F355', '\U0001F389', '\U0001F4BB', '\U0001F4E6', } var sweepMixedRunes64 = append(append([]rune{}, sweepMixedRunes32...), '\u0105', '\u0107', '\u0119', '\u0123', '\u0135', '\u0137', '\u0144', '\u015B', '\u015F', '\u0163', '\u0165', '\u017A', '\U0001F31F', '\U0001F4A1', '\U0001F4AF', '\U0001F680', '\U0001F9F0', '\U0001F9D1', '\U0001F9EB', '\U0001FAE0', '\u250C', '\u2510', '\u2514', '\u2518', '\u2500', '\u2502', '\u251C', '\u2524', '\u252C', '\u2534', '\u253C', '\u2571', ) func BenchmarkRuneStringCacheSweep(b *testing.B) { for _, tc := range []struct { name string seq []rune }{ {name: "mixed32", seq: sweepMixedRunes32}, {name: "mixed64", seq: sweepMixedRunes64}, } { b.Run(tc.name, func(b *testing.B) { for _, size := range []int{8, 16, 32, 64} { b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { cache := newBenchRuneCache(size) for _, r := range tc.seq { _ = cache.stringFor(r) } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = cache.stringFor(tc.seq[i%len(tc.seq)]) } }) } }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/coord.go000066400000000000000000000024021520475227200223220ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt // Separate Row and Col types are used to reduce the chance of mixing up the coordinate axes. // We use zero-based coordinates (although VT hardware underneath uses one-based in escape sequences). // The upper left corner of the screen is at coordinate (0, 0). // Row indicates a row number (y position). We use zero based indices, although the VT // standard mostly communicates using 1-based offsets. type Row int // Col indicates a column number (x position). We use zero based indices. type Col int // Coord indicates a coordinate. This can also be used for window sizes. type Coord struct { X Col // Column number, or X position. Y Row // Row number, or Y position. } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/coverage_test.go000066400000000000000000000221221520475227200240470ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "testing" "github.com/gdamore/tcell/v3/color" ) func TestCursorStyleHelpers(t *testing.T) { t.Parallel() tests := []struct { name string style CursorStyle visible bool blinking bool }{ {name: "steady block", style: SteadyBlock, visible: true, blinking: false}, {name: "steady bar", style: SteadyBar, visible: true, blinking: false}, {name: "steady underline", style: SteadyUnderline, visible: true, blinking: false}, {name: "blinking block", style: BlinkingBlock, visible: true, blinking: true}, {name: "hidden blink", style: BlinkingBar.Hide(), visible: false, blinking: true}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := tc.style.IsVisible(); got != tc.visible { t.Fatalf("IsVisible() = %v, want %v", got, tc.visible) } if got := tc.style.IsBlinking(); got != tc.blinking { t.Fatalf("IsBlinking() = %v, want %v", got, tc.blinking) } if got := tc.style.Hide().IsVisible(); got { t.Fatalf("Hide() should clear visibility") } if got := tc.style.Show().IsVisible(); !got { t.Fatalf("Show() should set visibility") } if got := tc.style.Blink().IsBlinking(); !got { t.Fatalf("Blink() should set blinking") } if got := tc.style.Steady().IsBlinking(); got { t.Fatalf("Steady() should clear blinking") } }) } } func TestModeFormatters(t *testing.T) { t.Parallel() tests := []struct { name string pm PrivateMode enable string disable string query string replyOn string replyOff string ansi AnsiMode ansiEnable string ansiDisable string ansiQuery string ansiReply string }{ { name: "auto margin", pm: PmAutoMargin, enable: "\x1b[?7h", disable: "\x1b[?7l", query: "\x1b[?7$p", replyOn: "\x1b[?7;1$y", replyOff: "\x1b[?7;2$y", ansi: AmNewLineMode, ansiEnable: "\x1b[20h", ansiDisable: "\x1b[20l", ansiQuery: "\x1b[20$p", ansiReply: "\x1b[20;1$y", }, } for _, tc := range tests { if got := tc.pm.Enable(); got != tc.enable { t.Fatalf("PrivateMode.Enable() = %q, want %q", got, tc.enable) } if got := tc.pm.Disable(); got != tc.disable { t.Fatalf("PrivateMode.Disable() = %q, want %q", got, tc.disable) } if got := tc.pm.Query(); got != tc.query { t.Fatalf("PrivateMode.Query() = %q, want %q", got, tc.query) } if got := tc.pm.Reply(ModeOn); got != tc.replyOn { t.Fatalf("PrivateMode.Reply(ModeOn) = %q, want %q", got, tc.replyOn) } if got := tc.pm.Reply(ModeOff); got != tc.replyOff { t.Fatalf("PrivateMode.Reply(ModeOff) = %q, want %q", got, tc.replyOff) } if got := tc.ansi.Enable(); got != tc.ansiEnable { t.Fatalf("AnsiMode.Enable() = %q, want %q", got, tc.ansiEnable) } if got := tc.ansi.Disable(); got != tc.ansiDisable { t.Fatalf("AnsiMode.Disable() = %q, want %q", got, tc.ansiDisable) } if got := tc.ansi.Query(); got != tc.ansiQuery { t.Fatalf("AnsiMode.Query() = %q, want %q", got, tc.ansiQuery) } if got := tc.ansi.Reply(ModeOn); got != tc.ansiReply { t.Fatalf("AnsiMode.Reply(ModeOn) = %q, want %q", got, tc.ansiReply) } } } func TestModeStatusChangeable(t *testing.T) { t.Parallel() cases := []struct { status ModeStatus want bool }{ {ModeNA, false}, {ModeOn, true}, {ModeOff, true}, {ModeOnLocked, false}, {ModeOffLocked, false}, } for _, tc := range cases { if got := tc.status.Changeable(); got != tc.want { t.Fatalf("Changeable(%v) = %v, want %v", tc.status, got, tc.want) } } } func TestMockBackendResizeAndBells(t *testing.T) { t.Parallel() mb := NewMockBackend(MockOptSize{X: 2, Y: 2}, MockOptColors(0)).(*mockBackend) if got := mb.Bells(); got != 0 { t.Fatalf("initial bells = %d, want 0", got) } mb.Beep() mb.Beep() if got := mb.Bells(); got != 2 { t.Fatalf("bells after beep = %d, want 2", got) } mb.Put(Coord{X: 0, Y: 0}, Cell{C: "x", S: BaseStyle, W: 1}) mb.SetPosition(Coord{X: 1, Y: 1}) mb.SetSize(Coord{X: 3, Y: 1}) if got := mb.GetSize(); got != (Coord{X: 3, Y: 1}) { t.Fatalf("GetSize() = %v, want {3 1}", got) } if got := mb.GetCell(Coord{X: 0, Y: 0}); got.C != "x" || got.W != 1 { t.Fatalf("resized cell preserved incorrectly: %#v", got) } if got := mb.GetCell(Coord{X: 2, Y: 0}); got.S != BaseStyle { t.Fatalf("new cell style = %#v, want BaseStyle", got.S) } } func TestStringCacheHelpers(t *testing.T) { t.Parallel() mb := NewMockBackend(MockOptSize{X: 4, Y: 1}, MockOptColors(0)).(*mockBackend) em := NewEmulator(mb).(*emulator) if got := em.runeString('a'); got != "a" { t.Fatalf("runeString(ascii) = %q, want %q", got, "a") } if got := em.runeString('\u03c0'); got != "π" { t.Fatalf("runeString(non-ascii) = %q, want %q", got, "π") } if got := em.runeString('\u03c0'); got != "π" { t.Fatalf("runeString cache hit returned %q, want %q", got, "π") } if got := em.clusterString([]byte("e\u0301")); got != "e\u0301" { t.Fatalf("clusterString(combining) = %q, want %q", got, "e\u0301") } if got := em.clusterString([]byte("e\u0301")); got != "e\u0301" { t.Fatalf("clusterString cache hit returned %q, want %q", got, "e\u0301") } } func TestEmulatorBellDispatch(t *testing.T) { t.Parallel() mb := NewMockBackend(MockOptSize{X: 4, Y: 1}, MockOptColors(0)).(*mockBackend) em := NewEmulator(mb).(*emulator) em.style = BaseStyle.WithFg(color.White).WithBg(color.Black) em.defaultStyle = em.style em.beep() if got := mb.Bells(); got != 1 { t.Fatalf("bell count = %d, want 1", got) } } func TestShouldCheckGrapheme(t *testing.T) { t.Parallel() cases := []struct { name string prev byte r rune want bool }{ {name: "crlf", prev: '\r', r: '\n', want: true}, {name: "ascii false", prev: 'a', r: 'b', want: false}, {name: "mark", prev: 'e', r: '\u0301', want: true}, {name: "zwj", prev: 'x', r: '\u200d', want: true}, {name: "vs16", prev: 'x', r: '\uFE0F', want: true}, {name: "supplementary vs", prev: 'x', r: '\U000E0100', want: true}, {name: "plain non-ascii", prev: 'x', r: 'π', want: false}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := shouldCheckGrapheme(tc.prev, tc.r); got != tc.want { t.Fatalf("shouldCheckGrapheme(%q, %q) = %v, want %v", tc.prev, tc.r, got, tc.want) } }) } } func TestPutRunePaths(t *testing.T) { newEm := func(width int) *emulator { em := NewEmulator(NewMockBackend(MockOptSize{X: Col(width), Y: 1}, MockOptColors(0))).(*emulator) em.style = BaseStyle.WithFg(color.White).WithBg(color.Black) em.defaultStyle = em.style em.localModes[PmGraphemeClusters] = ModeOn em.localModes[PmAutoMargin] = ModeOn em.cells[0].S = em.style em.cells[0].W = 1 return em } t.Run("ascii falls through", func(t *testing.T) { em := newEm(4) em.cells[0].C = "a" em.lastIndex = 1 em.setPosition(Coord{X: 1, Y: 0}) em.putRune('b') if got := em.cells[1].C; got != "b" { t.Fatalf("cell[1].C = %q, want %q", got, "b") } if got := em.cells[0].C; got != "a" { t.Fatalf("cell[0].C = %q, want %q", got, "a") } }) t.Run("combining mark extends cluster", func(t *testing.T) { em := newEm(4) em.cells[0].C = "e" em.lastIndex = 1 em.setPosition(Coord{X: 1, Y: 0}) em.putRune('\u0301') if got := em.cells[0].C; got != "e\u0301" { t.Fatalf("cell[0].C = %q, want %q", got, "e\u0301") } if got := em.cells[0].W; got != 1 { t.Fatalf("cell[0].W = %d, want 1", got) } }) t.Run("wide grapheme clears next cell", func(t *testing.T) { em := newEm(4) em.cells[0].C = "\u2764" em.cells[1].C = "x" em.cells[1].S = em.style em.cells[1].W = 1 em.lastIndex = 1 em.setPosition(Coord{X: 1, Y: 0}) em.putRune('\uFE0F') if got := em.cells[0].W; got != 2 { t.Fatalf("cell[0].W = %d, want 2", got) } if got := em.cells[1].C; got != "" || em.cells[1].W != 0 { t.Fatalf("cell[1] not cleared: %#v", em.cells[1]) } }) t.Run("auto wrap when width reaches margin", func(t *testing.T) { em := newEm(2) em.cells[0].C = "a" em.lastIndex = 1 em.setPosition(Coord{X: 1, Y: 0}) em.putRune('宽') if !em.autoWrap { t.Fatalf("autoWrap = false, want true") } }) t.Run("grapheme extension at margin preserves wrap", func(t *testing.T) { em := newEm(2) em.cells[1].C = "\u2764" em.cells[1].S = em.style em.cells[1].W = 1 em.lastIndex = 2 em.setPosition(Coord{X: 1, Y: 0}) em.putRune('\uFE0F') if !em.autoWrap { t.Fatalf("autoWrap = false, want true") } if got := em.getPosition(); got.X != 1 { t.Fatalf("cursor X = %d, want 1", got.X) } }) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/cursor.go000066400000000000000000000031221520475227200225310ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt // CursorStyle represents the style of cursor, and covers the shape, whether it // blinks, and whether it is visible. Cursor color is handled separately, if at all. type CursorStyle byte const ( SteadyBlock = CursorStyle(iota) // The default SteadyBar SteadyUnderline BlinkingBlock = SteadyBlock | blinkingCursor BlinkingBar = SteadyBar | blinkingCursor BlinkingUnderline = SteadyUnderline | blinkingCursor hiddenCursor = CursorStyle(1 << 7) // If set, cursor should be hidden blinkingCursor = CursorStyle(1 << 6) // If set, cursor should blink ) func (cs CursorStyle) IsVisible() bool { return cs&hiddenCursor == 0 } func (cs CursorStyle) IsBlinking() bool { return cs&blinkingCursor != 0 } func (cs CursorStyle) Hide() CursorStyle { return cs | hiddenCursor } func (cs CursorStyle) Show() CursorStyle { return cs &^ hiddenCursor } func (cs CursorStyle) Blink() CursorStyle { return cs | blinkingCursor } func (cs CursorStyle) Steady() CursorStyle { return cs &^ blinkingCursor } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/emulate.go000066400000000000000000002333751520475227200226670ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "bytes" "encoding/base64" "errors" "fmt" "io" "slices" "strconv" "strings" "sync" "unicode" "unicode/utf8" "github.com/clipperhouse/uax29/v2/graphemes" "github.com/gdamore/tcell/v3/color" ) // Emulator is a terminal emulator API. It implements the state machinery // (escape parsing and so forth) associated with being a terminal emulator. // The backend handles rendering the content, and some low level details. // // NOTE: This is not a committed interface yet, its entirely a work in progress. type Emulator interface { // SetId sets our identity. SetId(name string, version string) // SendRaw sends raw data to the consumer. This bypasses the normal encoding, // so it should be used with caution. SendRaw([]byte) // KeyEvent injects a keyboard event into the emulator, which will ultimately // result in data being sent via SendRaw. KeyEvent(ev KeyEvent) // ResizeEvent is called by a backend when the terminal has resized // This will send in-band resize notifications if the client has requested them. ResizeEvent(Coord) // MouseEvent is called by a backend to report mouse activity. MouseEvent(ev MouseEvent) // FocusEvent is called by a backend to report that focus is gained (true) or lost (false). FocusEvent(bool) // Drain waits until any queued but not processed input has finished processing. // It also wakes the reader. Drain() error // Start starts processing. Start() error // Stop stops processing. Stop() error // Reader reads data from the emulator. These are bytes that would be transmitted // to a remote party. io.Reader // Writer writes data to the emulator. These are commands that the emulator should process. io.Writer } // Style represents the styling of a cell. // This is an interface to prevent direct modification. type Style interface { Fg() color.Color // Fg returns the foreground color. Bg() color.Color // Bg returns the background color. Uc() color.Color // Uc returns the underline color.k Attr() Attr // Attr returns the associated attributes. Url() (string, string) // Url returns the URL and associated id if one was set. WithFg(color.Color) Style // WithFg creates a new style with the foreground WithBg(color.Color) Style // WithBg creates a new style with the background. WithUc(color.Color) Style // WithUc creates a new style with the underline color WithAttr(Attr) Style // WithAttr creates a new style with the attributes. WithUrl(string, string) Style // WithLink creates a new style with the URL and id. Equal(Style) bool // Equal returns true if the styles are the same. } // styleStruct implements Style. Note that it is possible to make this even more // compact, but we don't think further optimization here on size will justify the // complexity and runtime performance hit to do so. We're also already only storing // a class reference to this per cell. type styleStruct struct { fg color.Color bg color.Color uc color.Color // underline color attr Attr url string // URL id string // Id for link } var BaseStyle = &styleStruct{} var asciiRuneStrings = func() [utf8.RuneSelf]string { var table [utf8.RuneSelf]string for i := 0; i < utf8.RuneSelf; i++ { table[i] = string(rune(i)) } return table }() const ( runeStringCacheSize = 32 clusterStringCacheSize = 32 clusterStringCacheMaxLen = 128 ) type runeStringCache struct { entries [runeStringCacheSize]runeStringCacheEntry n int } type runeStringCacheEntry struct { r rune s string } func (c *runeStringCache) stringFor(r rune) string { for i := 0; i < c.n; i++ { if c.entries[i].r == r { return c.entries[i].s } } s := string(r) n := c.n if n < len(c.entries) { n++ } copy(c.entries[1:n], c.entries[:n-1]) c.entries[0] = runeStringCacheEntry{r: r, s: s} c.n = n return s } type clusterStringCache struct { entries [clusterStringCacheSize]clusterStringCacheEntry n int } type clusterStringCacheEntry struct { n int b [clusterStringCacheMaxLen]byte s string } func (c *clusterStringCache) stringFor(cluster []byte) string { if len(cluster) == 0 { return "" } if len(cluster) > clusterStringCacheMaxLen { return string(cluster) } for i := 0; i < c.n; i++ { e := &c.entries[i] if e.n == len(cluster) && bytes.Equal(e.b[:e.n], cluster) { return e.s } } s := string(cluster) n := c.n if n < len(c.entries) { n++ } copy(c.entries[1:n], c.entries[:n-1]) e := &c.entries[0] e.n = len(cluster) copy(e.b[:], cluster) e.s = s c.n = n return s } func (ss *styleStruct) Fg() color.Color { return ss.fg } func (ss *styleStruct) Bg() color.Color { return ss.bg } func (ss *styleStruct) Uc() color.Color { return ss.uc } func (ss *styleStruct) Attr() Attr { return ss.attr } func (ss *styleStruct) Url() (string, string) { return ss.url, ss.id } func (ss *styleStruct) WithFg(fg color.Color) Style { ns := *ss; ns.fg = fg; return &ns } func (ss *styleStruct) WithBg(bg color.Color) Style { ns := *ss; ns.bg = bg; return &ns } func (ss *styleStruct) WithUc(uc color.Color) Style { ns := *ss; ns.uc = uc; return &ns } func (ss *styleStruct) WithAttr(a Attr) Style { ns := *ss; ns.attr = a; return &ns } func (ss *styleStruct) WithUrl(url, id string) Style { ns := *ss; ns.url = url; ns.id = id; return &ns } func (ss *styleStruct) Equal(other Style) bool { if s2, ok := other.(*styleStruct); ok { return *ss == *s2 } // We have chosen not to support alternative implementations for this compare. // We could delegate to the other style, but that could lead to a loop if they // do the same. return (false) } // Cell is a representation of a display cell. Most consumers will not need this. // Note, this is not the simplest possible representation, and a 256x256 cell // display is going to need about 3MB to store it all, but it's simple, and adequate // to retain pretty much all of what we need for Unicode. We could save some memory // by using explicit struct pointers and by eliminating grapheme cluster support, // but modern users expect these features. type Cell struct { C string // Content, it will be a grapheme cluster S Style // Style, a pointer is used efficiency W int // Display width (0, 1, or 2) } // EmulatorOpt configures an Emulator. type EmulatorOpt interface { setEmulatorOpt(*emulator) } // EmulatorOpt8BitControls enables parsing of C1 controls, such as CSI and OSC, // when they are presented as raw 8-bit bytes or UTF-8 encoded C1 controls. // The default is to only accept the 7-bit ESC-prefixed forms. type EmulatorOpt8BitControls struct{} func (EmulatorOpt8BitControls) setEmulatorOpt(em *emulator) { em.c1Allowed = true em.c1Enabled = true } // NewEmulator creates an emulator instance on top of the given backend. // The input is relative to the emulator, so it receives data from the host, // whereas the emulator sends data to the application through the output. func NewEmulator(be Backend, opts ...EmulatorOpt) Emulator { stopQ := make(chan bool) defStyle := BaseStyle.WithFg(color.Silver).WithBg(color.Black) em := &emulator{ be: be, inBuf: &bytes.Buffer{}, writeQ: make(chan any), readQ: make(chan any, 1024), stopQ: stopQ, style: defStyle, defaultStyle: defStyle, ansiModes: map[AnsiMode]ModeStatus{ AmNewLineMode: ModeOff, }, localModes: map[PrivateMode]ModeStatus{ PmAppCursor: ModeOff, PmAutoMargin: ModeOn, PmAutoRepeat: ModeOn, PmVT52: ModeOnLocked, // we never support VT52 mode (note ON means ANSI mode) PmLeftRightMargin: ModeOff, PmShowCursor: ModeOn, PmBlinkCursor: ModeOn, PmWin32Input: ModeOff, }, mouseReports: MouseDisabled, } for _, opt := range opts { opt.setEmulatorOpt(em) } if _, ok := be.(Resizer); ok { em.localModes[PmResizeReports] = ModeOff } // add mouse modes - we also add focus reporting mode if _, ok := be.(Mouser); ok { em.localModes[PmMouseX10] = ModeOff em.localModes[PmMouseButton] = ModeOff em.localModes[PmMouseDrag] = ModeOff em.localModes[PmMouseMotion] = ModeOff em.localModes[PmMouseSgr] = ModeOff em.localModes[PmFocusReports] = ModeOff } if ak, ok := be.(AdvancedKeyboard); ok && ak.IsAdvancedKeyboard() { em.localModes[PmWin32Input] = ModeOff } em.size = em.be.GetSize() em.topMargin = 0 em.botMargin = em.size.Y - 1 em.ltMargin = 0 em.rtMargin = em.size.X - 1 em.cells = make([]Cell, int(em.size.X)*int(em.size.Y)) em.graphemeIter = *graphemes.FromBytes(nil) close(stopQ) em.inb = em.inbInit em.cursor = BlinkingBlock em.be.SetCursor(em.cursor) return em } // emulator is an implementation of a terminal emulator built on top of // a Backend. It implements the common escape sequence handling and high // level functionality that a real terminal emulator, or a mock, would need. type emulator struct { stopQ chan bool writeQ chan any // queues data from application to emulator readQ chan any // queues data from emulator to application be Backend inBuf *bytes.Buffer // buffer queued for input inb func(byte) // input byte function (faster than state switch) style Style defaultStyle Style utfLen int pos Coord buffering uint // reference count - number of (re-entrant) buffering calls autoWrap bool // next character will wrap (auto margin, deferred until char emitted) c1Allowed bool // allow C1 controls in raw 8-bit and UTF-8 encodings c1Enabled bool // C1 controls are currently enabled c1Prefix bool // string parser has seen the first byte of a UTF-8 encoded C1 control appKeyPad bool // use application key pad keys? name string // name of this emulator (used for extended attributes) vers string // version string of this emulator (used for extended attributes) saved savedCursor // data saved by save cursor (DECSC) sendLock sync.Mutex // ensures that send data cannot be intermixed modeLock sync.RWMutex // protects localModes/ansiModes and related derived state tabStops []Col // tab stops, ordered. if nil every 8th position is used lastIndex int // index of last cell written + 1 (for grapheme clustering) (zero means none) graphemeBuf []byte // scratch buffer for grapheme clustering checks graphemeIter graphemes.Iterator[[]byte] runeStrings runeStringCache clusterStrings clusterStringCache cells []Cell // content of cells, we have to maintain our own copy (backend might or might not) mouseReports MouseReporting // whether we have enabled mouse reports size Coord // physical window size topMargin Row // top margin, scrollable region includes this row botMargin Row // bottom margin, scrollable region includes this row ltMargin Col // left margin, scrollable region to the right rtMargin Col // right margin, scrollable region to the left cursor CursorStyle // current cursor style (visibility, blink, shape) localModes map[PrivateMode]ModeStatus // some modes we handle locally ansiModes map[AnsiMode]ModeStatus // some modes we handle locally } // savedCursor is the content we save when saving the cursor, // which is more than just the cursor location itself. type savedCursor struct { pos Coord style Style autoWrap bool // We should probably store OSC 8 data here, eventually. // TODO: Character sets // TODO: Origin mode (DEC Mode 6) } func (em *emulator) saveCursor() { em.saved.pos = em.getPosition() em.saved.style = em.style em.saved.autoWrap = em.autoWrap } func (em *emulator) restoreCursor() { em.setPosition(em.saved.pos) em.autoWrap = em.saved.autoWrap em.style = em.saved.style } func (em *emulator) bufferingStart() { em.buffering++ if em.buffering == 1 { em.be.Buffering(true) } } func (em *emulator) bufferingEnd() { em.buffering-- if em.buffering == 0 { em.be.Buffering(false) } } // inbInit processes bytes received in the "default" state. Most often these are just // text characters to display on screen, but if ESC is seen then additional processing will result. func (em *emulator) inbInit(b byte) { em.inBuf.Reset() // hot path - just doing ASCII directly. if b >= ' ' && b < 0x7f { // plain ascii em.putRune(rune(b)) return } // For C1 controls, the raw 8-bit form is the same as ESC followed by // (b - 0x40). This is disabled by default because modern protocols // generally treat these forms as insecure. if b >= 0x80 && b <= 0x9f { if em.c1Enabled { em.inbEsc(b - 0x40) } return } // TODO: To support non-UTF-8 locales, include a check here for > 0x7F. Those locales // might preclude 8-bit control sequences - 8859 character sets are fine, but e.g. KOI8, // and ShiftJIS use values in those ranges. switch b { case 0x1b: // ESC (escape) em.inb = em.inbEsc case 0x07: // BEL (bell) em.beep() case 0x08: // BS (backspace) em.moveLeft() case 0x09: // horizontal tab em.nextTab() case 0x0a, 0x0b, 0x0c: // LF (line feed), VF, FF em.processLineFeed() case 0x0d: // CR (carriage return) em.processCarriageReturn() case 0x0e: // TODO: SO em.lastIndex = 0 case 0x0f: // TODO: SI em.lastIndex = 0 case 0x18: //TODO Cancel (reset parser) em.lastIndex = 0 default: // TODO: consider separating Unicode from other 8-bit character sets if b&0xE0 == 0xC0 { em.utfLen = 2 em.inb = em.inbUTF em.inBuf.WriteByte(b) } else if b&0xF0 == 0xE0 { em.utfLen = 3 em.inb = em.inbUTF em.inBuf.WriteByte(b) } else if b&0xF8 == 0xF0 { em.utfLen = 4 em.inb = em.inbUTF em.inBuf.WriteByte(b) } else { em.lastIndex = 0 em.beep() } } } // inbEsc processes the next byte after an escape character is seen. func (em *emulator) inbEsc(b byte) { // By default, reset to init state. Other states will be set explicitly as needed. em.inb = em.inbInit em.lastIndex = 0 switch b { case '[': em.inb = em.inbCSI case ']': em.inb = em.inbOSC case ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/': // 0x20 - 0x2F -- usually followed by just one terminating character, but could include others em.inb = em.inbNF em.inBuf.WriteByte(b) case '^': // privacy message (PM) em.inb = em.inbStr case '_': // application program command (APC) em.inb = em.inbStr case '=': em.appKeyPad = true case '>': em.appKeyPad = false case 'D': // down one line (IND) em.processIndex() case 'E': // next line (NEL) em.nextLine() case 'H': // set tab stop (HTS) - VT52 is go home, but we do not support VT52 em.setTabStop(em.getPosition().X) case 'M': // up one line (RI) em.processReverseIndex() case 'N': // single shift two (SS2) (TODO) case 'O': // single shift three (SS3) (TODO) case 'P': em.inb = em.inbStr // device control string (DCS) (TODO) case 'X': // start of string (SOS) em.inb = em.inbStr case 'Z': // DECID, obsolete form to get primary DA em.sendDA() case 'c': // RIS, soft reset em.softReset() case '6': // back index (DECBI, VT420, not widely supported) em.moveLeft() case '7': // save cursor (DECSC, VT100) em.saveCursor() case '8': // restore cursor (DECRC, VT100) em.restoreCursor() case '9': // forward index (DECFI, VT420, not widely supported) em.moveRight() default: // ESC-V and ESC-W are for guarded area (TODO) em.inb = em.inbInit } } // inbNF processes bytes that are part of an "nF" sequence (see ECMA-48). func (em *emulator) inbNF(b byte) { if b >= 0x20 && b <= 0x2F { em.inBuf.WriteByte(b) return } if b < 0x20 || b > 0x7E { // not a valid sequence em.beep() em.inb = em.inbInit return } em.inBuf.WriteByte(b) em.inb = em.inbInit switch em.inBuf.String() { case "#8": // DECALN - fill screen with 'E' size := em.size em.autoWrap = false em.topMargin = 0 em.botMargin = em.size.Y - 1 em.ltMargin = 0 em.rtMargin = em.size.X - 1 em.setPosition(Coord{0, 0}) em.style = em.style.WithAttr(Plain) // TODO: Reset DECOM (when we implement origin mode) for row := range size.Y { ix := em.index(Coord{X: 0, Y: row}) for col := range size.X { em.cells[ix].S = em.style em.cells[ix].C = "E" em.cells[ix].W = 1 em.be.Put(Coord{X: col, Y: row}, em.cells[ix]) ix++ } } // most implementations leave the cursor at home for this em.setPosition(Coord{0, 0}) // case "%@": // TODO: select 8859-1 // case "%G": // TODO: select UTF-8 // case "(A": // TODO: select G0 as UK // case "(B": // TODO: select G0 as US // case "(C", "(5": // TODO: select G0 as Finnish // case "(H", "(7": // TODO: select G0 as Swedish // case "(K": // TODO: select G0 as German // case "(Q", "(9": // TODO: select G0 as French Canadian // case "(R", "(f": // TODO: select G0 as French // case "(Y": // TODO: select G0 as Italian case " F": // S7C1T - send/use 7-bit C1 controls em.c1Enabled = false case " G": // S8C1T - send/use 8-bit C1 controls if em.c1Allowed { em.c1Enabled = true } } } // inbCSI handles bytes that are part of a CSI based sequence. func (em *emulator) inbCSI(b byte) { if (b >= 0x30) && (b <= 0x3F) { em.inBuf.WriteByte(b) // parameter bytes } else if (b >= 0x20) && (b <= 0x2F) { em.inBuf.WriteByte(b) // intermediate bytes } else if b >= 0x40 && (b <= 0x7F) { em.inb = em.inbInit em.processCsi(b) } else { // error state em.beep() em.inb = em.inbInit } } // inbOSC handles bytes that are part of on OSC sequences (operating system command). func (em *emulator) inbOSC(b byte) { if em.inbStringC1(b, em.processOSC) { return } switch b { case 0x9c: if em.c1Enabled { em.inb = em.inbInit em.processOSC() } case 0x07: em.inb = em.inbInit em.processOSC() case '\\': if buf := em.inBuf.Bytes(); len(buf) > 0 && buf[len(buf)-1] == 0x1b { em.inb = em.inbInit em.inBuf.Truncate(em.inBuf.Len() - 1) em.processOSC() } else { em.inBuf.WriteByte(b) } default: em.inBuf.WriteByte(b) } } // inbStr handles PM, SOS, and any other string we want to consume and discard. func (em *emulator) inbStr(b byte) { if em.inbStringC1(b, nil) { return } switch b { case 0x9c: if em.c1Enabled { em.inb = em.inbInit } case 0x07: em.inb = em.inbInit case '\\': if buf := em.inBuf.Bytes(); len(buf) > 0 && buf[len(buf)-1] == 0x1b { em.inb = em.inbInit em.inBuf.Truncate(em.inBuf.Len() - 1) } else { em.inBuf.WriteByte(b) } default: em.inBuf.WriteByte(b) } } func (em *emulator) inbStringC1(b byte, done func()) bool { if em.c1Prefix { em.c1Prefix = false if b >= 0x80 && b <= 0x9f { if em.c1Enabled && b == 0x9c { em.inb = em.inbInit if done != nil { done() } } return true } em.inBuf.WriteByte(0xc2) } if b == 0xc2 { em.c1Prefix = true return true } return false } // inbUTF handles continuation bytes for UTF-8 sequences. func (em *emulator) inbUTF(b byte) { if b&0xC0 == 0x80 { // good continuation byte em.inBuf.WriteByte(b) if em.inBuf.Len() == em.utfLen { em.inb = em.inbInit r, _, err := em.inBuf.ReadRune() if err != nil { em.beep() } else { if r >= 0x80 && r <= 0x9f { if em.c1Enabled { em.inbEsc(byte(r) - 0x40) } } else { em.putRune(r) } } } } else { em.beep() em.inb = em.inbInit } } func (em *emulator) beep() { if beeper, ok := em.be.(Beeper); ok { beeper.Beep() } } // numericParams splits the string consisting of numeric parameters into integers. // It ensures a minimum number are present (needed for some safety cases). // Empty strings default to zero. func numericParams(str string, minimumLen int) ([]int, error) { ps := strings.Split(str, ";") pi := make([]int, max(len(ps), minimumLen)) for i, str := range ps { if str != "" { if iv, err := strconv.Atoi(str); err != nil { return nil, err } else { pi[i] = iv } } } return pi, nil } // parseSgrColor grabs either 2 arguments, or 4 arguments for palette or rgb values // used with SGR 38 and 48. The arguments must be numbers, and are returned as such. func (em *emulator) parseSgrColor(args []string, words []string) (color.Color, []string, error) { if len(args) == 0 { if len(words) == 0 { return color.None, nil, errors.New("invalid color specification") } switch words[0] { case "2": // RGB (direct) if len(words) < 4 { return color.None, nil, errors.New("invalid color specification") } args = words[:4] words = words[4:] case "5": // palette index if len(words) < 2 { return color.None, nil, errors.New("invalid color specification") } args = words[:2] words = words[2:] default: return color.None, nil, errors.New("invalid color specification") } } switch args[0] { case "2": // RGB color if len(args) < 4 || em.be.Colors() <= 256 { return color.None, nil, errors.New("invalid color specification") } r, re := strconv.Atoi(args[1]) g, ge := strconv.Atoi(args[2]) b, be := strconv.Atoi(args[3]) if re != nil || ge != nil || be != nil || r > 255 || g > 255 || b > 255 || r < 0 || g < 0 || b < 0 { return color.None, nil, errors.New("invalid color specification") } return color.NewRGBColor(int32(r), int32(g), int32(b)), words, nil case "5": // palette index if len(args) < 2 { return color.None, nil, errors.New("invalid color specification") } p, e := strconv.Atoi(args[1]) if e != nil || p < 0 || p >= em.be.Colors() { return color.None, nil, errors.New("invalid color specification") } return color.IsValid | color.Color(p&0xff), words, nil } return color.None, nil, errors.New("invalid color specification") } func (em *emulator) pickColor(c color.Color, def color.Color) (color.Color, bool) { numColors := em.be.Colors() if numColors == 0 { return color.None, false } if c.Valid() { if c.IsRGB() { if numColors > 256 { return c, true } } if (int(c) & 255) < numColors { return c, true } } if c == color.Reset { return def, true } return color.None, false } // processSgr processes SGR commands (things that change how characters are displayed). func (em *emulator) processSgr(str string) { words := strings.Split(str, ";") // technically parameters for 38 or 48 should be separated by colons, but due to historical // accident it is more common to see semicolon separation. Underline styles are also separated // by a colon, if present. if len(words) == 0 { words = []string{"0"} } for len(words) > 0 { // we do this instead of a range so we can lop off // multiple words for SGR38 and 48. word := words[0] words = words[1:] if word == "" { word = "0" } args := []string(nil) if strings.Contains(word, ":") { args = strings.Split(word, ":") word = args[0] args = args[1:] } v, err := strconv.Atoi(word) if err != nil { // just swallow it for now continue } switch v { case 0: em.style = em.defaultStyle case 1: em.style = em.style.WithAttr((em.style.Attr() &^ Dim) | Bold) case 2: em.style = em.style.WithAttr((em.style.Attr() &^ Bold) | Dim) case 3: em.style = em.style.WithAttr(em.style.Attr() | Italic) case 4: em.style = em.style.WithAttr((em.style.Attr() &^ UnderlineMask) | Underline) if len(args) > 0 { switch args[0] { case "2": em.style = em.style.WithAttr(em.style.Attr() | DoubleUnderline) case "3": em.style = em.style.WithAttr(em.style.Attr() | CurlyUnderline) case "4": em.style = em.style.WithAttr(em.style.Attr() | DottedUnderline) case "5": em.style = em.style.WithAttr(em.style.Attr() | DashedUnderline) } } case 5, 6: em.style = em.style.WithAttr(em.style.Attr() | Blink) case 7: em.style = em.style.WithAttr(em.style.Attr() | Reverse) case 8: // ignore, its for invisible case 9: em.style = em.style.WithAttr(em.style.Attr() | StrikeThrough) case 21: // Doubly underlined, per ECMA em.style = em.style.WithAttr((em.style.Attr() &^ UnderlineMask) | DoubleUnderline) case 22: em.style = em.style.WithAttr(em.style.Attr() &^ (Bold | Dim)) case 23: em.style = em.style.WithAttr(em.style.Attr() &^ Italic) case 24: em.style = em.style.WithAttr(em.style.Attr() &^ UnderlineMask) case 25: em.style = em.style.WithAttr(em.style.Attr() &^ Blink) case 27: em.style = em.style.WithAttr(em.style.Attr() &^ Reverse) case 29: em.style = em.style.WithAttr(em.style.Attr() &^ StrikeThrough) case 30, 31, 32, 33, 34, 35, 36, 37: // simple foreground colors if c, ok := em.pickColor(color.Black+color.Color(v-30), em.defaultStyle.Fg()); ok { em.style = em.style.WithFg(c) } case 38: if c, rest, err := em.parseSgrColor(args, words); err == nil { words = rest em.style = em.style.WithFg(c) } case 39: if c, ok := em.pickColor(color.Reset, em.defaultStyle.Fg()); ok { em.style = em.style.WithFg(c) } case 40, 41, 42, 43, 44, 45, 46, 47: // simple background colors if c, ok := em.pickColor(color.Black+color.Color(v-40), em.defaultStyle.Bg()); ok { em.style = em.style.WithBg(c) } case 48: if c, rest, err := em.parseSgrColor(args, words); err == nil { words = rest em.style = em.style.WithBg(c) } case 49: if c, ok := em.pickColor(color.Reset, em.defaultStyle.Bg()); ok { em.style = em.style.WithBg(c) } case 53: em.style = em.style.WithAttr(em.style.Attr() | Overline) case 55: em.style = em.style.WithAttr(em.style.Attr() &^ Overline) case 58: if c, rest, err := em.parseSgrColor(args, words); err == nil { words = rest em.style = em.style.WithUc(c) } } } } // processCursorUp implements CUU. func (em *emulator) processCursorUp(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false em.moveUpN(Row(max(1, pi[0]))) } } // processCursorDown implements CUD. func (em *emulator) processCursorDown(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false em.moveDownN(Row(max(1, pi[0]))) } } // processCursorForward implements CUF. func (em *emulator) processCursorForward(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false em.moveRightN(Col(max(1, pi[0]))) } } // processCursorBackward implements CUB. func (em *emulator) processCursorBackward(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false em.moveLeftN(Col(max(1, pi[0]))) } } // processCursorNextLine implements CNL. func (em *emulator) processCursorNextLine(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false em.moveDownN(Row(max(1, pi[0]))) pos := em.getPosition() pos.X = 0 em.setPosition(pos) } } // processCursorPreviousLine implements CPL. func (em *emulator) processCursorPreviousLine(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false em.moveUpN(Row(max(1, pi[0]))) pos := em.getPosition() pos.X = 0 em.setPosition(pos) } } // processCursorColumn implements CHA. func (em *emulator) processCursorColumn(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false pos := em.getPosition() // TODO: possibly clip to margins (origin mode) pos.X = min(Col(max(1, pi[0])), em.size.X) - 1 em.setPosition(pos) } } // processCursorPosition implements CUP, and also HVP. func (em *emulator) processCursorPosition(str string) { if pi, err := numericParams(str, 2); err == nil { em.autoWrap = false pos := em.getPosition() wsz := em.size row := Row(max(1, pi[0])) col := Col(max(1, pi[1])) row = max(1, min(row, wsz.Y)) col = max(1, min(col, wsz.X)) pos.X = col - 1 pos.Y = row - 1 em.setPosition(pos) } } // processCursorTab implements CHT. func (em *emulator) processCursorTab(str string) { if pi, err := numericParams(str, 1); err == nil { // Note: tab does not clear this field. for range max(1, pi[0]) { em.nextTab() } } } // processCursorBackTab implements CBT. func (em *emulator) processCursorBackTab(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false for range max(1, pi[0]) { em.prevTab() } } } // processEraseDisplay implements ED. func (em *emulator) processEraseDisplay(str string) { if pi, err := numericParams(str, 1); err == nil { em.bufferingStart() defer em.bufferingEnd() switch pi[0] { case 0: // erase below em.eraseBelow() case 1: // erase above em.eraseAbove() case 2: // erase all em.eraseAll() // others not supported (3 is erase saved lines) } } } // processEraseLine implements EL. func (em *emulator) processEraseLine(str string) { if pi, err := numericParams(str, 1); err == nil { em.bufferingStart() defer em.bufferingEnd() switch pi[0] { case 0: em.eraseToLineEnd() case 1: em.eraseToLineStart() case 2: em.eraseLine() } } } // processEraseCharacter implements ECH. // This ignores the margin. func (em *emulator) processEraseCharacter(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false pos := em.pos // TODO: delete wide character if we are splitting it at the start em.bufferingStart() defer em.bufferingEnd() for range max(1, pi[0]) { em.eraseCell(pos) pos.X++ if pos.X >= em.size.X { break } } } } // processScrollUp implements SU (VT420.) func (em *emulator) processScrollUp(str string) { if pi, err := numericParams(str, 1); err == nil { em.bufferingStart() defer em.bufferingEnd() for range max(pi[0], 1) { // TODO: consider faster jump scroll. // This should be something tunable as well. em.scrollUp() } } } // processScrollDown implements SD (VT420.) func (em *emulator) processScrollDown(str string) { if pi, err := numericParams(str, 1); err == nil { em.bufferingStart() defer em.bufferingEnd() for range max(pi[0], 1) { // TODO: consider faster jump scroll. // This should be something tunable as well. em.scrollDown() } } } // processWindowOps handles CSI ... t window operations. func (em *emulator) processWindowOps(str string) { if pi, err := numericParams(str, 3); err == nil { switch pi[0] { case 8: // Resize window: CSI 8 ; rows ; cols t rows := pi[1] cols := pi[2] if rows < 1 || cols < 1 { return } size := Coord{X: Col(cols), Y: Row(rows)} if ws, ok := em.be.(interface{ SetSize(Coord) }); ok { ws.SetSize(size) em.applyResize(size) } case 18: // Report text area size: CSI 8 ; rows ; cols t em.SendRaw(fmt.Appendf(nil, "\x1b[8;%d;%dt", em.size.Y, em.size.X)) } } } // processVerticalMargins implements DECSTBM (set top and bottom margins, VT220.) func (em *emulator) processVerticalMargins(str string) { if pi, err := numericParams(str, 2); err == nil { pi[0] = max(1, pi[0]) if pi[1] == 0 { pi[1] = int(em.size.Y) } pi[0]-- pi[1]-- top := min(Row(pi[0]), em.size.Y-1) bot := min(Row(pi[1]), em.size.Y-1) // no change if values are out of range if bot > top { em.topMargin = top em.botMargin = bot em.setPosition(Coord{X: 0, Y: 0}) } } } // processHorizontalMargins implements DECSLRM (set left and right margins, VT400.) // It only works if Private Mode 69 (Left and Right margins) func (em *emulator) processHorizontalMargins(str string) { if em.getPrivateMode(PmLeftRightMargin) != ModeOn { // For compat with SCO and ANSI.SYS. if str == "" { em.saveCursor() } return } if pi, err := numericParams(str, 2); err == nil { pi[0] = max(1, pi[0]) if pi[1] == 0 { pi[1] = int(em.size.X) } pi[0]-- pi[1]-- lm := min(Col(pi[0]), em.size.X-1) rm := min(Col(pi[1]), em.size.X-1) // no change if values are out of range if rm > lm { em.rtMargin = rm em.ltMargin = lm em.setPosition(Coord{X: 0, Y: 0}) } } } // processIndex moves down, unless already on the bottom margin, in which case it scrolls Up. func (em *emulator) processIndex() { pos := em.getPosition() em.autoWrap = false if pos.Y == em.botMargin && em.ltMargin <= pos.X && pos.X <= em.rtMargin { em.scrollUp() } else { em.processCursorDown("") } } // processCarriageReturn handle CR. func (em *emulator) processCarriageReturn() { em.lastIndex = 0 em.setPosition(Coord{0, em.getPosition().Y}) } // processLineFeed is like IND, but if ANSI mode 20 is set, then a CR is appended as well. func (em *emulator) processLineFeed() { em.lastIndex = 0 em.processIndex() if em.getAnsiMode(AmNewLineMode) == ModeOn { em.processCarriageReturn() } } // processReverseIndex moves up, unless already on the top margin, in which case it scrolls down. func (em *emulator) processReverseIndex() { pos := em.getPosition() if pos.Y == em.topMargin { em.scrollDown() } else { em.processCursorUp("") } } // processCursorRow implements VPA (set vertical position absolute) func (em *emulator) processCursorRow(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false pos := em.getPosition() pos.Y = min(Row(max(1, pi[0])), em.size.Y) - 1 em.setPosition(pos) } } // processCursorRowAdvance implements VPR (vertical position relative). func (em *emulator) processCursorRowAdvance(str string) { if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false pos := em.getPosition() pos.Y = min(pos.Y+Row(max(1, pi[0])), em.size.Y-1) em.setPosition(pos) } } // processInsertLine implements IL. func (em *emulator) processInsertLine(str string) { // insert line only takes effect within the scrolling region if em.pos.X < em.ltMargin || em.pos.X > em.rtMargin || em.pos.Y < em.topMargin || em.pos.Y > em.botMargin { return } if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false num := Row(max(1, pi[0])) num = min(num, em.botMargin-em.pos.Y+1) // process as a scroll down at our position. top := em.topMargin em.topMargin = em.pos.Y for range num { em.scrollDown() } em.topMargin = top em.pos.X = 0 em.setPosition(em.pos) } } // processDeleteLine implements DL. func (em *emulator) processDeleteLine(str string) { // delete line only takes effect within the scrolling region if em.pos.X < em.ltMargin || em.pos.X > em.rtMargin || em.pos.Y < em.topMargin || em.pos.Y > em.botMargin { return } if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false num := Row(max(1, pi[0])) num = min(num, em.botMargin-em.pos.Y+1) if num < 1 { return } // process as a scroll up at our position. top := em.topMargin em.topMargin = em.pos.Y for range num { em.scrollUp() } em.topMargin = top em.pos.X = 0 em.setPosition(em.pos) } } // processDeleteCharacter implements DCH. func (em *emulator) processDeleteCharacter(str string) { // only takes effect within the scrolling region if em.pos.X < em.ltMargin || em.pos.X > em.rtMargin || em.pos.Y < em.topMargin || em.pos.Y > em.botMargin { return } em.bufferingStart() defer em.bufferingEnd() if pi, err := numericParams(str, 1); err == nil { em.autoWrap = false num := Col(max(1, pi[0])) num = min(num, em.rtMargin-em.pos.X+1) if num < 1 { return } // this is essentially a one line scroll left pos := em.pos // if we are breaking a wide rune, delete it (but preserve style) if em.pos.X > 0 { if ix := em.index(em.pos); em.cells[ix-1].W > 1 { em.cells[ix-1].C = "" em.cells[ix-1].W = 0 em.be.Put(Coord{X: em.pos.X - 1, Y: em.pos.Y}, em.cells[ix-1]) } } for range num { src := Coord{X: em.pos.X + 1, Y: em.pos.Y} dst := Coord{X: em.pos.X, Y: em.pos.Y} dim := Coord{X: em.rtMargin - em.pos.X, Y: 1} em.blit(src, dst, dim) em.eraseCell(Coord{X: em.rtMargin, Y: em.pos.Y}) } em.pos = pos em.setPosition(em.pos) } } // processInsertCharacter implements ICH. func (em *emulator) processInsertCharacter(str string) { em.autoWrap = false // only takes effect within the scrolling region -- HOWEVER, // ICH still resets auto-wrap in this case, unlike DCH. if em.pos.X < em.ltMargin || em.pos.X > em.rtMargin || em.pos.Y < em.topMargin || em.pos.Y > em.botMargin { return } em.bufferingStart() defer em.bufferingEnd() if pi, err := numericParams(str, 1); err == nil { num := Col(max(1, pi[0])) num = min(num, em.rtMargin-em.pos.X+1) if num < 1 { return } // this is essentially a one line scroll right pos := em.pos // if we are breaking a wide rune, delete it if em.pos.X > 0 { if ix := em.index(em.pos); em.cells[ix-1].W > 1 { em.cells[ix-1].C = "" em.cells[ix-1].W = 0 em.be.Put(Coord{X: em.pos.X - 1, Y: em.pos.Y}, em.cells[ix-1]) } } for range num { src := Coord{X: em.pos.X, Y: em.pos.Y} dst := Coord{X: em.pos.X + 1, Y: em.pos.Y} dim := Coord{X: em.rtMargin - em.pos.X, Y: 1} em.blit(src, dst, dim) ix := em.index(Coord{X: em.pos.X, Y: em.pos.Y}) em.cells[ix].C = "" em.cells[ix].W = 0 em.cells[ix].S = em.style // NB: We don't use eraseCell, because we need to preserve attributes. em.be.Put(Coord{X: em.pos.X, Y: em.pos.Y}, em.cells[ix]) } // if we clipped off the end of a wide character, then delete it. if ix := em.index(Coord{X: em.rtMargin, Y: em.pos.Y}); em.cells[ix].W > 1 { em.cells[ix].C = "" em.cells[ix].W = 0 em.be.Put(Coord{X: em.rtMargin, Y: em.pos.Y}, em.cells[ix]) } em.pos = pos em.setPosition(em.pos) } } // processSetMode implements SM (set ANSI mode). func (em *emulator) processSetMode(str string) { if pi, err := numericParams(str, 1); err == nil { for _, pm := range pi { em.setAnsiMode(AnsiMode(pm), ModeOn) } } } // processResetMode implements RM (reset ANSI mode). func (em *emulator) processResetMode(str string) { if pi, err := numericParams(str, 1); err == nil { for _, pm := range pi { em.setAnsiMode(AnsiMode(pm), ModeOff) } } } // processRequestMode implements DECRQM for ANSI modes. // Only a single numeric parameter (mode number) can be supplied (VT300+) func (em *emulator) processRequestMode(str string) { if m, err := strconv.Atoi(str); err == nil { am := AnsiMode(m) status := em.getAnsiMode(am) em.SendRaw([]byte(am.Reply(status))) } } // processSetPrivateMode implements DECSET (set private mode). func (em *emulator) processSetPrivateMode(str string) { if pi, err := numericParams(str, 1); err == nil { for _, pm := range pi { em.setPrivateMode(PrivateMode(pm), ModeOn) } } } // processResetPrivateMode implements DECRST (reset private mode). func (em *emulator) processResetPrivateMode(str string) { if pi, err := numericParams(str, 1); err == nil { for _, pm := range pi { em.setPrivateMode(PrivateMode(pm), ModeOff) } } } // processRequestPrivateMode implements DECRQM for private modes. // Only a single numeric parameter (mode number) can be supplied (VT300+) func (em *emulator) processRequestPrivateMode(str string) { if m, err := strconv.Atoi(str); err == nil { pm := PrivateMode(m) status := em.getPrivateMode(pm) em.SendRaw([]byte(pm.Reply(status))) } } // processTabReset implements DECST8C (set tab stops to every 8 chars) func (em *emulator) processTabReset(str string) { if pi, err := numericParams(str, 1); err == nil && pi[0] == 5 { em.tabStops = nil } } // processTabClear implements TBC (clear horizontal tab). func (em *emulator) processTabClear(str string) { if pi, err := numericParams(str, 1); err == nil { switch pi[0] { case 0: // clear stop at current column em.clrTabStop(em.getPosition().X) case 3: // clear all columns em.tabStops = []Col{} // this is distinct from nil } } } // processPrimaryAttributes implements send DA. func (em *emulator) processPrimaryAttributes(str string) { if pi, err := numericParams(str, 1); err == nil && pi[0] == 0 { em.sendDA() } } // processExtendedAttributes implements XTVERSION (send terminal name and version). func (em *emulator) processExtendedAttributes(str string) { if pi, err := numericParams(str, 1); err == nil && pi[0] == 0 && em.name != "" { em.SendRaw(fmt.Appendf(nil, "\x1bP>|%s %s\x1b\\", em.name, em.vers)) } } // processCursorStyle implements DECSCUSR (set cursor style). func (em *emulator) processCursorStyle(str string) { // get previous visibility state, as we don't change it with this call. visible := em.cursor.IsVisible() if pi, err := numericParams(str, 1); err == nil { switch pi[0] { case 0, 1: em.cursor = BlinkingBlock case 2: em.cursor = SteadyBlock case 3: em.cursor = BlinkingUnderline case 4: em.cursor = SteadyUnderline case 5: em.cursor = BlinkingBar case 6: em.cursor = SteadyBar } if !visible { em.cursor = em.cursor.Hide() } em.be.SetCursor(em.cursor) } } // processCsi processes CSI sequences. func (em *emulator) processCsi(final byte) { // CSI sequences are supported in several different possible ways: // parameters may have a prefix character that is not numeric, typically // indicating a whole different mode of operation than the final byte. // There may also be intermediate bytes, but we only look for one, because // the use cases we have this are that only a single intermediate byte is // sometimes used to affect function. (E.g. $ in some cases.) cmd := "" if em.inBuf.Len() > 0 { if b := em.inBuf.Bytes()[0]; b > '9' && b <= '?' { cmd += string(b) em.inBuf.ReadByte() } } if l := em.inBuf.Len(); l > 0 { if b := em.inBuf.Bytes()[l-1]; b >= 0x20 && b <= 0x2F { cmd += string(b) em.inBuf.Truncate(l - 1) } } cmd += string(final) str := em.inBuf.String() switch cmd { case "@": em.processInsertCharacter(str) case "A": em.processCursorUp(str) case "B": em.processCursorDown(str) case "C": em.processCursorForward(str) case "D": em.processCursorBackward(str) case "E": em.processCursorNextLine(str) case "F": em.processCursorPreviousLine(str) case "G": em.processCursorColumn(str) case "H", "f": em.processCursorPosition(str) case "I": em.processCursorTab(str) case "J": em.processEraseDisplay(str) case "K": em.processEraseLine(str) case "L": em.processInsertLine(str) case "M": em.processDeleteLine(str) case "P": em.processDeleteCharacter(str) case "S": em.processScrollUp(str) case "T": em.processScrollDown(str) case "X": em.processEraseCharacter(str) case "Z": em.processCursorBackTab(str) case "c": em.processPrimaryAttributes(str) case "d": em.processCursorRow(str) case "e": em.processCursorRowAdvance(str) case "g": em.processTabClear(str) case "h": em.processSetMode(str) case "l": em.processResetMode(str) case "m": em.processSgr(str) case "n": em.deviceReport(str) case "r": em.processVerticalMargins(str) case "s": em.processHorizontalMargins(str) case "t": em.processWindowOps(str) case " q": em.processCursorStyle(str) case "?W": em.processTabReset(str) case "?h": em.processSetPrivateMode(str) case "?l": em.processResetPrivateMode(str) case "$p": em.processRequestMode(str) case "?$p": em.processRequestPrivateMode(str) case ">q": em.processExtendedAttributes(str) } } // processClipboard handles OSC 52 commands. func (em *emulator) processClipboard(str string) { clipper, ok := em.be.(Clipboard) if !ok { return } // first parameter is the target. We only have a single // target, and alias all possibilities to the same. parts := strings.SplitN(str, ";", 2) if len(parts) != 2 { return } if parts[1] == "?" { // request for clipboard content data := clipper.GetClipboard() if data != nil { em.SendRaw(fmt.Appendf(nil, "\x1b]52;c;%s\x1b\\", base64.StdEncoding.EncodeToString(data))) } return } buf := make([]byte, base64.StdEncoding.DecodedLen(len(parts[1]))) if n, err := base64.StdEncoding.Decode(buf, []byte(parts[1])); err == nil { clipper.SetClipboard(buf[:n]) return } clipper.SetClipboard([]byte{}) } // processHyperLink handles OSC 8 commands. func (em *emulator) processHyperLink(str string) { // format is params;URI params are colon separated key value pairs. // if the URI is absent, then the link is terminated. parts := strings.SplitN(str, ";", 2) if len(parts) == 2 { if parts[1] == "" { // No URI em.style = em.style.WithUrl("", "") return } url := parts[1] id := "" for pair := range strings.SplitSeq(parts[0], ":") { if val, ok := strings.CutPrefix(pair, "id="); ok { id = val } } em.style = em.style.WithUrl(url, id) } } // processOSC processes an operating system command. func (em *emulator) processOSC() { // Every OSC we support has a number, semicolon, then string. ns, str, ok := strings.Cut(em.inBuf.String(), ";") if !ok { return } if num, err := strconv.Atoi(ns); err != nil { return } else { switch num { case 2: // Set window title if t, ok := em.be.(Titler); ok { // TODO: possibly validate the UTF-8 content? em.bufferingStart() defer em.bufferingEnd() t.SetWindowTitle(str) } case 8: em.processHyperLink(str) case 52: em.processClipboard(str) } } } func (em *emulator) getPosition() Coord { pos := em.be.GetPosition() em.pos = pos return em.pos } func (em *emulator) setPosition(pos Coord) { em.pos = pos em.be.SetPosition(pos) } func (em *emulator) deviceReport(s string) { switch s { case "5": em.SendRaw([]byte("\x1b[0n")) case "6": pos := em.getPosition() em.SendRaw(fmt.Appendf(nil, "\x1b[%d;%dR", pos.Y+1, pos.X+1)) default: // ignore } } func (em *emulator) moveUpN(count Row) { for range count { em.moveUp() } } func (em *emulator) moveDownN(count Row) { for range count { em.moveDown() } } func (em *emulator) moveLeftN(count Col) { for range count { em.moveLeft() } } func (em *emulator) moveRightN(count Col) { for range count { em.moveRight() } } // moveDown moves down, to the limit of either bottom margin, or the bottom of the screen if outside the margin. func (em *emulator) moveDown() { pos := em.getPosition() win := em.size if pos.Y == em.botMargin || pos.Y == win.Y-1 { return } pos.Y++ em.setPosition(pos) } // moveUp moves up, to the limit of either top margin, or zero if outside the margin. func (em *emulator) moveUp() { pos := em.getPosition() if pos.Y == 0 || pos.Y == em.topMargin { return } pos.Y-- em.setPosition(pos) } func (em *emulator) moveLeft() { em.autoWrap = false em.lastIndex = 0 pos := em.getPosition() if pos.X > 0 { pos.X-- em.setPosition(pos) } } func (em *emulator) moveRight() { pos := em.getPosition() win := em.size if pos.X < win.X-1 { pos.X++ em.setPosition(pos) } } // nextLine is like CNL with 1, but it optionally also scrolls. func (em *emulator) nextLine() { em.autoWrap = false if em.pos.Y == em.botMargin { em.scrollUp() } em.moveDown() em.pos.X = 0 em.setPosition(em.pos) } // blit performs a data move operation. It does ignores margins. func (em *emulator) blit(src, dst, dim Coord) { em.bufferingStart() defer em.bufferingEnd() // save the source and destination for the backend blit bsrc := src bdst := dst lim := em.size // clip to visible source if dim.X+src.X > lim.X { dim.X = lim.X - src.X } if dim.Y+src.Y > lim.Y { dim.Y = lim.Y - src.Y } // and clip to final destination if dim.X+dst.X > lim.X { dim.X = lim.X - dst.X } if dim.Y+dst.Y > lim.Y { dim.Y = lim.Y - dst.Y } // gap represents decrement when shifting to the next row -- // skipping over the irrelevant cells. (The increment in the // index when going from last cell of row to first cell of next row, // or vice versa.) gap := int(lim.X - dim.X) // the following logic is carefully constructed to avoid expensive // operations in the loops (only addition or subtraction) if em.index(src) > em.index(dst) { // source appears later, so we can forward copy si := em.index(src) di := em.index(dst) for range dim.Y { for range dim.X { em.cells[di] = em.cells[si] di++ si++ } // advance to next row si += gap di += gap } } else { // source appears earlier, so we have to reverse copy src.Y += dim.Y - 1 dst.Y += dim.Y - 1 src.X += dim.X - 1 dst.X += dim.X - 1 si := em.index(src) di := em.index(dst) for range dim.Y { for range dim.X { em.cells[di] = em.cells[si] si-- di-- } si -= gap di -= gap } } // Now we possibly blit underneath. We'll use the underlying // implementation's blit operation if it has one, else we'll // just rewrite the cells in linear order. if b, ok := em.be.(Blitter); ok { // The backend implements what should be a fast blit. b.Blit(bsrc, bdst, dim) } else { // This does math for each cell, so you're looking at a lot of multiplications // for a bit display -- 100x100 means 10,000x2 multiplications. This could be // optimized, but this is really a fallback as every backend really *should* have // an efficient blit operation. (The ones that don't probably don't keep their own // state, such as a wrappers on top of TTYs.) In these cases the cost of writing // the content is probably substantially dominant anyway. for row := range dim.Y { for col := range dim.X { pos := bdst pos.X += col pos.Y += row cell := em.cells[em.index(pos)] em.be.Put(pos, cell) } } } } func (em *emulator) scrollUp() { dim := Coord{X: em.rtMargin - em.ltMargin + 1, Y: em.botMargin - em.topMargin} src := Coord{X: em.ltMargin, Y: em.topMargin + 1} dst := Coord{X: em.ltMargin, Y: em.topMargin} // TODO: deal with wide characters broken across the margin pos := em.pos em.blit(src, dst, dim) bot := Coord{X: em.ltMargin, Y: em.botMargin} for bot.X <= em.rtMargin { em.eraseCell(bot) bot.X++ } em.setPosition(pos) } func (em *emulator) scrollDown() { dim := Coord{X: em.rtMargin - em.ltMargin + 1, Y: em.botMargin - em.topMargin} src := Coord{X: em.ltMargin, Y: em.topMargin} dst := Coord{X: em.ltMargin, Y: em.topMargin + 1} pos := em.pos em.blit(src, dst, dim) em.setPosition(Coord{X: em.ltMargin, Y: em.topMargin}) top := Coord{X: em.ltMargin, Y: em.topMargin} for top.X <= em.rtMargin { em.eraseCell(top) top.X++ } em.setPosition(pos) } // nextTab advances to the next tab stop, or the end of // the line if there is no further tab. func (em *emulator) nextTab() { em.lastIndex = 0 maxX := em.size.X - 1 curX := em.getPosition().X if curX == maxX { // already at end return } nextX := maxX if em.tabStops == nil { // just advance to the next one nextX = min((curX+8)&^7, maxX) } else { for _, p := range em.tabStops { if p > curX { nextX = p break } } } em.setPosition(Coord{X: nextX, Y: em.pos.Y}) } func (em *emulator) prevTab() { curX := em.getPosition().X if curX == 0 { return } nextX := curX - 1 if em.tabStops == nil { nextX &^= 7 } else if i, exist := slices.BinarySearch(em.tabStops, nextX); exist { nextX = em.tabStops[i] } else if i > 0 { nextX = em.tabStops[i-1] } else { nextX = 0 } em.setPosition(Coord{X: nextX, Y: em.pos.Y}) } // initTabStops initializes the tab stops assuming every 8th column // is a tab stop. This should only be called if the user is intentionally // changing the tab stops, because it will no longer support expanding // tab stops on resizing. func (em *emulator) initTabStops() { if em.tabStops == nil { // no tab stop at offset 0 since that would be pointless em.tabStops = make([]Col, 0, int(em.size.X/8)+1) for col := Col(8); col < em.size.X; col += 8 { em.tabStops = append(em.tabStops, col) } } } // setTabStop sets a tab stop at the given location. // This calls initTabStops - please see the description of that function for ramifications. func (em *emulator) setTabStop(ts Col) { em.initTabStops() if index, exist := slices.BinarySearch(em.tabStops, ts); !exist { em.tabStops = slices.Insert(em.tabStops, index, ts) } } // clrTabStop clears the tab stop at the given column. This calls // initTabStops - please see the description of that function for ramifications. func (em *emulator) clrTabStop(ts Col) { em.initTabStops() em.tabStops = slices.DeleteFunc(em.tabStops, func(x Col) bool { return x == ts }) } // index obtains the index in the cells slice for the given coordinates, // which must be within the bounds of the display size. func (em *emulator) index(c Coord) int { return int(c.Y)*int(em.size.X) + int(c.X) } // putRune puts out a single rune. This might be a subsequent part of a grapheme cluster, in // which case it will be emitted together with the preceding base character. func (em *emulator) putRune(r rune) { dim := em.size if lastIdx := em.lastIndex; lastIdx != 0 { lastIdx-- if pm := em.getPrivateMode(PmGraphemeClusters); pm == ModeOn || pm == ModeOnLocked { // ASCII-to-ASCII pairs cannot extend a grapheme cluster, except CRLF. prev := em.cells[lastIdx].C if len(prev) == 1 && prev[0] < utf8.RuneSelf && !shouldCheckGrapheme(prev[0], r) { // fall through to the normal single-rune path } else { // maybe we need to update the last index buf := em.graphemeBuf[:0] need := len(prev) + utf8.UTFMax if cap(buf) < need { buf = make([]byte, 0, need) } buf = append(buf, prev...) buf = utf8.AppendRune(buf, r) em.graphemeIter.SetText(buf) if em.graphemeIter.Next() && len(em.graphemeIter.Value()) == len(buf) { // we are adding to a cluster cluster := em.graphemeIter.Value() width := em.cells[lastIdx].W if w := textWidthOptions.Rune(r); w > width { width = w } if isRegionalIndicator(r) && width < 2 { width = 2 } if r == '\uFE0F' && width < 2 { width = 2 } em.cells[lastIdx].C = em.clusterString(cluster) em.cells[lastIdx].W = width col := Col(lastIdx) % dim.X row := Row(lastIdx / int(dim.X)) // we may have to move position if this switches to wide, so recalculate expected end next := col + Col(width) if em.getPrivateMode(PmAutoMargin) == ModeOn && next >= dim.X { em.autoWrap = true } end := next if end >= dim.X { end = dim.X - 1 } if width == 2 && col < dim.X-1 && em.cells[lastIdx+1].W != 0 { // erase the next cell before putting down a character em.cells[lastIdx+1].C = "" em.cells[lastIdx+1].S = em.cells[lastIdx].S em.cells[lastIdx+1].W = 0 em.be.Put(Coord{X: col + 1, Y: row}, em.cells[lastIdx+1]) } // we leave the em.lastIndex for now, we might keep extending this cluster em.be.Put(Coord{X: col, Y: row}, em.cells[lastIdx]) em.setPosition(Coord{X: end, Y: row}) em.graphemeBuf = buf[:0] return } em.graphemeBuf = buf[:0] } } } if em.autoWrap { em.nextLine() } autoMargin := em.getPrivateMode(PmAutoMargin) == ModeOn pos := em.getPosition() w := textWidthOptions.Rune(r) if autoMargin && pos.X+Col(w) >= dim.X { em.autoWrap = true } index := em.index(pos) em.cells[index].C = em.runeString(r) em.cells[index].S = em.style em.cells[index].W = w em.be.Put(em.pos, em.cells[index]) em.lastIndex = index + 1 if w == 2 && pos.X < dim.X-1 { index++ em.cells[index].C = "" em.cells[index].S = em.style em.cells[index].W = 0 } // Advance the cursor. This will stop at the margin. // Note that if auto margin is enabled, we will have set // autoWrap above if we were at the margin already. em.moveRightN(Col(w)) } func (em *emulator) runeString(r rune) string { if r < utf8.RuneSelf { return asciiRuneStrings[r] } return em.runeStrings.stringFor(r) } func (em *emulator) clusterString(cluster []byte) string { return em.clusterStrings.stringFor(cluster) } func shouldCheckGrapheme(prev byte, r rune) bool { if r < utf8.RuneSelf { return prev == '\r' && r == '\n' } if unicode.Is(unicode.M, r) { return true } if r == '\u200d' { return true } if r >= 0xFE00 && r <= 0xFE0F { return true } if r >= 0xE0100 && r <= 0xE01EF { return true } return false } func isRegionalIndicator(r rune) bool { return r >= 0x1F1E6 && r <= 0x1F1FF } // eraseCell erases a single cell at the given offset. // It clears attributes, but leaves the colors intact. func (em *emulator) eraseCell(c Coord) { s := em.style.WithAttr(Plain) index := em.index(c) em.cells[index].C = "" em.cells[index].S = s em.cells[index].W = 0 em.be.Put(c, em.cells[index]) } // eraseBelow erases from (and including) the current cursor position to the end of the window. func (em *emulator) eraseBelow() { size := em.size pos := em.getPosition() for x := pos.X; x < size.X; x++ { em.eraseCell(Coord{X: x, Y: pos.Y}) } for y := pos.Y + 1; y < size.Y; y++ { for x := Col(0); x < size.X; x++ { em.eraseCell(Coord{X: x, Y: y}) } } em.setPosition(pos) } // eraseAbove erases from the origin to (and including) the current cursor position. func (em *emulator) eraseAbove() { size := em.size pos := em.getPosition() for y := Row(0); y < pos.Y; y++ { for x := Col(0); x < size.X; x++ { em.eraseCell(Coord{X: x, Y: y}) } } for x := Col(0); x <= pos.X; x++ { em.eraseCell(Coord{X: x, Y: pos.Y}) } em.setPosition(pos) } // eraseAll erases the entire screen. It uses the color, but resets all other attributes. func (em *emulator) eraseAll() { size := em.size pos := em.getPosition() for y := Row(0); y < size.Y; y++ { for x := Col(0); x < size.X; x++ { em.eraseCell(Coord{X: x, Y: y}) } } em.setPosition(pos) } // eraseToLineEnd erases to the end of the line, including the cursor position. func (em *emulator) eraseToLineEnd() { size := em.size pos := em.getPosition() for x := pos.X; x < size.X; x++ { em.eraseCell(Coord{x, pos.Y}) } em.setPosition(pos) } // eraseToLineStart erases to the start of the line, including the cursor position. func (em *emulator) eraseToLineStart() { pos := em.getPosition() for x := Col(0); x <= pos.X; x++ { em.eraseCell(Coord{x, pos.Y}) } em.setPosition(pos) } // eraseLine erases the entire line. func (em *emulator) eraseLine() { size := em.size pos := em.getPosition() for x := range size.X { em.eraseCell(Coord{x, pos.Y}) } em.setPosition(pos) } // softReset performs a soft reset. func (em *emulator) softReset() { // TODO: // Select default character sets em.tabStops = nil em.autoWrap = false em.style = em.defaultStyle em.saved = savedCursor{style: em.defaultStyle} em.topMargin = 0 em.botMargin = em.size.Y - 1 em.ltMargin = 0 em.rtMargin = em.size.X - 1 em.appKeyPad = false em.be.Reset() // start by resetting all modes for _, am := range em.ansiModeKeys() { em.setAnsiMode(am, ModeOff) // NB: No effect for non-changeable modes } for _, pm := range em.privateModeKeys() { em.setPrivateMode(pm, ModeOff) // NB: No effect for non-changeable modes } // and set any that should reset on (auto-margin) em.setPrivateMode(PmAutoMargin, ModeOn) em.setPrivateMode(PmAutoRepeat, ModeOn) em.setPrivateMode(PmShowCursor, ModeOn) em.setPrivateMode(PmBlinkCursor, ModeOn) // set default cursor - matches VT defaults em.cursor = BlinkingBlock em.be.SetCursor(em.cursor) em.setPosition(Coord{0, 0}) em.eraseAll() } func (em *emulator) ansiModeKeys() []AnsiMode { em.modeLock.RLock() defer em.modeLock.RUnlock() keys := make([]AnsiMode, 0, len(em.ansiModes)) for am := range em.ansiModes { keys = append(keys, am) } return keys } func (em *emulator) privateModeKeys() []PrivateMode { em.modeLock.RLock() defer em.modeLock.RUnlock() keys := make([]PrivateMode, 0, len(em.localModes)) for pm := range em.localModes { keys = append(keys, pm) } return keys } // sendDA ends the primary device attributes. func (em *emulator) sendDA() { buf := &bytes.Buffer{} _, _ = fmt.Fprintf(buf, "\x1b[?63") if em.be.Colors() > 0 { _, _ = fmt.Fprintf(buf, ";22") } if _, ok := em.be.(Clipboard); ok { _, _ = fmt.Fprintf(buf, ";52") } // 9 for NRC? // 15 for graphics? buf.WriteRune('c') em.SendRaw(buf.Bytes()) } // setAnsiMode sets the ANSI mode. func (em *emulator) setAnsiMode(mode AnsiMode, ms ModeStatus) { if !ms.Changeable() { return } em.modeLock.Lock() defer em.modeLock.Unlock() if old, ok := em.ansiModes[mode]; ok && old.Changeable() { em.ansiModes[mode] = ms } } func (em *emulator) getAnsiMode(mode AnsiMode) ModeStatus { em.modeLock.RLock() defer em.modeLock.RUnlock() return em.ansiModes[mode] } // getPrivateMode returns the value of a DEC private mode. func (em *emulator) getPrivateMode(pm PrivateMode) ModeStatus { em.modeLock.RLock() if ms, ok := em.localModes[pm]; ok { em.modeLock.RUnlock() return ms } em.modeLock.RUnlock() return em.be.GetPrivateMode(pm) } func (em *emulator) updateMouseReporting() { mi, ok := em.be.(Mouser) if !ok { return } em.modeLock.RLock() report := em.mouseReportingLocked() em.modeLock.RUnlock() mi.SetMouse(report) } // setPrivateMode sets the DEC private mode. func (em *emulator) setPrivateMode(pm PrivateMode, ms ModeStatus) { if !ms.Changeable() { return } em.modeLock.Lock() old, ok := em.localModes[pm] if ok && old.Changeable() { em.localModes[pm] = ms var ( setMouse bool report MouseReporting setCursor bool cursor CursorStyle ) switch pm { case PmMouseButton, PmMouseDrag, PmMouseMotion, PmMouseSgr, PmMouseSgrPixel, PmMouseX10: report = em.mouseReportingLocked() setMouse = true case PmShowCursor: if ms == ModeOn { em.cursor = em.cursor.Show() } else { em.cursor = em.cursor.Hide() } cursor = em.cursor setCursor = true case PmBlinkCursor: if ms == ModeOn { em.cursor = em.cursor.Blink() } else { em.cursor = em.cursor.Steady() } cursor = em.cursor setCursor = true } em.modeLock.Unlock() if setMouse { if mi, ok := em.be.(Mouser); ok { mi.SetMouse(report) } } if setCursor { em.be.SetCursor(cursor) } return } em.modeLock.Unlock() if em.be.GetPrivateMode(pm).Changeable() { _ = em.be.SetPrivateMode(pm, ms) } } func (em *emulator) mouseReportingLocked() MouseReporting { switch { case em.localModes[PmMouseButton] == ModeOn: em.mouseReports = MouseButtons if em.localModes[PmMouseMotion] == ModeOn { em.mouseReports = MouseMotion } else if em.localModes[PmMouseDrag] == ModeOn { em.mouseReports = MouseDrag } case em.localModes[PmMouseX10] == ModeOn: em.mouseReports = MouseButtons default: em.mouseReports = MouseDisabled } return em.mouseReports } // SendRaw allows raw data to be sent to the application. // This is done in a thread-safe way, so that content is not intermingled. func (em *emulator) SendRaw(b []byte) { em.sendLock.Lock() defer em.sendLock.Unlock() // Do not attempt to send *anything* if we are stopped. select { case <-em.stopQ: return default: } // Try to write to the readQ, but if we cannot, then wait until // either we can, or the stopQ is fired. This ensures that we avoid // breaking up content if at all possible. for _, ch := range b { select { case em.readQ <- ch: default: select { case em.readQ <- ch: case <-em.stopQ: return } } } } // KeyEvent injects a keyboard event into the emulator func (em *emulator) KeyEvent(ev KeyEvent) { if em.getPrivateMode(PmWin32Input) == ModeOn { em.keyWin32IM(ev) } else { // eliminate "control" keys (which keyboard maps provide) from consideration. // (We handle control keys explicitly.) if ev.Utf != "" && ev.Utf[0] < ' ' { ev.Utf = "" } // TODO: more add support for kitty, and maybe modify other keys em.keyLegacy(ev) } } // ResizeEvent is called by the backend when a resize occurs. A real backend with a child // process (essentially a "real emulator") should probably also fire SIGWINCH if appropriate. // That would be the job of something other than this code. func (em *emulator) ResizeEvent(size Coord) { select { case em.writeQ <- size: case <-em.stopQ: } } func (em *emulator) applyResize(size Coord) { // resize clobbers our content, until it is redrawn em.size = size em.tabStops = slices.DeleteFunc(em.tabStops, func(x Col) bool { return x >= em.size.X }) // resizing resets the margins em.topMargin = 0 em.botMargin = em.size.Y - 1 em.ltMargin = 0 em.rtMargin = em.size.X - 1 em.cells = make([]Cell, int(em.size.X)*int(em.size.Y)) for i := range em.cells { em.cells[i].S = em.defaultStyle } em.pos = em.getPosition() if em.getPrivateMode(PmResizeReports) == ModeOn { // NB: we never support "ModeOnLocked" // NB: for now we do not support pixel sizes em.SendRaw(fmt.Appendf(nil, "\x1b[48;%d;%d;0;0t", em.size.Y, em.size.X)) } // Send a SIGWINCH or similar. em.be.RaiseResize() } var legacyKeys = map[Key]struct { K string // unmodified key A string // unmodified in application cursor mode (smkx) S string // with shift (if empty use regular modifier) C string // with control (if empty use regular modifier) CS string // with ctrl-shift }{ KeyF1: {K: "\x1bOP"}, // SS3 P KeyF2: {K: "\x1bOQ"}, // SS3 Q KeyF3: {K: "\x1bOR"}, // SS3 R KeyF4: {K: "\x1bOS"}, // SS3 S KeyF5: {K: "\x1b[15~"}, KeyF6: {K: "\x1b[17~"}, KeyF7: {K: "\x1b[18~"}, KeyF8: {K: "\x1b[19~"}, KeyF9: {K: "\x1b[20~"}, KeyF10: {K: "\x1b[21~"}, KeyF11: {K: "\x1b[23~"}, KeyF12: {K: "\x1b[24~"}, KeyF13: {K: "\x1b[25~"}, KeyF14: {K: "\x1b[26~"}, KeyF15: {K: "\x1b[28~"}, KeyF16: {K: "\x1b[29~"}, KeyF17: {K: "\x1b[31~"}, KeyF18: {K: "\x1b[32~"}, KeyF19: {K: "\x1b[33~"}, KeyF20: {K: "\x1b[34~"}, KeyUp: {K: "\x1b[A", A: "\x1bOA"}, KeyDown: {K: "\x1b[B", A: "\x1bOB"}, KeyRight: {K: "\x1b[C", A: "\x1bOC"}, KeyLeft: {K: "\x1b[D", A: "\x1bOD"}, KeyHome: {K: "\x1b[H", A: "\x1bOH"}, KeyEnd: {K: "\x1b[F", A: "\x1bOF"}, KeyPgUp: {K: "\x1b[5~"}, KeyPgDn: {K: "\x1b[6~"}, KeyDelete: {K: "\x1b[3~"}, KeyInsert: {K: "\x1b[2~"}, KeyMenu: {K: "\x1b[29~"}, // also F16 KeyTab: {K: "\t", S: "\x1b[Z", CS: "\x1b[Z"}, KeyBackspace: {K: "\x7f", S: "\x7f", C: "\x08", CS: "\x08"}, KeySpace: {K: " ", S: " ", C: "\x00", CS: "\x00"}, KeyEnter: {K: "\r", S: "\r", CS: "\r"}, // NB: consider using kitty encoding here KeyPadEnter: {K: "\r", S: "\r", CS: "\r"}, // NB: consider using kitty encoding here KeyEsc: {K: "\x1b", S: "\x1b", C: "\x1b"}, } var legacyControls = map[Key]string{ // These ones are weird legacy control sequences that we mostly // do not care about. We don't include shifted variants. Key2: "\x00", Key3: "\x1b", Key4: "\x1c", Key5: "\x1d", Key6: "\x1e", Key7: "\x1f", Key8: "\x7f", KeyLBrace: "\x1b", KeySlash: "\x1c", KeyRBrace: "\x1d", } // legacyPadKeys are keys that are on the keypad, when not in numeric keypad mode. // Note that num lock overrides this. var legacyPadKeys = map[Key]struct { app string num string }{ KeyPadEnter: {"\x1bOM", "\r"}, KeyPadMul: {"\x1bOj", "*"}, KeyPadAdd: {"\x1bOk", "+"}, KeyPadSub: {"\x1bOm", "-"}, KeyPadDiv: {"\x1bOo", "/"}, KeyPadDec: {"\x1b[3~", "."}, // Del KeyPad0: {"\x1b[2~", "0"}, // Ins KeyPad1: {"\x1bOF", "1"}, // End KeyPad2: {"\x1b[B", "2"}, // Down KeyPad3: {"\x1b[6~", "3"}, // PgDn KeyPad4: {"\x1b[D", "4"}, // Left KeyPad5: {"\x1b[E", "5"}, // Clear/Begin KeyPad6: {"\x1b[C", "6"}, // Right KeyPad7: {"\x1bOH", "7"}, // Home KeyPad8: {"\x1b[A", "8"}, // Up KeyPad9: {"\x1b[5~", "9"}, // PgUp KeyPadEqual: {"\x1bOX", "="}, } // repeatRaw is called to provide key repeat. We limit key repeating to just 40, // and we ensure that at least one is included. We only repeat if key repeat is enabled. func (em *emulator) repeatRaw(ev KeyEvent, data []byte) { if pm := em.getPrivateMode(PmAutoRepeat); pm == ModeOn || pm == ModeOnLocked { for range min(max(1, ev.Repeat), 40) { em.SendRaw(data) } } else { if ev.Repeat == 0 { em.SendRaw(data) } } } // noRepeatRaw is used to send a key that should never repeat. // It will only send if the repeat count is zero. func (em *emulator) noRepeatRaw(ev KeyEvent, data []byte) { if ev.Repeat == 0 { em.SendRaw(data) } } // keyLegacy handles a keyboard event when in legacy vt220 style mode. func (em *emulator) keyLegacy(ev KeyEvent) { if !ev.Down { // legacy protocol does not support key release return } if ev.Mod.IsMeta() || ev.Mod.IsHyper() { // legacy protocol does not support these return } // Shift-Ctrl keys are never sent in the legacy protocol. We do have to ensure // that if we are sending other Utf (for example with AltGr), then we still might // send it, but this is only an issue for non-ASCII runes. Also, this filter only // applies for "regular" keys (i.e. not function keys, cursor keys, etc.) if ev.Mod.IsShift() && ev.Mod.IsCtrl() && (ev.Utf == "" || ev.Utf[0] < 0x80) { if base := ev.Key.KittyBase(); base >= ' ' && base < 0x80 { return } } // keypad sequences if v, ok := legacyPadKeys[ev.Key]; ok { if ev.Mod&ModNumLock == 0 { if em.appKeyPad { em.repeatRaw(ev, []byte(v.app)) } else { em.repeatRaw(ev, []byte(v.num)) } return } else { ev.Utf = v.num } } // For control keys (e.g. control-J) we never emit a rune directly -- but we might later // add after decoding the key accordingly. if ev.Utf != "" && (ev.Mod == ModLCtrl || ev.Mod == ModRCtrl || ev.Utf[0] < ' ') { ev.Utf = "" } if ev.Utf != "" { if ev.Utf[0] < 0x80 && ev.Mod.IsAlt() { // ASCII might get alt em.noRepeatRaw(ev, fmt.Appendf(nil, "\x1b%s", ev.Utf)) } else { // otherwise send the UTF as-is em.repeatRaw(ev, []byte(ev.Utf)) } return } // some weird number control sequences - legacy compatibility // We do not repeat these. if v, ok := legacyControls[ev.Key]; ok && (ev.Mod == ModLCtrl || ev.Mod == ModRCtrl) { em.noRepeatRaw(ev, []byte(v)) return } if v, ok := legacyKeys[ev.Key]; ok { str := "" match := false if !ev.Mod.IsShift() && !ev.Mod.IsCtrl() { if em.getPrivateMode(PmAppCursor) == ModeOn && v.A != "" { str = v.A } else { str = v.K } // AnsiMode 20 sends newline, but only in legacy mode. if str == "\r" && em.getAnsiMode(AmNewLineMode) == ModeOn { str = "\r\n" } match = true } else if ev.Mod.IsShift() && !ev.Mod.IsCtrl() { if str = v.S; str != "" { match = true } } else if ev.Mod.IsCtrl() && !ev.Mod.IsShift() { if str = v.C; str != "" { match = true } } else { // IsCtrl & IsShift if str = v.CS; str != "" { match = true } } if !match { // No specific modifiers present, lets add them. There are two cases, // one for SS3 based keys and another for CSI based keys. SS3 based // keys are converted to CSI - 1 ; mod ; final // Note: legacy encoding does not use modifiers for alt or super - alt will be // determined by sending an escape prefix. mod := 0 if ev.Mod.IsShift() { mod |= 1 } if ev.Mod.IsCtrl() { mod |= 4 } if strings.HasPrefix(v.K, "\x1bO") { str = fmt.Sprintf("\x1b[1;%d%c", mod+1, v.K[len(v.K)-1]) } else { str = fmt.Sprintf("%s;%d%c", v.K[:len(v.K)-1], mod+1, v.K[len(v.K)-1]) } } if ev.Mod.IsAlt() { // no repeating ALT sequences em.noRepeatRaw(ev, append([]byte{'\x1b'}, []byte(str)...)) // alt sends leading escape } else if ev.Mod.IsCtrl() { // no repeating CTRL sequences em.noRepeatRaw(ev, []byte(str)) } else { // but other sequences (should just be shifted or unmodified) // are fine. (E.g. we want to allow repeats of cursor keys) em.repeatRaw(ev, []byte(str)) } return } // fallback control key handling if ev.Key >= KeyA && ev.Key <= KeyZ && ev.Mod.IsCtrl() { b := byte(ev.Key-KeyA) + 1 /* ctrl-A */ if ev.Mod.IsAlt() { em.noRepeatRaw(ev, []byte{'\x1b', b}) } else { em.noRepeatRaw(ev, []byte{b}) } return } } var win32NoRepeat = map[Key]bool{ KeyLShift: true, KeyRShift: true, KeyLCtrl: true, KeyRCtrl: true, KeyLAlt: true, KeyRAlt: true, KeyLMeta: true, KeyRMeta: true, KeyCapsLock: true, KeyNumLock: true, KeyEnter: true, KeyScrLock: true, KeyPause: true, KeyPrtScr: true, } // keyWin32IM generates the sequence for a key event when in Win32 input mode. // Win32 input mode is ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ // Note that we specifically do NOT doubly encode non-keyboard events -- those // are already unambiguously handled within the protocol. (Windows Terminal behaves // the same way, but most 3rd party terminals do doubly encode.) func (em *emulator) keyWin32IM(ev KeyEvent) { // Some keys that never repeat if pm := em.getPrivateMode(PmAutoRepeat); pm == ModeOff || pm == ModeOffLocked { if ev.Repeat != 0 { return } } r := rune(0) if ev.Utf != "" { runes := []rune(ev.Utf) if len(runes) == 1 { r = runes[0] } } kd := 0 if ev.Down { kd = 1 } cs := 0 // Modifiers if ev.Mod&ModRAlt != 0 { cs |= 0x01 } if ev.Mod&ModLAlt != 0 { cs |= 0x02 } if ev.Mod&ModRCtrl != 0 { cs |= 0x04 } if ev.Mod&ModLCtrl != 0 { cs |= 0x08 } if ev.Mod.IsShift() { cs |= 0x10 } if ev.Mod.IsNumLock() { cs |= 0x20 } // NB: 0x40 is for scroll lock, we don't support it for now if ev.Mod.IsCapsLock() { cs |= 0x80 } switch ev.Key { case KeyPadEnter: case KeyPadDiv: case KeyInsert: case KeyDelete: case KeyHome: case KeyEnd: case KeyPgUp: case KeyPgDn: cs |= 0x100 // enhanced } if win32NoRepeat[ev.Key] { if ev.Repeat > 0 { return } ev.Repeat = 1 } em.SendRaw(fmt.Appendf(nil, "\x1b[%d;%d;%d;%d;%d;%d_", ev.VK, ev.SC, r, kd, cs, max(1, ev.Repeat))) } func (em *emulator) MouseEvent(ev MouseEvent) { if pm := em.getPrivateMode(PmMouseButton); pm == ModeOn { if em.getPrivateMode(PmMouseDrag) != ModeOn && em.getPrivateMode(PmMouseMotion) != ModeOn { // suppress motion events if the user didn't request if ev.Button == NoButton { return // if entire event was just motion, bail } ev.Motion = false } if pm = em.getPrivateMode(PmMouseSgr); pm == ModeOn { btn := ev.encodeButton() if ev.Down { em.SendRaw(fmt.Appendf(nil, "\x1b[<%d;%d;%dM", btn, ev.Position.X+1, ev.Position.Y+1)) } else { em.SendRaw(fmt.Appendf(nil, "\x1b[<%d;%d;%dm", btn, ev.Position.X+1, ev.Position.Y+1)) } } else { // Old style reporting (via 1000h). // Limitations of legacy VT200 reporting are that the coordinates must be between // 1 and 223 inclusive, and that once any release occurs all buttons are assumed // to be released. (Please use SGR mode if at all possible.) // Further, this mode is not CSI compliant as the encoded values that arrive ahead of // the final character may be within the range of technically legal CSI final bytes. if !ev.Down { ev.Button = NoButton } btn := ev.encodeButton() data := append([]byte{'\x1b', '[', 'M', btn + 32}, byte(min(ev.Position.X+1, 223)+32), byte(min(ev.Position.Y+1, 223)+32)) em.SendRaw(data) } } else if pm := em.getPrivateMode(PmMouseX10); pm == ModeOn && ev.Down { // legacy X10 reporting only x := byte(min(ev.Position.X+1, 223)) + 32 y := byte(min(ev.Position.Y+1, 223)) + 32 // NB: we intentionally reverse buttons 2 & 3 (for xterm compatibility) switch ev.Button { case Button1: em.SendRaw([]byte{'\x1b', '[', 'M', ' ', x, y}) case Button2: em.SendRaw([]byte{'\x1b', '[', 'M', '"', x, y}) case Button3: em.SendRaw([]byte{'\x1b', '[', 'M', '!', x, y}) } } } func (em *emulator) FocusEvent(focused bool) { if pm := em.getPrivateMode(PmFocusReports); pm == ModeOn { if focused { em.SendRaw([]byte{'\x1b', '[', 'I'}) } else { em.SendRaw([]byte{'\x1b', '[', 'O'}) } } } // SetId sets the terminal name and version. func (em *emulator) SetId(name string, version string) { em.name = name em.vers = version } // Start the terminal emulator. func (em *emulator) Start() error { select { case <-em.stopQ: default: // already running return errors.New("terminal already started") } stopQ := make(chan bool) em.stopQ = stopQ go em.run(stopQ) return nil } // Stop the terminal emulator. This also wakes any blocked // Read or Write calls, which will return an error. func (em *emulator) Stop() error { select { case <-em.stopQ: default: close(em.stopQ) } return nil } // Drain pending output to the terminal emulator. func (em *emulator) Drain() error { q := make(chan bool) select { case em.writeQ <- q: case <-em.stopQ: } select { case <-q: case <-em.stopQ: } // make sure to wake the reader select { case em.readQ <- true: default: } return nil } // Write data to the emulator (commands). func (em *emulator) Write(data []byte) (n int, err error) { stopQ := em.stopQ writeQ := em.writeQ drainQ := make(chan bool) select { case writeQ <- data: // we add the drainQ for synchronization, so that we only // return after the the emulator has processed this. select { case <-stopQ: return 0, errors.New("terminal emulator stopped") case writeQ <- drainQ: } select { case <-stopQ: return 0, errors.New("terminal emulator stopped") case <-drainQ: return len(data), nil } case <-stopQ: return 0, errors.New("terminal emulator stopped") } } // Read data (key events, etc.) from the emulator. func (em *emulator) Read(data []byte) (n int, err error) { stopQ := em.stopQ readQ := em.readQ n = 0 if len(data) < 1 { return 0, nil } select { case <-stopQ: return 0, errors.New("terminal emulator stopped") case v := <-readQ: // The data arriving in the channel may be a byte, or it might be a bool // trying to force a wake up. Note that the bool may be intermingled with other // bytes, so we check it. Also data may have arrived since the bool was posted, // so make sure we don't terminate until we have collected all the relevant data // that we can (up to the limit of what was requested.) if ch, ok := v.(byte); ok { data[n] = ch n++ } for n < len(data) { select { case v = <-readQ: if ch, ok := v.(byte); ok { data[n] = ch n++ } default: return n, nil } } return n, nil } } func (em *emulator) run(stopQ <-chan bool) { for { select { case item := <-em.writeQ: switch d := item.(type) { case byte: em.inb(d) case []byte: for _, ch := range d { em.inb(ch) } case chan bool: close(d) case Coord: // resize notification em.applyResize(d) } case <-stopQ: return } } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/emulate_bench_test.go000066400000000000000000000045551520475227200250610ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "testing" "github.com/gdamore/tcell/v3/color" ) func BenchmarkPutRuneCurrent(b *testing.B) { benchPutRune(b, "current", (*emulator).putRune) } func benchPutRune(b *testing.B, name string, put func(*emulator, rune)) { cases := []struct { name string r rune seq []rune }{ {name: "ascii", r: 'a'}, {name: "width", r: 'π'}, {name: "wide", r: '宽'}, {name: "combining", r: '\u0301'}, {name: "line", seq: []rune{ '\u250C', '\u2510', '\u2514', '\u2518', '\u2500', '\u2502', '\u251C', '\u2524', '\u252C', '\u2534', '\u253C', '\u256D', '\u256E', '\u256F', '\u2570', '\u2571', }}, {name: "mixed32", seq: []rune{ '\u0416', '\u0414', '\u042E', '\u042F', '\u041F', '\u041B', '\u0424', '\u042B', '\u042D', '\u0411', '\u0413', '\u0428', '\u00E9', '\u00F6', '\u00FC', '\u00F1', '\u00E7', '\u00F8', '\u00E5', '\u00DF', '\u0142', '\u0111', '\u0127', '\u0131', '\U0001F600', '\U0001F680', '\U0001F9EA', '\U0001F525', '\U0001F355', '\U0001F389', '\U0001F4BB', '\U0001F4E6', }}, {name: "mixed64", seq: sweepMixedRunes64}, } for _, tc := range cases { b.Run(name+"/"+tc.name, func(b *testing.B) { em := NewEmulator(NewMockBackend(MockOptSize{X: 8, Y: 1}, MockOptColors(0))).(*emulator) em.style = BaseStyle.WithFg(color.White).WithBg(color.Black) em.defaultStyle = em.style em.localModes[PmGraphemeClusters] = ModeOn em.localModes[PmAutoMargin] = ModeOn em.cells[0].S = em.style em.cells[0].W = 1 b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { r := tc.r if len(tc.seq) != 0 { r = tc.seq[i%len(tc.seq)] } em.pos = Coord{X: 1, Y: 0} em.lastIndex = 1 em.autoWrap = false em.cells[0].C = "e" em.cells[0].S = em.style em.cells[0].W = 1 put(em, r) } }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/emulate_internal_test.go000066400000000000000000000142431520475227200256110ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "slices" "testing" "github.com/gdamore/tcell/v3/color" ) type noMouseBackend struct { mb *mockBackend } func (n noMouseBackend) GetPrivateMode(pm PrivateMode) ModeStatus { return n.mb.GetPrivateMode(pm) } func (n noMouseBackend) SetPrivateMode(pm PrivateMode, status ModeStatus) error { return n.mb.SetPrivateMode(pm, status) } func (n noMouseBackend) GetSize() Coord { return n.mb.GetSize() } func (n noMouseBackend) Colors() int { return n.mb.Colors() } func (n noMouseBackend) Put(pos Coord, cell Cell) { n.mb.Put(pos, cell) } func (n noMouseBackend) GetPosition() Coord { return n.mb.GetPosition() } func (n noMouseBackend) SetPosition(pos Coord) { n.mb.SetPosition(pos) } func (n noMouseBackend) Reset() { n.mb.Reset() } func (n noMouseBackend) RaiseResize() { n.mb.RaiseResize() } func (n noMouseBackend) Buffering(enabled bool) { n.mb.Buffering(enabled) } func (n noMouseBackend) SetCursor(cs CursorStyle) { n.mb.SetCursor(cs) } func TestEmulatorModeHelpers(t *testing.T) { mb := NewMockBackend().(*mockBackend) em := NewEmulator(mb).(*emulator) ansiKeys := em.ansiModeKeys() if !slices.Contains(ansiKeys, AmNewLineMode) { t.Fatalf("expected AmNewLineMode in ansi mode keys, got %v", ansiKeys) } privateKeys := em.privateModeKeys() if !slices.Contains(privateKeys, PmAutoMargin) { t.Fatalf("expected PmAutoMargin in private mode keys, got %v", privateKeys) } em.setAnsiMode(AmNewLineMode, ModeOn) if got := em.getAnsiMode(AmNewLineMode); got != ModeOn { t.Fatalf("setAnsiMode did not update changeable mode: got %v", got) } em.setAnsiMode(AmNewLineMode, ModeNA) if got := em.getAnsiMode(AmNewLineMode); got != ModeOn { t.Fatalf("setAnsiMode changed mode for non-changeable status: got %v", got) } em.ansiModes[AmInsertReplace] = ModeOnLocked em.setAnsiMode(AmInsertReplace, ModeOff) if got := em.getAnsiMode(AmInsertReplace); got != ModeOnLocked { t.Fatalf("setAnsiMode changed locked mode: got %v", got) } em.setAnsiMode(AnsiMode(9999), ModeOn) if _, ok := em.ansiModes[AnsiMode(9999)]; ok { t.Fatalf("setAnsiMode created an unknown mode entry") } } func TestEmulatorUpdateMouseReporting(t *testing.T) { mb := NewMockBackend().(*mockBackend) em := NewEmulator(mb).(*emulator) em.localModes[PmMouseButton] = ModeOn em.updateMouseReporting() if em.mouseReports != MouseButtons { t.Fatalf("expected mouse buttons reporting, got %v", em.mouseReports) } em.localModes[PmMouseDrag] = ModeOn em.updateMouseReporting() if em.mouseReports != MouseDrag { t.Fatalf("expected mouse drag reporting, got %v", em.mouseReports) } em.localModes[PmMouseMotion] = ModeOn em.updateMouseReporting() if em.mouseReports != MouseMotion { t.Fatalf("expected mouse motion reporting, got %v", em.mouseReports) } em.localModes[PmMouseButton] = ModeOff em.localModes[PmMouseDrag] = ModeOff em.localModes[PmMouseMotion] = ModeOff em.localModes[PmMouseX10] = ModeOn em.updateMouseReporting() if em.mouseReports != MouseButtons { t.Fatalf("expected mouse X10 reporting to map to buttons, got %v", em.mouseReports) } em.localModes[PmMouseX10] = ModeOff em.updateMouseReporting() if em.mouseReports != MouseDisabled { t.Fatalf("expected mouse reporting disabled, got %v", em.mouseReports) } noMouse := NewEmulator(noMouseBackend{mb: NewMockBackend().(*mockBackend)}).(*emulator) noMouse.updateMouseReporting() } func TestMockBackendHelpers(t *testing.T) { mb := NewMockBackend().(*mockBackend) style := BaseStyle.WithFg(color.Red).WithBg(color.Blue) mb.SetStyle(style) if mb.style != style { t.Fatalf("SetStyle did not update backend style") } mb.SetMouse(MouseMotion) mb.Buffering(true) mb.Buffering(false) MockOptNoBlit{}.SetMockOpt(mb) } func TestApplyResizePrunesTabStops(t *testing.T) { em := NewEmulator(NewMockBackend(MockOptSize{X: 80, Y: 24})).(*emulator) em.tabStops = []Col{8, 16, 24, 32, 40, 48} em.applyResize(Coord{X: 32, Y: 24}) if got, want := em.tabStops, []Col{8, 16, 24}; !slices.Equal(got, want) { t.Fatalf("unexpected tab stops after resize: got %v, want %v", got, want) } em.setPosition(Coord{X: 0, Y: 0}) for _, want := range []Col{8, 16, 24, 31} { em.nextTab() if got := em.getPosition().X; got != want { t.Fatalf("unexpected tab position after resize: got %d, want %d", got, want) } } } func TestPutRuneGraphemeExtensions(t *testing.T) { t.Parallel() newEm := func() *emulator { em := NewEmulator(NewMockBackend(MockOptSize{X: 8, Y: 1}, MockOptColors(0))).(*emulator) em.style = BaseStyle.WithFg(color.White).WithBg(color.Black) em.defaultStyle = em.style em.localModes[PmGraphemeClusters] = ModeOn em.localModes[PmAutoMargin] = ModeOn em.cells[0].S = em.style em.cells[0].W = 1 return em } t.Run("combining", func(t *testing.T) { em := newEm() em.cells[0].C = "e" em.lastIndex = 1 em.pos = Coord{X: 1, Y: 0} em.putRune('\u0301') if got := em.cells[0].C; got != "e\u0301" { t.Fatalf("unexpected cluster: %q", got) } if got := em.cells[0].W; got != 1 { t.Fatalf("unexpected width: got %d, want 1", got) } }) t.Run("variation-selector", func(t *testing.T) { em := newEm() em.cells[0].C = "\u2764" em.lastIndex = 1 em.pos = Coord{X: 1, Y: 0} em.putRune('\uFE0F') if got := em.cells[0].W; got != 2 { t.Fatalf("unexpected width: got %d, want 2", got) } }) t.Run("regional-indicator", func(t *testing.T) { em := newEm() em.cells[0].C = "\U0001F1E6" em.lastIndex = 1 em.pos = Coord{X: 1, Y: 0} em.putRune('\U0001F1E7') if got := em.cells[0].W; got != 2 { t.Fatalf("unexpected width: got %d, want 2", got) } }) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/event.go000066400000000000000000000073441520475227200223470ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt // KeyEvent is a key event. type KeyEvent struct { Down bool // true if event is for key down event Repeat int // if > 1, a repeat count Key Key // Key symbol. Base BaseKey // base key code (physical key, e.g 'a'), may be zero if same as code VK WinVK // Windows virtual key. 0 for none, or if not known. SC ScanCode // Windows scan code. 0 for none, or if not known. Mod Modifier // modifiers Utf string // if non-empty, the unicode content for this } type Modifier int const ( ModNone = Modifier(0) ModLShift = Modifier(1 << iota) ModRShift ModLCtrl ModRCtrl ModLAlt ModRAlt ModLMeta ModRMeta ModLHyper ModRHyper ModCapsLock ModNumLock ) func (m Modifier) IsShift() bool { return (m & (ModLShift | ModRShift)) != 0 } func (m Modifier) IsCtrl() bool { return (m & (ModLCtrl | ModRCtrl)) != 0 } func (m Modifier) IsAlt() bool { return (m & (ModLAlt | ModRAlt)) != 0 } func (m Modifier) IsMeta() bool { return (m & (ModLMeta | ModRMeta)) != 0 } func (m Modifier) IsHyper() bool { return (m & (ModLHyper | ModRHyper)) != 0 } func (m Modifier) IsNumLock() bool { return m&ModNumLock != 0 } func (m Modifier) IsCapsLock() bool { return m&ModCapsLock != 0 } func (m Modifier) IsCapitals() bool { return m.IsCapsLock() != m.IsShift() } func (m Modifier) IsAltGr() bool { return m.IsCtrl() && m.IsAlt() } // Button is the mouse button pressed or released. type Button int const ( NoButton = Button(0) // No buttons are pressed. Button1 = Button(1 << iota) // Usually left most button. Button2 // Usually right most button. Button3 // Usually middle button. Button4 Button5 Button6 Button7 Button8 WheelUp // Wheel motion up/away from user. WheelDown // Wheel motion down/towards user. WheelLeft // Wheel motion to left. WheelRight // Wheel motion to right. ) // MouseEvent reports a single mouse event. Only a single button // may be reported for a given event. The application will have // to keep state. As buttons are never pressed exactly simultaneously, // the backend will send chords as a series of presses followed by a series // of releases. type MouseEvent struct { Position Coord // Location of pointer. Button Button // Buttons pressed. Down bool // True on press, false on release. Motion bool // True if mouse moved at least once cell. Mod Modifier // Modifiers (for modified click). } // encodeButton just encodes the XTerm style button details into a byte func (ev MouseEvent) encodeButton() byte { var btn byte switch ev.Button { case NoButton: btn = 3 case Button1: btn = 0 case Button2: // intentionally reversed with button 3 btn = 2 case Button3: btn = 1 case WheelUp: btn = 0x40 case WheelDown: btn = 0x41 case WheelLeft: btn = 0x42 case WheelRight: btn = 0x43 case Button4: btn = 0x80 case Button5: btn = 0x81 case Button6: btn = 0x82 case Button7: btn = 0x83 default: btn = 3 } if ev.Motion { btn += 0x20 } if ev.Mod.IsShift() { btn += 4 } if ev.Mod.IsAlt() || ev.Mod.IsMeta() { btn += 8 } if ev.Mod.IsCtrl() { btn += 16 } return btn } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/key.go000066400000000000000000000444271520475227200220210ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt // BaseKey is the Kitty protocol base key. These are kitty's representation of a scan code. // As the Kitty protocol is likely the extended keyboard protocol we care most about, we use // this as the primary reporting mechanism. (It also helps that this may provide an easier // fallback for implementations that don't have raw scan codes and are willing to assume an // ANSI layout.) type BaseKey rune var shiftedBaseKeys map[BaseKey]rune func (bk BaseKey) Shifted() rune { if s, ok := shiftedBaseKeys[bk]; ok { return s } if bk >= 'a' && bk <= 'z' { return rune(bk - 32) } return rune(bk) } // ScanCode is the scan code used by Windows for a key. These are physical key locations, // and every physical should have a exactly one mapping here. type ScanCode uint16 // Key represents the key code for a key. These are largely taken from the HID specification page 0x07, // although there are gaps and some inconsistencies. We chose this specification because it covers all // keyboards we are likely to see in practice. Note that the zero value is reserved and will never be // assigned a valid key. A number of keys are from UNIX keyboards (e.g. Sun type 6 keyboards), and we do not // explicitly support these because none of the common reporting protocols have a way to report them. // Instead these should probably be mapped higher functions (F13 and up), if you're facing this. // These are key locations on a US keyboard. For example on AZERTY layout KeyQ corresponds to a key // that has a printed "A", but is in the upper left position (below the digits.) type Key uint16 const ( KeyUTF = Key(0x01) // virtual UTF-8 content KeyA = Key(0x04) KeyB = Key(0x05) KeyC = Key(0x06) KeyD = Key(0x07) KeyE = Key(0x08) KeyF = Key(0x09) KeyG = Key(0x0A) KeyH = Key(0x0B) KeyI = Key(0x0C) KeyJ = Key(0x0D) KeyK = Key(0x0E) KeyL = Key(0x0F) KeyM = Key(0x10) KeyN = Key(0x11) KeyO = Key(0x12) KeyP = Key(0x13) KeyQ = Key(0x14) KeyR = Key(0x15) KeyS = Key(0x16) KeyT = Key(0x17) KeyU = Key(0x18) KeyV = Key(0x19) KeyW = Key(0x1A) KeyX = Key(0x1B) KeyY = Key(0x1C) KeyZ = Key(0x1D) Key1 = Key(0x1E) Key2 = Key(0x1F) Key3 = Key(0x20) Key4 = Key(0x21) Key5 = Key(0x22) Key6 = Key(0x23) Key7 = Key(0x24) Key8 = Key(0x25) Key9 = Key(0x26) Key0 = Key(0x27) KeyEnter = Key(0x28) KeyEsc = Key(0x29) KeyBackspace = Key(0x2a) // sometimes called delete KeyTab = Key(0x2b) KeySpace = Key(0x2c) KeyMinus = Key(0x2d) // - and _ KeyEqual = Key(0x2e) // = and + KeyLBrace = Key(0x2f) // [ and { KeyRBrace = Key(0x30) // ] and } KeyBackslash = Key(0x31) // \ KeyIsoHash = Key(0x32) // international only KeySemi = Key(0x33) // ; KeyQuote = Key(0x34) // ' and " aka apostrophe KeyGrave = Key(0x35) // ` and ~ KeyComma = Key(0x36) // , and < KeyPeriod = Key(0x37) // . and > KeySlash = Key(0x38) // / and ? KeyCapsLock = Key(0x39) KeyF1 = Key(0x3a) KeyF2 = Key(0x3b) KeyF3 = Key(0x3c) KeyF4 = Key(0x3d) KeyF5 = Key(0x3e) KeyF6 = Key(0x3f) KeyF7 = Key(0x40) KeyF8 = Key(0x41) KeyF9 = Key(0x42) KeyF10 = Key(0x43) KeyF11 = Key(0x44) KeyF12 = Key(0x45) KeyPrtScr = Key(0x46) KeyScrLock = Key(0x47) KeyPause = Key(0x48) KeyInsert = Key(0x49) KeyHome = Key(0x4a) KeyPgUp = Key(0x4b) KeyDelete = Key(0x4c) // forward delete (DEL) KeyEnd = Key(0x4d) KeyPgDn = Key(0x4e) KeyRight = Key(0x4f) KeyLeft = Key(0x50) KeyDown = Key(0x51) KeyUp = Key(0x52) KeyNumLock = Key(0x53) // also clear KeyPadDiv = Key(0x54) KeyPadMul = Key(0x55) KeyPadSub = Key(0x56) KeyPadAdd = Key(0x57) KeyPadEnter = Key(0x58) KeyPad1 = Key(0x59) // also pad end KeyPad2 = Key(0x5a) // also pad down KeyPad3 = Key(0x5b) // also pad page down KeyPad4 = Key(0x5c) // also pad left KeyPad5 = Key(0x5d) KeyPad6 = Key(0x5e) // also pad right KeyPad7 = Key(0x5f) // also pad home KeyPad8 = Key(0x60) // also pad up KeyPad9 = Key(0x61) // also pad page up KeyPad0 = Key(0x62) // also pad insert KeyPadDec = Key(0x63) // also pad delete KeyIsoBackSlash = Key(0x64) // international keyboards only KeyPadEqual = Key(0x67) KeyF13 = Key(0x68) KeyF14 = Key(0x69) KeyF15 = Key(0x6a) KeyF16 = Key(0x6b) KeyF17 = Key(0x6c) KeyF18 = Key(0x6d) KeyF19 = Key(0x6e) KeyF20 = Key(0x6f) KeyF21 = Key(0x70) KeyF22 = Key(0x71) KeyF23 = Key(0x72) KeyF24 = Key(0x73) KeyMenu = Key(0x76) // might also be 0x65, KeyApplication KeyPadComma = Key(0x85) KeyPadEqSign = Key(0x86) KeyIsoSlash = Key(0x87) // also International 1 KeyHiragana = Key(0x88) // also International 2 KeyYen = Key(0x89) // found on JIS keyboards KeyConvert = Key(0x8a) KeyNonConvert = Key(0x8b) KeyAltErase = Key(0x99) // e.g split space-erase bar KeySysReq = Key(0x9a) KeyCancel = Key(0x9b) KeyLCtrl = Key(0xe0) KeyLShift = Key(0xe1) KeyLAlt = Key(0xe2) KeyLMeta = Key(0xe3) KeyRCtrl = Key(0xe4) KeyRShift = Key(0xe5) KeyRAlt = Key(0xe6) KeyRMeta = Key(0xe7) KeyLHyper = Key(0xff01) // software only, in reserved space KeyRHyper = Key(0xff02) // software only, in reserved space ) // scanCodes is a list of Windows scan codes for physical keys. var scanCodes map[Key]ScanCode // ScanCode returns the corresponding Windows Scan Code (not a VK!) // for the given key. (Virtual keys should be determined by the // host OS using the current keyboard layout.) func (k Key) ScanCode() ScanCode { if w, ok := scanCodes[k]; ok { return w } return 0 } // WinVK represents a windows virtual key code. // These are similar to base keys, but multiple scanned key codes // may result in the same virtual key. This can also be sensitive to // the keyboard layout. type WinVK rune const ( VkBack = WinVK(0x08) // backspace VkTab = WinVK(0x09) VkClear = WinVK(0x0c) VkReturn = WinVK(0x0d) VkShift = WinVK(0x10) VkControl = WinVK(0x11) VkMenu = WinVK(0x12) VkPause = WinVK(0x13) VkCapital = WinVK(0x14) // caps lock VkKana = WinVK(0x15) VkHangul = WinVK(0x15) VkImeOn = WinVK(0x16) VkJunja = WinVK(0x17) VkFinal = WinVK(0x18) VkKanji = WinVK(0x19) VkImeOff = WinVK(0x1a) VkEscape = WinVK(0x1b) VkConvert = WinVK(0x1c) VkNonConvert = WinVK(0x1d) VkAccept = WinVK(0x1e) VkModeChange = WinVK(0x1f) VkSpace = WinVK(0x20) VkPrior = WinVK(0x21) // page up VkNext = WinVK(0x22) // page down VkEnd = WinVK(0x23) VkHome = WinVK(0x24) VkLeft = WinVK(0x25) VkUp = WinVK(0x26) VkRight = WinVK(0x27) VkDown = WinVK(0x28) VkSelect = WinVK(0x29) VkPrint = WinVK(0x2a) VkExecute = WinVK(0x2b) VkSnapshot = WinVK(0x2c) // print screen VkInsert = WinVK(0x2D) VkDelete = WinVK(0x2E) VkHelp = WinVK(0x2F) Vk0 = WinVK(0x30) Vk1 = WinVK(0x31) Vk2 = WinVK(0x32) Vk3 = WinVK(0x33) Vk4 = WinVK(0x34) Vk5 = WinVK(0x35) Vk6 = WinVK(0x36) Vk7 = WinVK(0x37) Vk8 = WinVK(0x38) Vk9 = WinVK(0x39) VkA = WinVK(0x41) VkB = WinVK(0x42) VkC = WinVK(0x43) VkD = WinVK(0x44) VkE = WinVK(0x45) VkF = WinVK(0x46) VkG = WinVK(0x47) VkH = WinVK(0x48) VkI = WinVK(0x49) VkJ = WinVK(0x4a) VkK = WinVK(0x4b) VkL = WinVK(0x4c) VkM = WinVK(0x4d) VkN = WinVK(0x4e) VkO = WinVK(0x4f) VkP = WinVK(0x50) VkQ = WinVK(0x51) VkR = WinVK(0x52) VkS = WinVK(0x53) VkT = WinVK(0x54) VkU = WinVK(0x55) VkV = WinVK(0x56) VkW = WinVK(0x57) VkX = WinVK(0x58) VkY = WinVK(0x59) VkZ = WinVK(0x5a) VkLWin = WinVK(0x5b) // left meta VkRWin = WinVK(0x5c) // right meta VkApps = WinVK(0x5d) // menu key VkNumPad0 = WinVK(0x60) VkNumPad1 = WinVK(0x61) VkNumPad2 = WinVK(0x62) VkNumPad3 = WinVK(0x63) VkNumPad4 = WinVK(0x64) VkNumPad5 = WinVK(0x65) VkNumPad6 = WinVK(0x66) VkNumPad7 = WinVK(0x67) VkNumPad8 = WinVK(0x68) VkNumPad9 = WinVK(0x69) VkMultiply = WinVK(0x6a) // pad multiply VkAdd = WinVK(0x6b) // pad add VkSeparator = WinVK(0x6c) // separator VkSubtract = WinVK(0x6d) // pad subtract VkDecimal = WinVK(0x6e) // pad decimal point VkDivide = WinVK(0x6f) // pad divide VkF1 = WinVK(0x70) VkF2 = WinVK(0x71) VkF3 = WinVK(0x72) VkF4 = WinVK(0x73) VkF5 = WinVK(0x74) VkF6 = WinVK(0x75) VkF7 = WinVK(0x76) VkF8 = WinVK(0x77) VkF9 = WinVK(0x78) VkF10 = WinVK(0x79) VkF11 = WinVK(0x7a) VkF12 = WinVK(0x7b) VkF13 = WinVK(0x7c) VkF14 = WinVK(0x7d) VkF15 = WinVK(0x7e) VkF16 = WinVK(0x7f) VkF17 = WinVK(0x80) VkF18 = WinVK(0x81) VkF19 = WinVK(0x82) VkF20 = WinVK(0x83) VkF21 = WinVK(0x84) VkF22 = WinVK(0x85) VkF23 = WinVK(0x86) VkF24 = WinVK(0x87) VkNumLock = WinVK(0x90) VkScroll = WinVK(0x91) // scroll lock VkLShift = WinVK(0xa0) VkRShift = WinVK(0xa1) VkLControl = WinVK(0xa2) VkRControl = WinVK(0xa3) VkLMenu = WinVK(0xa4) // left alt VkRMenu = WinVK(0xa5) // right alt VkOem1 = WinVK(0xba) // ; and : VkOemPlus = WinVK(0xbb) // = and + VkOemComma = WinVK(0xbc) // , and < VkOemMinus = WinVK(0xbd) // - and _ VkOemPeriod = WinVK(0xbe) // . and > VkOem2 = WinVK(0xbf) // / and ? VkOem3 = WinVK(0xc0) // ` and ~ VkOem4 = WinVK(0xdb) // [ and { VkOem5 = WinVK(0xdc) // \ and | VkOem6 = WinVK(0xdd) // ] and } VkOem7 = WinVK(0xde) // ' and " VkOem8 = WinVK(0xdf) // right control for Canadian CSA VkOem102 = WinVK(0xe2) // ISO backslash VkPacket = WinVK(0xe7) ) var baseKeys map[Key]BaseKey // KittyBase returns the corresponding Kitty "base" key for the given USB code. // If no corresponding value can be found, then zero is returned. Note that // some keys (such as F1) are valid, and recognized by Kitty, but do not use the // base key encoding because they use another reporting format. func (k Key) KittyBase() BaseKey { if w, ok := baseKeys[k]; ok { return w } return 0 } func init() { // we place them in init to avoid incorrect processing in coverage checks. baseKeys = map[Key]BaseKey{ KeyA: 'a', KeyB: 'b', KeyC: 'c', KeyD: 'd', KeyE: 'e', KeyF: 'f', KeyG: 'g', KeyH: 'h', KeyI: 'i', KeyJ: 'j', KeyK: 'k', KeyL: 'l', KeyM: 'm', KeyN: 'n', KeyO: 'o', KeyP: 'p', KeyQ: 'q', KeyR: 'r', KeyS: 's', KeyT: 't', KeyU: 'u', KeyV: 'v', KeyW: 'w', KeyX: 'x', KeyY: 'y', KeyZ: 'z', Key1: '1', Key2: '2', Key3: '3', Key4: '4', Key5: '5', Key6: '6', Key7: '7', Key8: '8', Key9: '9', Key0: '0', KeyEnter: '\r', KeyEsc: '\x1b', KeyBackspace: '\x7f', KeyTab: '\t', KeySpace: ' ', KeyMinus: '-', KeyEqual: '=', KeyLBrace: '[', KeyRBrace: ']', KeyBackslash: '\\', KeyIsoHash: '#', KeySemi: ';', KeyQuote: '\'', KeyGrave: '`', KeyComma: ',', KeyPeriod: '.', KeySlash: '/', KeyCapsLock: 57357, KeyPrtScr: 57361, KeyScrLock: 57359, KeyPause: 57362, KeyNumLock: 57360, KeyPadDiv: 57410, KeyPadMul: 57411, KeyPadSub: 57412, KeyPadAdd: 57413, KeyPadEnter: 57414, KeyPad1: 57400, KeyPad2: 57401, KeyPad3: 57402, KeyPad4: 57403, KeyPad5: 57404, KeyPad6: 57405, KeyPad7: 57406, KeyPad8: 57407, KeyPad9: 57408, KeyPad0: 57399, KeyPadDec: 57409, KeyIsoBackSlash: '\\', // TODO: does kitty have another mapping for this? KeyMenu: 57363, KeyPadEqual: 57415, KeyF13: 57376, KeyF14: 57377, KeyF15: 57378, KeyF16: 57379, KeyF17: 57380, KeyF18: 57381, KeyF19: 57382, KeyF20: 57383, KeyF21: 57384, KeyF22: 57385, KeyF23: 57386, KeyF24: 57387, // NB: F25 up through 35 are notionally supported by Kitty, but not by us KeyPadComma: 57416, KeyIsoSlash: '/', // Kitty cannot discriminate? KeyYen: '¥', KeyLCtrl: 57442, KeyLShift: 57441, KeyLAlt: 57443, KeyLMeta: 57444, KeyRCtrl: 57448, KeyRShift: 57447, KeyRAlt: 57449, KeyRMeta: 57450, // KeyHiragana: 0, // Later // KeyConvert: 0, // Later // KeyNonConvert: 0, // Later // Windows uses a bunch of HID usages from // the consumer page (0x0c) for media playback, and // other applications. We just ignore them. } // Scan codes used by Windows. scanCodes = map[Key]ScanCode{ KeyA: 0x1e, KeyB: 0x30, KeyC: 0x2e, KeyD: 0x20, KeyE: 0x12, KeyF: 0x21, KeyG: 0x22, KeyH: 0x23, KeyI: 0x17, KeyJ: 0x24, KeyK: 0x25, KeyL: 0x26, KeyM: 0x32, KeyN: 0x31, KeyO: 0x18, KeyP: 0x19, KeyQ: 0x10, KeyR: 0x13, KeyS: 0x1f, KeyT: 0x14, KeyU: 0x16, KeyV: 0x2f, KeyW: 0x11, KeyX: 0x2d, KeyY: 0x15, KeyZ: 0x2c, Key1: 0x02, Key2: 0x03, Key3: 0x04, Key4: 0x05, Key5: 0x06, Key6: 0x07, Key7: 0x08, Key8: 0x09, Key9: 0x0a, Key0: 0x0b, KeyEnter: 0x1c, KeyEsc: 0x01, KeyBackspace: 0x0e, KeyTab: 0x0f, KeySpace: 0x39, KeyMinus: 0x0c, KeyEqual: 0x0d, KeyLBrace: 0x1a, KeyRBrace: 0x1b, KeyBackslash: 0x2b, KeySemi: 0x27, KeyQuote: 0x28, KeyGrave: 0x29, KeyComma: 0x33, KeyPeriod: 0x34, KeySlash: 0x35, KeyCapsLock: 0x3a, KeyF1: 0x3b, KeyF2: 0x3c, KeyF3: 0x3d, KeyF4: 0x3e, KeyF5: 0x3f, KeyF6: 0x40, KeyF7: 0x41, KeyF8: 0x42, KeyF9: 0x43, KeyF10: 0x44, KeyF11: 0x57, KeyF12: 0x58, KeyPrtScr: 0x54, KeyScrLock: 0x46, KeyPause: 0xe046, KeyInsert: 0xe052, KeyHome: 0xe047, KeyPgUp: 0xe049, KeyDelete: 0xe053, KeyEnd: 0xe04f, KeyPgDn: 0xe051, KeyRight: 0xe04d, KeyLeft: 0xe04b, KeyDown: 0xe050, KeyUp: 0xe048, KeyNumLock: 0x45, KeyPadDiv: 0xe035, KeyPadMul: 0x37, KeyPadSub: 0x4a, KeyPadAdd: 0x4e, KeyPadEnter: 0xe01c, KeyPad1: 0x4f, KeyPad2: 0x50, KeyPad3: 0x51, KeyPad4: 0x4b, KeyPad5: 0x4c, KeyPad6: 0x4d, KeyPad7: 0x47, KeyPad8: 0x48, KeyPad9: 0x49, KeyPad0: 0x52, KeyPadDec: 0x53, KeyIsoBackSlash: 0x56, KeyPadEqual: 0x59, KeyF13: 0x64, KeyF14: 0x65, KeyF15: 0x66, KeyF16: 0x67, KeyF17: 0x68, KeyF18: 0x69, KeyF19: 0x6a, KeyF20: 0x6b, KeyF21: 0x6c, KeyF22: 0x6d, KeyF23: 0x6e, KeyF24: 0x76, KeyPadComma: 0x7e, KeyIsoSlash: 0x73, KeyHiragana: 0x70, KeyYen: 0x7d, KeyConvert: 0x79, KeyNonConvert: 0x7b, KeyLCtrl: 0x1d, KeyLShift: 0x2a, KeyLAlt: 0x38, KeyLMeta: 0xe05b, KeyRCtrl: 0xe01d, KeyRShift: 0x36, KeyRAlt: 0xe038, KeyRMeta: 0xe05c, KeyMenu: 0xe05d, // Windows uses a bunch of HID usages from // the consumer page (0x0c) for media playback, and // other applications. We just ignore them. } shiftedBaseKeys = map[BaseKey]rune{ '`': '~', '1': '!', '2': '@', '3': '#', '4': '$', '5': '%', '6': '^', '7': '&', '8': '*', '9': '(', '0': ')', '-': '_', '=': '+', '¥': '|', '[': '{', ']': '}', '\\': '|', ';': ':', '\'': '"', ',': '<', '.': '>', '/': '?', } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layout.go000066400000000000000000000432471520475227200225450ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "sort" "sync" "time" ) // This file defines the layout mechanism for keyboards. // All keyboards are assumed to pass "codes" that are mapped to one of // the Key values (this is easy for USB because the codes are mostly the // same as USB already!), and then we look up values. This can be done // using a simple map, and we allow inheritance so we don't have to redefine // too many things. For complex scenarios you'll probably want to make use // of operating system facilities or supply your own tables (for example for // Japanese keyboards.) // ModifierMap is a map that represents keys that generate runes in various // shift states (modifier states). A layout may have many of these, and they // are searched until a match is fine. type ModifierMap struct { When func(Modifier) bool // Map only applies if this returns true Map map[Key]rune // Map is the mapping from Key to specific rune when this map matches. } // KeyboardState represents the current state of the keyboard. // A zero initialized value is ready for use. Note that emulators that // get the associated events from their operating system do not need to // make use of this, but this structure makes it possible to build an emulator // with a keyboard layout that is not known to the operating system. // // The keyboard state is assumed to be "single threaded", meaning only a single // caller will operate on it at any given time. Typically there is just a single // keyboard polling thread or goroutine. type KeyboardState struct { deadKey map[rune]DeadKey // current dead key state mod Modifier layout *Layout lastKey Key // last key pressed lastRune rune // last rune for last key repeating bool // true if we are repeating repeatStart time.Time // when we started repeating repeatTime time.Time // last time we checked repeatDelay time.Duration // delay before starting repeat repeatInterval time.Duration // duration between repeats pressed map[Key]bool initialized bool } // initialize the keyboard, lazily. func (ks *KeyboardState) initialize() { if !ks.initialized { ks.pressed = make(map[Key]bool) if ks.layout == nil { ks.layout = KeyboardANSI } ks.repeatDelay = time.Millisecond * 250 ks.repeatInterval = time.Millisecond * 30 ks.initialized = true } } // reset the keyboard state. func (ks *KeyboardState) reset() { ks.clearRepeat() ks.mod = 0 ks.deadKey = nil ks.pressed = make(map[Key]bool) } // clear repeat clears any repeating key. func (ks *KeyboardState) clearRepeat() { ks.repeating = false ks.repeatStart = time.Time{} ks.repeatTime = time.Time{} ks.lastRune = 0 ks.lastKey = 0 } // SetRepeat sets the repeat parameters. Note that this will only have any meaningful // impact if the caller calls the Pressed function repeatedly (periodically) while // a key is depressed. // // The repeat starts after a key has been held for for delay, with a new repeat // added every interval. // // The caller should usually call this before processing keyboard events. It must // not be called concurrently with either of the Pressed or Release functions. func (ks *KeyboardState) SetRepeat(delay time.Duration, interval time.Duration) { ks.initialize() ks.repeatDelay = delay ks.repeatInterval = interval ks.clearRepeat() } // SetLayout sets the layout this keyboard should use. // This also resets the keyboard state. func (ks *KeyboardState) SetLayout(km *Layout) { ks.initialize() ks.reset() ks.layout = km } // Pressed should be called when a given key is depressed. func (ks *KeyboardState) Pressed(k Key) *KeyEvent { ks.initialize() event := &KeyEvent{ Down: true, Key: k, SC: k.ScanCode(), Base: k.KittyBase(), Mod: ks.mod, } // if another key was pressed, then clear the repeat state lastKey := ks.lastKey if lastKey != k && ks.repeatInterval != 0 { ks.clearRepeat() ks.repeatStart = time.Now().Add(ks.repeatDelay) ks.repeatTime = ks.repeatStart } wasPressed := ks.pressed[k] ks.pressed[k] = true ks.lastKey = k l := ks.layout event.VK = l.Virtual[k] if mod, ok := l.Locking[k]; ok { // locking modifiers never repeat if wasPressed { return nil } if ks.mod&mod == 0 { ks.mod |= mod } else { ks.mod &^= mod } ks.pressed[k] = true event.Mod = ks.mod return event } if mod, ok := l.Modifiers[k]; ok { if wasPressed { return nil } ks.mod |= mod event.Mod = ks.mod return event } // attempt to look up the rune for this r := l.KeyToUTF(k, ks.mod) if ks.deadKey == nil && l.DeadKeys != nil && r != 0 { if dk, ok := l.DeadKeys[r]; ok { ks.deadKey = dk.Next return event } } if dk := ks.deadKey; dk != nil { if n, ok := dk[r]; ok { if n.U != 0 { event.Utf = string(n.U) ks.lastRune = n.U ks.deadKey = nil } else { ks.deadKey = n.Next } return event } // failed lookup - ignore it ks.deadKey = nil } if r != 0 { event.Utf = string(r) } if lastKey == k && wasPressed && ks.repeatInterval > 0 { ks.repeating = true if time.Now().After(ks.repeatStart) { deltaT := time.Since(ks.repeatTime).Truncate(ks.repeatInterval) event.Repeat = int(deltaT / ks.repeatInterval) if ks.repeatTime == ks.repeatStart { // fence post - count the first one! event.Repeat++ } ks.repeatTime = ks.repeatTime.Add(deltaT) // if we polled before the repeat interval then report nothing if event.Repeat == 0 { return nil } } } return event } // Released should be called when a given key is Released. func (ks *KeyboardState) Released(k Key) *KeyEvent { ks.initialize() event := &KeyEvent{ Down: false, Key: k, SC: k.ScanCode(), Base: k.KittyBase(), Mod: ks.mod, } if ks.lastKey == k { ks.clearRepeat() } wasPressed := ks.pressed[k] delete(ks.pressed, k) l := ks.layout event.VK = l.Virtual[k] if mod, ok := ks.layout.Modifiers[k]; ok { if !wasPressed { // something weird return nil } ks.mod &^= mod event.Mod = ks.mod return event } if _, ok := ks.layout.Locking[k]; !ok { if r := l.KeyToUTF(k, ks.mod); r != 0 { event.Utf = string(r) } } // no real point in looking up UTF for key release, so we don't return event } // DeadKey is what happens when a dead key is pressed. Either it starts an unresolved sequence, // (in which case Next will be non-nil), or it resolves to a final rune (in which case U will be non-zero) // This is also sometimes called a composed key sequence. type DeadKey struct { Next map[rune]DeadKey // Next corresponds to the next key in the sequence for dead keys U rune // U is the rune that should be emitted on completion of the sequence. } // DeadRune is used internally to create a rune to represent a dead key. // While any numbers can be used from this point on, the expectation is // that this will be added to a smaller rune (such as an ASCII character) // This rune value a bit inside the supplementary private use area B, // in an attempt to minimize the chance of any conflicting use (for // example nerd fonts.) const DeadRune = 0x101000 // Layout is a structure that represents a keyboard layout. // Applications or users may implement their own layouts by // registering an instance of this. type Layout struct { // Name is the name of the keyboard layout. // We prefer to use the same names that Microsoft uses for keyboard layouts. Name string // Base is a base keyboard layout, so that we can simplify by only // overriding keys we are are handling differently. Base *Layout // DeadKeys maps specific starting keys, to an emitted rune. // The keys should a rune value starting with DeadRune. DeadKeys map[rune]DeadKey // Locking are modifiers that toggle a locked state like NumLock or CapsLock. Locking map[Key]Modifier // Modifiers toggle a state, but only while the key is depressed. Modifiers map[Key]Modifier // Virtual maps keys to virtual keys. Virtual map[Key]WinVK // Maps is the list of key maps by modifier and mask. // Use more specific masks first - for example NumLock // mask might contain only the masks for the number keypad, // and that should be listed before the maps for the rest // of the keyboard. The algorithm searches for the first // match, not the best match. Maps []ModifierMap } func (km *Layout) KeyToUTF(k Key, m Modifier) rune { for _, mm := range km.Maps { if mm.When != nil && !mm.When(m) { continue } if u, ok := mm.Map[k]; ok { return u } } if km.Base != nil { return km.Base.KeyToUTF(k, m) } return 0 } // KeysUsLower is a list of lower case key maps. var KeysUsLower = map[Key]rune{ KeyA: 'a', KeyB: 'b', KeyC: 'c', KeyD: 'd', KeyE: 'e', KeyF: 'f', KeyG: 'g', KeyH: 'h', KeyI: 'i', KeyJ: 'j', KeyK: 'k', KeyL: 'l', KeyM: 'm', KeyN: 'n', KeyO: 'o', KeyP: 'p', KeyQ: 'q', KeyR: 'r', KeyS: 's', KeyT: 't', KeyU: 'u', KeyV: 'v', KeyW: 'w', KeyX: 'x', KeyY: 'y', KeyZ: 'z', } // KeysUsUpper is a list of upper case key maps. var KeysUsUpper = map[Key]rune{ KeyA: 'A', KeyB: 'B', KeyC: 'C', KeyD: 'D', KeyE: 'E', KeyF: 'F', KeyG: 'G', KeyH: 'H', KeyI: 'I', KeyJ: 'J', KeyK: 'K', KeyL: 'L', KeyM: 'M', KeyN: 'N', KeyO: 'O', KeyP: 'P', KeyQ: 'Q', KeyR: 'R', KeyS: 'S', KeyT: 'T', KeyU: 'U', KeyV: 'V', KeyW: 'W', KeyX: 'X', KeyY: 'Y', KeyZ: 'Z', } // KeysDigits is a map of number keys to corresponding digits. var KeysDigits = map[Key]rune{ Key1: '1', Key2: '2', Key3: '3', Key4: '4', Key5: '5', Key6: '6', Key7: '7', Key8: '8', Key9: '9', Key0: '0', } // KeysPadDigits is a map of the digits on the numeric keypad, // when num lock is engaged. var KeysPadDigits = map[Key]rune{ KeyPad0: '0', KeyPad1: '1', KeyPad2: '2', KeyPad3: '3', KeyPad4: '4', KeyPad5: '5', KeyPad6: '6', KeyPad7: '7', KeyPad8: '8', KeyPad9: '9', KeyPadDec: '.', } var KeysPadOps = map[Key]rune{ KeyPadMul: '*', KeyPadAdd: '+', KeyPadSub: '-', KeyPadDiv: '/', } // KeyboardANSI is the base PC keyboard used in ANSI (US) systems. var KeyboardANSI = &Layout{ Name: "US", Base: nil, // Virtual maps physical keys to virtual keys. Virtual: map[Key]WinVK{ KeyEsc: VkEscape, KeyF1: VkF1, KeyF2: VkF2, KeyF3: VkF3, KeyF4: VkF4, KeyF5: VkF5, KeyF6: VkF6, KeyF7: VkF7, KeyF8: VkF8, KeyF9: VkF9, KeyF10: VkF10, KeyF11: VkF11, KeyF12: VkF12, KeyF13: VkF13, KeyF14: VkF14, KeyF15: VkF15, KeyF16: VkF16, KeyF17: VkF17, KeyF18: VkF18, KeyF19: VkF19, KeyF20: VkF20, KeyF21: VkF21, KeyF22: VkF22, KeyF23: VkF23, KeyF24: VkF24, KeyPrtScr: VkSnapshot, KeyScrLock: VkScroll, KeyPause: VkPause, KeyInsert: VkInsert, KeyDelete: VkDelete, KeyHome: VkHome, KeyEnd: VkEnd, KeyPgUp: VkPrior, KeyPgDn: VkNext, KeyLeft: VkLeft, KeyRight: VkRight, KeyUp: VkUp, KeyDown: VkDown, KeyNumLock: VkNumLock, KeyCapsLock: VkCapital, KeyLShift: VkLShift, KeyLCtrl: VkLControl, KeyLMeta: VkLWin, KeyLAlt: VkLMenu, KeyRShift: VkRShift, KeyRCtrl: VkRControl, KeyRMeta: VkRWin, KeyRAlt: VkRMenu, KeyMenu: VkApps, KeyConvert: VkConvert, KeyNonConvert: VkNonConvert, KeyBackspace: VkBack, KeyEnter: VkReturn, KeySpace: VkSpace, KeyTab: VkTab, Key1: Vk1, Key2: Vk2, Key3: Vk3, Key4: Vk4, Key5: Vk5, Key6: Vk6, Key7: Vk7, Key8: Vk8, Key9: Vk9, Key0: Vk0, KeyA: VkA, KeyB: VkB, KeyC: VkC, KeyD: VkD, KeyE: VkE, KeyF: VkF, KeyG: VkG, KeyH: VkH, KeyI: VkI, KeyJ: VkJ, KeyK: VkK, KeyL: VkL, KeyM: VkM, KeyN: VkN, KeyO: VkO, KeyP: VkP, KeyQ: VkQ, KeyR: VkR, KeyS: VkS, KeyT: VkT, KeyU: VkU, KeyV: VkV, KeyW: VkW, KeyX: VkX, KeyY: VkY, KeyZ: VkZ, KeyPadMul: VkMultiply, KeyPadAdd: VkAdd, KeyPadSub: VkSubtract, KeyPadDiv: VkDivide, KeyEqual: VkOemPlus, KeyComma: VkOemComma, KeyMinus: VkOemMinus, KeyPeriod: VkOemPeriod, KeySlash: VkOem2, KeyGrave: VkOem3, KeyLBrace: VkOem4, KeyBackslash: VkOem5, KeyRBrace: VkOem6, KeyQuote: VkOem7, KeyIsoBackSlash: VkOem102, KeyPad0: VkInsert, KeyPad1: VkEnd, KeyPad2: VkDown, KeyPad3: VkNext, KeyPad4: VkLeft, KeyPad5: VkClear, KeyPad6: VkRight, KeyPad7: VkHome, KeyPad8: VkUp, KeyPad9: VkPrior, KeyPadDec: VkDelete, }, Maps: []ModifierMap{ // Specials - without control { When: func(m Modifier) bool { return !m.IsCtrl() }, Map: map[Key]rune{ KeyTab: '\t', KeyEnter: '\r', KeyBackspace: '\b', KeyEsc: '\x1b', KeySpace: ' ', KeyPadEnter: '\r', }, }, // Specials - with control (but without shift) { When: func(m Modifier) bool { return m.IsCtrl() && !m.IsShift() }, Map: map[Key]rune{ KeyTab: '\t', KeyEnter: '\n', KeyBackspace: '\x7f', KeyEsc: '\x1b', KeySpace: ' ', KeyPadEnter: '\n', }, }, // Key pad operators { When: func(m Modifier) bool { return !m.IsAlt() }, Map: KeysPadOps, }, // Numeric keypad when num lock is engaged { When: func(m Modifier) bool { return m.IsNumLock() }, Map: KeysPadDigits, }, // Numbers - without shift { When: func(m Modifier) bool { return !m.IsShift() && !m.IsCtrl() }, Map: KeysDigits, }, // Numbers - with shift - this is locale sensitive usually { When: func(m Modifier) bool { return m.IsShift() && !m.IsCtrl() }, Map: map[Key]rune{ Key1: '!', Key2: '@', Key3: '#', Key4: '$', Key5: '%', Key6: '^', Key7: '&', Key8: '*', Key9: '(', Key0: ')', }, }, // Special shift-control cases { When: func(m Modifier) bool { return m.IsCtrl() && m.IsShift() }, Map: map[Key]rune{ Key2: 0, Key6: '\x1e', KeyMinus: '\x1f', }, }, // Letters - base (lower case) { When: func(m Modifier) bool { return !m.IsCtrl() && !m.IsCapitals() }, Map: KeysUsLower, }, // Letters - capitals (either caps lock or shift, but not both) { When: func(m Modifier) bool { return !m.IsCtrl() && m.IsCapitals() }, Map: KeysUsUpper, }, // OEM keys - base { When: func(m Modifier) bool { return !m.IsShift() && !m.IsCtrl() }, Map: map[Key]rune{ KeySemi: ';', KeyEqual: '=', KeyComma: ',', KeyMinus: '-', KeyPeriod: '.', KeySlash: '/', KeyGrave: '`', KeyLBrace: '[', KeyBackslash: '\\', KeyRBrace: ']', KeyQuote: '\'', KeyIsoBackSlash: '\\', }, }, // OEM keys - shift { When: func(m Modifier) bool { return m.IsShift() && !m.IsCtrl() }, Map: map[Key]rune{ KeySemi: ':', KeyEqual: '+', KeyComma: '<', KeyMinus: '_', KeyPeriod: '>', KeySlash: '?', KeyGrave: '~', KeyLBrace: '{', KeyBackslash: '|', KeyRBrace: '}', KeyQuote: '"', KeyIsoBackSlash: '|', }, }, // OEM keys - control (odd balls) { When: func(m Modifier) bool { return m.IsCtrl() && !m.IsShift() }, Map: map[Key]rune{ KeyLBrace: '\x1b', KeyBackslash: '\x1c', KeyRBrace: '\x1d', KeyIsoBackSlash: '\x1c', }, }, }, Modifiers: map[Key]Modifier{ KeyLShift: ModLShift, KeyRShift: ModRShift, KeyLCtrl: ModLCtrl, KeyRCtrl: ModRCtrl, KeyLAlt: ModLAlt, KeyRAlt: ModRAlt, KeyRMeta: ModRMeta, KeyLMeta: ModLMeta, KeyRHyper: ModRHyper, KeyLHyper: ModLHyper, }, Locking: map[Key]Modifier{ KeyNumLock: ModNumLock, KeyCapsLock: ModCapsLock, }, } var allLayouts = map[string]*Layout{ KeyboardANSI.Name: KeyboardANSI, } var layoutsLock sync.Mutex // RegisterLayout registers the given layout. func RegisterLayout(km *Layout) { layoutsLock.Lock() allLayouts[km.Name] = km layoutsLock.Unlock() } // GetLayout returns a keyboard layout for the given name. // The layout must have been previously registered with RegisterLayout. // (Builtin layouts do this as a consequence of importing the layout.) func GetLayout(name string) *Layout { layoutsLock.Lock() defer layoutsLock.Unlock() return allLayouts[name] } // Layouts returns a list of all known layout names. func Layouts() []string { layoutsLock.Lock() defer layoutsLock.Unlock() res := make([]string, 0, len(allLayouts)) for k := range allLayouts { res = append(res, k) } sort.Strings(res) return res } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layouts/000077500000000000000000000000001520475227200223675ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layouts/README.md000066400000000000000000000017121520475227200236470ustar00rootroot00000000000000# Layouts This directory contains packages which implement keyboard layouts for the emulator subsystem. The emulator subsystem is designed to support writing your own terminal emulators, including the simulation (mock) terminal emulator used to test TCell. If you're not implementing a terminal emulator, you don't need this. ## Designing a New Layout The layout subsystem is intended to be extensible, where layouts can inherit from other layouts, so that you only need to implement the differences. A good place to start is by looking at the us/ package, and the US International Layout in particular (which extends on the vanilla ANSI layout. The ANSI layout is always available.) ## Finding Layout Information A good source for information about keyboard layouts is [this site](https://kbdlayout.info). Pay particular attention to shift states for a given layout, and the dead keys. ## Limitations As of this writing, support for Ligature Keys is absent. golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layouts/all/000077500000000000000000000000001520475227200231375ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layouts/all/all.go000066400000000000000000000014651520475227200242440ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package all is used to import all keyboard layouts. It has no executable code. // This may substantially increase your binary size due to the extra data files. package all import _ "github.com/gdamore/tcell/v3/vt/layouts/us" golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layouts/us/000077500000000000000000000000001520475227200230165ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/layouts/us/us.go000066400000000000000000000127021520475227200237760ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package us import "github.com/gdamore/tcell/v3/vt" const ( UsInternationalLayout = "US International" // name of the US International ) const ( deadCarat = vt.DeadRune + '^' deadAcute = vt.DeadRune + '\'' deadQuote = vt.DeadRune + '"' deadGrave = vt.DeadRune + '`' deadTilde = vt.DeadRune + '~' ) var usInternationalUpper = map[vt.Key]rune{ vt.KeyA: 'Á', vt.KeyD: 'Ð', vt.KeyE: 'É', vt.KeyI: 'Í', vt.KeyL: 'Ø', vt.KeyN: 'Ñ', vt.KeyO: 'Ó', vt.KeyP: 'Ö', vt.KeyQ: 'Ä', vt.KeyT: 'Þ', vt.KeyU: 'Ú', vt.KeyW: 'Å', vt.KeyY: 'Ü', vt.KeyZ: 'Æ', vt.KeyComma: 'Ç', } var usInternationalLower = map[vt.Key]rune{ vt.KeyA: 'á', vt.KeyD: 'ð', vt.KeyE: 'é', vt.KeyI: 'í', vt.KeyL: 'ø', vt.KeyN: 'ñ', vt.KeyO: 'ó', vt.KeyP: 'ö', vt.KeyQ: 'ä', vt.KeyT: 'þ', vt.KeyU: 'ú', vt.KeyW: 'å', vt.KeyY: 'ü', vt.KeyZ: 'æ', vt.KeyComma: 'ç', } // UsInternational keyboard layout, includes Alt-Gr support (as Alt-Ctrl). // Keys that are not overridden here fallback to the standard US Layout. var UsInternational = &vt.Layout{ Name: UsInternationalLayout, Base: vt.KeyboardANSI, Locking: vt.KeyboardANSI.Locking, Modifiers: map[vt.Key]vt.Modifier{ vt.KeyLShift: vt.ModLShift, vt.KeyRShift: vt.ModRShift, vt.KeyLCtrl: vt.ModLCtrl, vt.KeyRCtrl: vt.ModRCtrl, vt.KeyRAlt: vt.ModRAlt | vt.ModRCtrl, // acts as AltGr vt.KeyLAlt: vt.ModLAlt, vt.KeyRMeta: vt.ModRMeta, vt.KeyLMeta: vt.ModLMeta, vt.KeyRHyper: vt.ModRHyper, vt.KeyLHyper: vt.ModLHyper, }, Maps: []vt.ModifierMap{ { When: func(m vt.Modifier) bool { return !m.IsShift() && !m.IsCtrl() && !m.IsAlt() }, Map: map[vt.Key]rune{ vt.KeyGrave: deadGrave, vt.KeyQuote: deadAcute, }, }, { When: func(m vt.Modifier) bool { return m.IsShift() && !m.IsCtrl() && !m.IsAlt() }, Map: map[vt.Key]rune{ vt.Key6: deadCarat, vt.KeyGrave: deadTilde, vt.KeyQuote: deadQuote, }, }, // Letters { When: func(m vt.Modifier) bool { return m.IsAltGr() && !m.IsCapitals() }, Map: usInternationalLower, }, { When: func(m vt.Modifier) bool { return m.IsAltGr() && m.IsCapitals() }, Map: usInternationalUpper, }, // Extended non-letter forms { When: func(m vt.Modifier) bool { return m.IsAltGr() && !m.IsShift() }, Map: map[vt.Key]rune{ vt.Key0: '’', vt.Key1: '¡', vt.Key2: '²', vt.Key3: '³', vt.Key4: '¤', vt.Key5: '€', vt.Key6: '¼', vt.Key7: '½', vt.Key8: '¾', vt.Key9: '‘', vt.KeyC: '©', vt.KeyM: 'µ', vt.KeyR: '®', vt.KeyS: 'ß', // technically also a letter, but no capital form vt.KeySemi: '¶', vt.KeyEqual: '×', vt.KeyMinus: '¥', vt.KeySlash: '¿', vt.KeyLBrace: '«', vt.KeyBackslash: '¬', vt.KeyRBrace: '»', vt.KeyQuote: '´', }, }, // Even more extended non-letter forms { When: func(m vt.Modifier) bool { return m.IsAltGr() && m.IsShift() }, Map: map[vt.Key]rune{ vt.Key1: '¹', vt.Key4: '£', vt.KeyC: '¢', vt.KeyS: '§', vt.KeySemi: '°', vt.KeyEqual: '÷', vt.KeyBackslash: '¦', vt.KeyQuote: '¨', }, }, }, DeadKeys: map[rune]vt.DeadKey{ deadCarat: { Next: map[rune]vt.DeadKey{ ' ': {U: '^'}, 'a': {U: 'â'}, 'A': {U: 'Â'}, 'e': {U: 'ê'}, 'E': {U: 'Ê'}, 'i': {U: 'î'}, 'I': {U: 'Î'}, 'o': {U: 'ô'}, 'O': {U: 'Ô'}, 'u': {U: 'û'}, 'U': {U: 'Û'}, }, }, deadAcute: { Next: map[rune]vt.DeadKey{ ' ': {U: '\''}, 'a': {U: 'á'}, 'A': {U: 'Á'}, 'c': {U: 'ç'}, 'C': {U: 'Ç'}, 'e': {U: 'é'}, 'E': {U: 'É'}, 'i': {U: 'í'}, 'I': {U: 'Í'}, 'o': {U: 'ó'}, 'O': {U: 'Ó'}, 'u': {U: 'ú'}, 'U': {U: 'Ú'}, 'y': {U: 'ý'}, 'Y': {U: 'Ý'}, }, }, deadQuote: { Next: map[rune]vt.DeadKey{ ' ': {U: '"'}, 'a': {U: 'ä'}, 'A': {U: 'Ä'}, 'e': {U: 'ë'}, 'E': {U: 'Ë'}, 'i': {U: 'ï'}, 'I': {U: 'Ï'}, 'o': {U: 'ö'}, 'O': {U: 'Ö'}, 'u': {U: 'ü'}, 'U': {U: 'Ü'}, 'y': {U: 'ÿ'}, 'Y': {U: 'Ÿ'}, }, }, deadGrave: { Next: map[rune]vt.DeadKey{ ' ': {U: '`'}, 'a': {U: 'à'}, 'A': {U: 'À'}, 'e': {U: 'è'}, 'E': {U: 'È'}, 'i': {U: 'ì'}, 'I': {U: 'Ì'}, 'o': {U: 'ò'}, 'O': {U: 'Ò'}, 'u': {U: 'ù'}, 'U': {U: 'Ù'}, }, }, deadTilde: { Next: map[rune]vt.DeadKey{ ' ': {U: '~'}, 'a': {U: 'ã'}, 'A': {U: 'Ã'}, 'n': {U: 'ñ'}, 'N': {U: 'Ñ'}, 'o': {U: 'õ'}, 'O': {U: 'Õ'}, }, }, }, } func init() { vt.RegisterLayout(UsInternational) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/mock.go000066400000000000000000000417671520475227200221660ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import ( "slices" "sync" "time" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/tty" ) // mockTerm implements MockTerm. type mockTerm struct { mb MockBackend em Emulator ks *KeyboardState } // Stop the terminal. func (mt *mockTerm) Stop() error { return mt.em.Stop() } // Start the terminal. func (mt *mockTerm) Start() error { return mt.em.Start() } // Drain all output from the terminal, ensuring // any queued commands are processed. func (mt *mockTerm) Drain() error { return mt.em.Drain() } // Read data from the terminal. This is called by a terminal // application (e.g. via tcell Tty.) Read data will include // key strokes, mouse events, and responses to terminal queries. func (mt *mockTerm) Read(data []byte) (int, error) { return mt.em.Read(data) } // Write data to the terminal, typically either commands or data // that should be displayed on the virtual screen. func (mt *mockTerm) Write(b []byte) (n int, err error) { return mt.em.Write(b) } // WindowSize obtains the dimensions of the window. func (mt *mockTerm) WindowSize() (tty.WindowSize, error) { sz := mt.mb.GetSize() // No pixel sizes for now return tty.WindowSize{Width: int(sz.X), Height: int(sz.Y)}, nil } // NotifyResize registers a channel to be signaled when a resize has occurred. // In real terminal emulators this would be posted (non-blocking) by a signal handler. func (mt *mockTerm) NotifyResize(resizeq chan<- bool) { if rs, ok := mt.mb.(Resizer); ok { rs.NotifyResize(resizeq) } } // Close closes the terminal, after which it should no longer be used. Stop is implied. func (mt *mockTerm) Close() error { return mt.Stop() } // Pos returns the cursor position. func (mt *mockTerm) Pos() Coord { return mt.mb.GetPosition() } // GetCell returns the contents of the cell at the given coordinates, or a zero value // if the coordinates are out of range. func (mt *mockTerm) GetCell(pos Coord) Cell { return mt.mb.GetCell(pos) } // Bells counts the number of times the bell has rung. func (mt *mockTerm) Bells() int { return mt.mb.Bells() } // KeyEvent is used to inject a key event. Call this to inject // a synthetic, fully specified key event. Most uses should just use // the KeyPress, KeyRelease, or even simpler KeyTap APIs. func (mt *mockTerm) KeyEvent(ev KeyEvent) { mt.em.KeyEvent(ev) if ev.Key == KeyEsc { // Inject a delay to simulate human typing. // Necessary to disambiguate Escape from other sequences. time.Sleep(time.Millisecond * 150) } } // KeyPress implements MockTerm.KeyPress. func (mt *mockTerm) KeyPress(k Key) { if event := mt.ks.Pressed(k); event != nil { mt.KeyEvent(*event) } } // KeyRelease implements MockTerm.KeyRelease. func (mt *mockTerm) KeyRelease(k Key) { if event := mt.ks.Released(k); event != nil { mt.KeyEvent(*event) } } // KeyTap implements MockTerm.KeyTap. func (mt *mockTerm) KeyTap(keys ...Key) { for _, k := range keys { mt.KeyPress(k) } for _, k := range slices.Backward(keys) { mt.KeyRelease(k) } } // SetRepeat sets the repeat interval for the keyboard. // Set the interval to zero to disable repeat. func (mt *mockTerm) SetRepeat(delay, interval time.Duration) { mt.ks.SetRepeat(delay, interval) } // MouseEvent implements MockTerm.MouseEvent. func (mt *mockTerm) MouseEvent(ev MouseEvent) { mt.em.MouseEvent(ev) } // FocusEvent implements MockTerm.FocusEvent. func (mt *mockTerm) FocusEvent(focused bool) { mt.em.FocusEvent(focused) } // GetTitle returns the current window title. func (mt *mockTerm) GetTitle() string { return mt.mb.GetTitle() } // SetSize is used to change the terminal size. func (mt *mockTerm) SetSize(size Coord) { mt.mb.SetSize(size) mt.em.ResizeEvent(size) } // Backend returns the backend for testing. func (mt *mockTerm) Backend() MockBackend { return mt.mb } // SendRaw is used to inject raw bytes to the read stream of the app. // Use this for fuzz testing. func (mt *mockTerm) SendRaw(data []byte) { mt.em.SendRaw(data) } // SetLayout sets the keyboard layout. func (mt *mockTerm) SetLayout(km *Layout) { mt.ks.SetLayout(km) } // MockTerm is a mock terminal (emulator). It can be used to // test the emulator itself, or to test applications (or tcell) that // uses the terminal. It also implements the Tty interface used // by tcell itself. type MockTerm interface { tty.Tty // Pos reports the current cursor position. Pos() Coord // GetCell returns the cell at the given coordinates. // The coordinates must be valid. GetCell(Coord) Cell // Bells returns the number of times the bell has been rung. Bells() int // Inject a keyboard event - this is a full event, and bypasses // the layout and keyboard state processor. KeyEvent(KeyEvent) // Inject a key press. KeyPress(Key) // Inject a key release. KeyRelease(Key) // SetRepeat configures keyboard repeating. Repeat keystrokes // will be assumed after the key has been held for at least delay, // with new keys added each interval. SetRepeat(delay, interval time.Duration) // Inject one or more key press and releases. // The keys are pressed in the order, and released in reverse order. // Thus modifiers should be listed first. This should not be used // to simulate typing a sequence (e.g. a word), but if you wanted to // test say N-Key rollover you could do that here. KeyTap(...Key) // Inject a mouse event. MouseEvent(MouseEvent) // Inject a focus event. FocusEvent(bool) // GetTitle obtains the current window title. GetTitle() string // SetSize is used to resize the terminal. SetSize(Coord) // SendRaw is used to send raw data to the application. // This is mostly intended to facilitate fuzz testing the application. SendRaw([]byte) // Backend returns the backend (used for testing). Backend() MockBackend // SetLayout sets the keyboard layout to use. // If not specified, a US standard ANSI keyboard will be assumed. SetLayout(*Layout) } type noMockBlit struct { MockBackend Blit struct{} // prevents use as Blitter } // NewMockTerm gives a mock terminal emulator. func NewMockTerm(opts ...MockOpt) MockTerm { mt := &mockTerm{} mt.mb = NewMockBackend(opts...) var be MockBackend = mt.mb emOpts := []EmulatorOpt{} for _, o := range opts { switch o.(type) { case MockOptNoBlit: be = &noMockBlit{be, struct{}{}} case MockOpt8BitControls: emOpts = append(emOpts, EmulatorOpt8BitControls{}) } } mt.em = NewEmulator(be, emOpts...) mt.em.SetId("TCellMock", "1.0") mt.ks = &KeyboardState{} return mt } // MockBackend provides additional mock-specific capabilities on top of Backend. // This is meant to facilitate test cases type MockBackend interface { Backend // GetCell returns the cell at the given position, or an empty cell if the // position is out of the bounds of the window. GetCell(Coord) Cell // Bells counts the number of bells rung. Bells() int // GetTitle gets the current window title. GetTitle() string // SetSize is used to resize the window. // Newly added cells are empty, and content in old cells that out of range is lost. SetSize(Coord) // GetCursor is used to obtain the current cursor style. GetCursor() CursorStyle // SetClipboard sets the clipboard contents (copy buffer). SetClipboard([]byte) // GetClipboard returns the clipboard (copy buffer). GetClipboard() []byte // IsAdvancedKeyboard returns true, as we always support the full keyboard protocol. IsAdvancedKeyboard() bool } // mockBackend is a mock of a backend device for use with the emulator. // It implements the following interfaces: // vt.Backend, vt.Beeper, vt.Colorer, vt.Titler, vt.Resizer, vt.Blitter type mockBackend struct { cells []Cell // Content of cells size Coord pos Coord colors int style Style defaultStyle Style notifyQ chan<- bool resized bool newSize Coord modes map[PrivateMode]ModeStatus bells int errs int title string clipboard []byte cursor CursorStyle lock sync.Mutex } func (mb *mockBackend) GetSize() Coord { mb.lock.Lock() defer mb.lock.Unlock() mb.checkSize() return mb.size } func (mb *mockBackend) Beep() { mb.lock.Lock() mb.bells++ mb.lock.Unlock() } func (mb *mockBackend) SetMouse(MouseReporting) {} func (mb *mockBackend) GetPrivateMode(pm PrivateMode) ModeStatus { mb.lock.Lock() defer mb.lock.Unlock() // note default (zero) value is ModeNA return mb.modes[pm] } func (mb *mockBackend) SetPrivateMode(pm PrivateMode, status ModeStatus) error { mb.lock.Lock() defer mb.lock.Unlock() if old := mb.modes[pm]; old == ModeOn || old == ModeOff { if status == ModeOn || status == ModeOff { mb.modes[pm] = status } else { mb.errs++ } } else { mb.errs++ } return nil } func (mb *mockBackend) Put(pos Coord, cell Cell) { // grapheme string, width int, style Style) { mb.lock.Lock() defer mb.lock.Unlock() mb.checkSize() if index := mb.index(pos); index >= 0 { mb.cells[index] = cell // writing to a cell right after a wide // character clears that wide character (but leaves style/attributes) if cell.W > 0 && pos.X > 0 && mb.cells[index-1].W > 1 { mb.cells[index-1].C = "" mb.cells[index-1].W = 0 } // wide characters delete the next cell if cell.W == 2 && pos.X < mb.size.X-1 { mb.cells[index+1].C = "" mb.cells[index+1].W = 0 mb.cells[index+1].S = cell.S } } else { mb.errs++ } } func (mb *mockBackend) isPositionValid(pos Coord) bool { mb.checkSize() return pos.X < mb.size.X && pos.Y < mb.size.Y && pos.X >= 0 && pos.Y >= 0 } // index calculates the index in the cells array. If the coordinates are invalid, // -1 will be returned. func (mb *mockBackend) index(pos Coord) int { mb.checkSize() if !mb.isPositionValid(pos) { return -1 } return int(pos.X) + int(pos.Y)*int(mb.size.X) } func (mb *mockBackend) GetCell(pos Coord) Cell { mb.lock.Lock() defer mb.lock.Unlock() if index := mb.index(pos); index >= 0 { return mb.cells[index] } return Cell{S: BaseStyle} } func (mb *mockBackend) Bells() int { mb.lock.Lock() defer mb.lock.Unlock() return mb.bells } func (mb *mockBackend) GetPosition() Coord { mb.lock.Lock() defer mb.lock.Unlock() mb.checkSize() return mb.pos } func (mb *mockBackend) SetPosition(pos Coord) { mb.lock.Lock() defer mb.lock.Unlock() mb.checkSize() pos.X = min(mb.size.X-1, max(0, pos.X)) pos.Y = min(mb.size.Y-1, max(0, pos.Y)) mb.pos = pos } func (mb *mockBackend) Colors() int { return mb.colors } func (mb *mockBackend) SetStyle(style Style) { mb.lock.Lock() defer mb.lock.Unlock() mb.style = style } // SetWindowTitle implements the Titler interface. func (mb *mockBackend) SetWindowTitle(title string) { mb.lock.Lock() defer mb.lock.Unlock() mb.title = title } // GetTitle allows test code to observe what was set with SetWindowTitle. func (mb *mockBackend) GetTitle() string { mb.lock.Lock() defer mb.lock.Unlock() return mb.title } // NotifyResize registers a channel to be written to (non-blocking) if the // backend changes size. func (mb *mockBackend) NotifyResize(rq chan<- bool) { mb.lock.Lock() defer mb.lock.Unlock() mb.notifyQ = rq } // checkSize performs a possible terminal resize. Cells that are // added are treated as empty, while cells that are removed are just lost. // (Note that at least one other emulator erases content on a resize. There is no // standard for what to do here.) This is done inline when calculating the index. // The caller is expected to hold mb.lock. func (mb *mockBackend) checkSize() { if !mb.resized { return } size := mb.newSize old := mb.cells ox := int(mb.size.X) oy := int(mb.size.Y) nx := int(size.X) ny := int(size.Y) cells := make([]Cell, int(size.Y)*int(size.X)) for i := range cells { cells[i].S = BaseStyle } for y := range min(ny, oy) { for x := range min(nx, ox) { cells[y*nx+x] = old[y*ox+x] } } mb.cells = cells mb.size = size mb.pos.X = min(mb.pos.X, size.X-1) mb.pos.Y = min(mb.pos.Y, size.Y-1) mb.resized = false } func (mb *mockBackend) RaiseResize() { mb.lock.Lock() defer mb.lock.Unlock() if rq := mb.notifyQ; rq != nil { select { case rq <- true: default: } } } // SetSize is used to change the size of the virtual terminal. func (mb *mockBackend) SetSize(size Coord) { mb.lock.Lock() mb.resized = true mb.newSize = size mb.lock.Unlock() } // Reset the terminal to startup defaults. func (mb *mockBackend) Reset() { mb.lock.Lock() defer mb.lock.Unlock() mb.style = mb.defaultStyle mb.title = "" mb.errs = 0 mb.bells = 0 mb.pos = Coord{X: 0, Y: 0} mb.modes[PmShowCursor] = ModeOn mb.modes[PmBlinkCursor] = ModeOn mb.modes[PmGraphemeClusters] = ModeOff } func (mb *mockBackend) Blit(src, dst, dim Coord) { mb.lock.Lock() defer mb.lock.Unlock() mb.checkSize() // clip to visible source if dim.X+src.X > mb.size.X { dim.X = mb.size.X - src.X } if dim.Y+src.Y > mb.size.Y { dim.Y = mb.size.Y - src.Y } // and clip to final destination if dim.X+dst.X > mb.size.X { dim.X = mb.size.X - dst.X } if dim.Y+dst.Y > mb.size.Y { dim.Y = mb.size.Y - dst.Y } // gap represents decrement when shifting to the next row -- // skipping over the irrelevant cells. (The increment in the // index when going from last cell of row to first cell of next row, // or vice versa.) gap := int(mb.size.X - dim.X) // the following logic is carefully constructed to avoid expensive // operations in the loops (only addition or subtraction) if mb.index(src) > mb.index(dst) { // source appears later, so we can forward copy si := mb.index(src) di := mb.index(dst) for range dim.Y { for range dim.X { mb.cells[di] = mb.cells[si] di++ si++ } // advance to next row si += gap di += gap } } else { // source appears earlier, so we have to reverse copy src.Y += dim.Y - 1 dst.Y += dim.Y - 1 src.X += dim.X - 1 dst.X += dim.X - 1 si := mb.index(src) di := mb.index(dst) for range dim.Y { for range dim.X { mb.cells[di] = mb.cells[si] si-- di-- } si -= gap di -= gap } } } // Buffering is not supported by the mockBackend, and there is little point in it. func (mb *mockBackend) Buffering(bool) {} // SetCursor is used to set how the cursor is displayed. func (mb *mockBackend) SetCursor(cs CursorStyle) { mb.lock.Lock() defer mb.lock.Unlock() mb.cursor = cs } // GetCursor returns the current cursor style. func (mb *mockBackend) GetCursor() CursorStyle { mb.lock.Lock() defer mb.lock.Unlock() return mb.cursor } // SetClipboard sets the current clipboard contents. func (mb *mockBackend) SetClipboard(data []byte) { mb.lock.Lock() defer mb.lock.Unlock() mb.clipboard = data } // GetClipboard gets the current clipboard contents. func (mb *mockBackend) GetClipboard() []byte { mb.lock.Lock() defer mb.lock.Unlock() return mb.clipboard } // IsAdvancedKeyboard returns true - we always implement // the raw keyboard protocol. func (mb *mockBackend) IsAdvancedKeyboard() bool { return true } // MockOpt is an interface by which options can change the behavior of the mocked terminal. // This is intended to permit easier testing. type MockOpt interface{ SetMockOpt(mb *mockBackend) } // MockOptSize changes the default terminal size, which is normally 80x24. type MockOptSize Coord func (o MockOptSize) SetMockOpt(mb *mockBackend) { mb.size = Coord(o) } // MockOptColors changes the number of colors the terminal supports. type MockOptColors int func (o MockOptColors) SetMockOpt(mb *mockBackend) { mb.colors = int(o) } // MockOptNoBlit suppresses the blitter interface. type MockOptNoBlit struct{} func (MockOptNoBlit) SetMockOpt(mb *mockBackend) {} // MockOpt8BitControls enables raw 8-bit and UTF-8 encoded C1 controls in the // emulator. The default is to accept only 7-bit ESC-prefixed controls. type MockOpt8BitControls struct{} func (MockOpt8BitControls) SetMockOpt(mb *mockBackend) {} // NewMockBackend returns a MockBackend modified by the given options. // The default is a fully featured 256-color backend with initial size 80x24. func NewMockBackend(options ...MockOpt) MockBackend { mb := &mockBackend{ size: Coord{X: 80, Y: 24}, colors: 256, style: BaseStyle, defaultStyle: BaseStyle.WithFg(color.Silver).WithBg(color.Black), cursor: BlinkingBlock, } for _, opt := range options { opt.SetMockOpt(mb) } if mb.colors > 0 { mb.style = mb.defaultStyle } mb.cells = make([]Cell, int(mb.size.X)*int(mb.size.Y)) for i := range mb.cells { mb.cells[i].S = BaseStyle } mb.modes = make(map[PrivateMode]ModeStatus) mb.modes[PmShowCursor] = ModeOn mb.modes[PmBlinkCursor] = ModeOn mb.modes[PmGraphemeClusters] = ModeOff mb.modes[PmSyncOutput] = ModeOff return mb } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/mode.go000066400000000000000000000123411520475227200221430ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package vt provides common definitions for VT derived terminals and applications. // This includes the venerable VT100, XTerm, and newer emulators such as Kitty and // the Windows Terminal. package vt import "fmt" // PrivateMode describes a DEC Private Mode. type PrivateMode int const ( PmAppCursor PrivateMode = 1 // Application cursor keys. PmVT52 PrivateMode = 2 // Clear to enable VT52 compatibility (not supported). PmColumns PrivateMode = 3 // Set to enable 132 columns, reset for 80 columns. PmScrolling PrivateMode = 4 // Smooth scrolling (jump by default). PmScreen PrivateMode = 5 // Set to reverse dark and light on screen. PmOrigin PrivateMode = 6 // Coordinates are relative to margins. PmAutoMargin PrivateMode = 7 // Automatically wrap at margin. PmAutoRepeat PrivateMode = 8 // Enable automatic key repeat. PmMouseX10 PrivateMode = 9 // Legacy (X10) mouse reporting. PmBlinkCursor PrivateMode = 12 // Blinking (on) or steady (off) cursor. PmPrintFF PrivateMode = 18 // Print form feed after printing screen. PmPrintExtent PrivateMode = 19 // Print full screen (on) or scrolling region (off). PmShowCursor PrivateMode = 25 // Show the cursor (default on). PmCharSet PrivateMode = 42 // Enable national (on) or multinational (off) character sets. PmLeftRightMargin PrivateMode = 69 // Enable left and right margins PmMouseButton PrivateMode = 1000 // Report mouse button events. PmMouseDrag PrivateMode = 1002 // Report mouse motion events when button depressed, requires PmMouseButton. PmMouseMotion PrivateMode = 1003 // Report mouse motion events, requires PmMouseButton. PmFocusReports PrivateMode = 1004 // Send focus gained or lost reports. PmMouseSgr PrivateMode = 1006 // Use SGR sequences for mouse reports. PmMouseSgrPixel PrivateMode = 1016 // Use SGR sequences for mouse reports, using pixel-level coordinates. PmAltScreen PrivateMode = 1049 // 47 and 1047 are alternates, but we use 1049 PmBracketedPaste PrivateMode = 2004 // Bracket pasted text with bracketed paste escape sequences. PmSyncOutput PrivateMode = 2026 // Buffer output when enabled, updating screen when reset. PmGraphemeClusters PrivateMode = 2027 // Support for grapheme cluster handling. PmResizeReports PrivateMode = 2048 // Send in-band resize reports. PmWin32Input PrivateMode = 9001 // Use Win32-Input-Mode for keyboard reports ) // Enable returns the string used to enable this private mode. func (pm PrivateMode) Enable() string { return fmt.Sprintf("\x1b[?%dh", pm) } // Disable returns the string used to disable this private mode. func (pm PrivateMode) Disable() string { return fmt.Sprintf("\x1b[?%dl", pm) } // Query returns the string used to query the state of this private mode. func (pm PrivateMode) Query() string { return fmt.Sprintf("\x1b[?%d$p", pm) } // Reply returns a string representing a query reply for the given mode and status. func (pm PrivateMode) Reply(status ModeStatus) string { return fmt.Sprintf("\x1b[?%d;%d$y", pm, status) } // ModeStatus represents the status of the mode. type ModeStatus int // AnsiMode are modes standardized in ECMA-48. // They use CSI-h and CSI-l (no question mark). type AnsiMode int // Enable returns the string used to enable this ANSI mode. func (pm AnsiMode) Enable() string { return fmt.Sprintf("\x1b[%dh", pm) } // Disable returns the string used to disable this ANSI mode. func (pm AnsiMode) Disable() string { return fmt.Sprintf("\x1b[%dl", pm) } // Query returns the string used to query the state of this ANSI mode. func (pm AnsiMode) Query() string { return fmt.Sprintf("\x1b[%d$p", pm) } // Reply returns a string representing a query reply for the given mode and status. func (pm AnsiMode) Reply(status ModeStatus) string { return fmt.Sprintf("\x1b[%d;%d$y", pm, status) } const ( AmKeyboardAction AnsiMode = 2 // Lock the keyboard. AmInsertReplace AnsiMode = 4 // Insert or replace characters when a new character is added. AmSendReceive AnsiMode = 12 // XON or XOFF. AmNewLineMode AnsiMode = 20 // If true, LF emits CR as well, and Return sends both CR and LF. ) const ( ModeNA ModeStatus = 0 // Mode is not supported (or unknown) ModeOn ModeStatus = 1 // Mode is on (e.g. via CSI-h) ModeOff ModeStatus = 2 // Mode is off (e.g. via CSI-l) ModeOnLocked ModeStatus = 3 // Mode is hardwired on ModeOffLocked ModeStatus = 4 // Mode is hardwired off ) // Changeable indicates that the mode may be changed. func (ms ModeStatus) Changeable() bool { return ms == ModeOn || ms == ModeOff } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/mode_test.go000066400000000000000000000026351520475227200232070ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import "testing" func TestPmStrings(t *testing.T) { pm := PmAutoMargin if s := pm.Enable(); s != "\x1b[?7h" { t.Errorf("enable string wrong: %s", s) } if s := pm.Disable(); s != "\x1b[?7l" { t.Errorf("disable string wrong: %s", s) } if s := pm.Query(); s != "\x1b[?7$p" { t.Errorf("query string wrong: %s", s) } if s := pm.Reply(ModeNA); s != "\x1b[?7;0$y" { t.Errorf("reply(NA) string wrong: %s", s) } if s := pm.Reply(ModeOn); s != "\x1b[?7;1$y" { t.Errorf("reply(On) string wrong: %s", s) } if s := pm.Reply(ModeOff); s != "\x1b[?7;2$y" { t.Errorf("reply(Off) string wrong: %s", s) } if s := pm.Reply(ModeOnLocked); s != "\x1b[?7;3$y" { t.Errorf("reply(OnLocked) string wrong: %s", s) } if s := pm.Reply(ModeOffLocked); s != "\x1b[?7;4$y" { t.Errorf("reply(OffLocked) string wrong: %s", s) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/000077500000000000000000000000001520475227200220315ustar00rootroot00000000000000golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/cursor_test.go000066400000000000000000000133771520475227200247470ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "testing" "github.com/gdamore/tcell/v3/vt" ) func TestCursorStyle(t *testing.T) { VerifyF(t, vt.SteadyBlock.IsVisible(), "steady block should be visible") VerifyF(t, vt.SteadyBlock.Show() == vt.SteadyBlock, "steady block show should be non-op") VerifyF(t, !vt.SteadyBlock.Hide().IsVisible(), "steady block hide should be hidden") VerifyF(t, vt.SteadyBlock.Hide().Show() == vt.SteadyBlock, "steady block hide and show should be inverse") VerifyF(t, !vt.SteadyBlock.IsBlinking(), "steady block should be steady") VerifyF(t, vt.SteadyBlock.Blink().IsBlinking(), "steady block blink should work") VerifyF(t, vt.SteadyBlock.Blink() == vt.BlinkingBlock, "steady block blink should be blinking block") VerifyF(t, vt.SteadyBlock.Steady() == vt.SteadyBlock, "steady block should be steady") VerifyF(t, vt.SteadyBlock.Blink().Steady() == vt.SteadyBlock, "steady block blink and steady should be inverse") VerifyF(t, vt.SteadyBar.IsVisible(), "steady bar should be visible") VerifyF(t, vt.SteadyBar.Show() == vt.SteadyBar, "steady bar show should be non-op") VerifyF(t, !vt.SteadyBar.Hide().IsVisible(), "steady bar hide should be hidden") VerifyF(t, vt.SteadyBar.Hide().Show() == vt.SteadyBar, "steady bar hide and show should be inverse") VerifyF(t, !vt.SteadyBar.IsBlinking(), "steady bar should be steady") VerifyF(t, vt.SteadyBar.Blink().IsBlinking(), "steady bar blink should work") VerifyF(t, vt.SteadyBar.Blink() == vt.BlinkingBar, "steady bar blink should be blinking bar") VerifyF(t, vt.SteadyBar.Steady() == vt.SteadyBar, "steady bar should be steady") VerifyF(t, vt.SteadyBar.Blink().Steady() == vt.SteadyBar, "steady bar blink and steady should be inverse") VerifyF(t, vt.SteadyUnderline.IsVisible(), "steady underline should be visible") VerifyF(t, vt.SteadyUnderline.Show() == vt.SteadyUnderline, "steady underline show should be non-op") VerifyF(t, !vt.SteadyUnderline.Hide().IsVisible(), "steady underline hide should be hidden") VerifyF(t, vt.SteadyUnderline.Hide().Show() == vt.SteadyUnderline, "steady underline hide and show should be inverse") VerifyF(t, !vt.SteadyUnderline.IsBlinking(), "steady underline should be steady") VerifyF(t, vt.SteadyUnderline.Blink().IsBlinking(), "steady underline blink should work") VerifyF(t, vt.SteadyUnderline.Blink() == vt.BlinkingUnderline, "steady underline blink should be blinking underline") VerifyF(t, vt.SteadyUnderline.Steady() == vt.SteadyUnderline, "steady underline should be steady") VerifyF(t, vt.SteadyUnderline.Blink().Steady() == vt.SteadyUnderline, "steady underline blink and steady should be inverse") VerifyF(t, vt.BlinkingBlock.IsVisible(), "blinking block should be visible") VerifyF(t, vt.BlinkingBlock.Show() == vt.BlinkingBlock, "blinking block show should be non-op") VerifyF(t, !vt.BlinkingBlock.Hide().IsVisible(), "blinking block hide should be hidden") VerifyF(t, vt.BlinkingBlock.Hide().Show() == vt.BlinkingBlock, "blinking block hide and show should be inverse") VerifyF(t, vt.BlinkingBlock.IsBlinking(), "blinking block should be blinking") VerifyF(t, vt.BlinkingBlock.Blink().IsBlinking(), "blinking block blink should work") VerifyF(t, vt.BlinkingBlock.Steady() == vt.SteadyBlock, "blinking block steady should be steady block") VerifyF(t, vt.BlinkingBlock.Blink() == vt.BlinkingBlock, "blinking block should be blinking block") VerifyF(t, vt.BlinkingBlock.Steady().Blink() == vt.BlinkingBlock, "blinking block steady and blink should be inverse") VerifyF(t, vt.BlinkingBar.IsVisible(), "blinking bar should be visible") VerifyF(t, vt.BlinkingBar.Show() == vt.BlinkingBar, "blinking bar show should be non-op") VerifyF(t, !vt.BlinkingBar.Hide().IsVisible(), "blinking bar hide should be hidden") VerifyF(t, vt.BlinkingBar.Hide().Show() == vt.BlinkingBar, "blinking bar hide and show should be inverse") VerifyF(t, vt.BlinkingBar.IsBlinking(), "blinking bar should be blinking") VerifyF(t, vt.BlinkingBar.Blink().IsBlinking(), "blinking bar blink should work") VerifyF(t, vt.BlinkingBar.Steady() == vt.SteadyBar, "blinking bar steady should be steady bar") VerifyF(t, vt.BlinkingBar.Blink() == vt.BlinkingBar, "blinking bar should be blinking bar") VerifyF(t, vt.BlinkingBar.Steady().Blink() == vt.BlinkingBar, "blinking bar steady and blink should be inverse") VerifyF(t, vt.BlinkingUnderline.IsVisible(), "blinking underline should be visible") VerifyF(t, vt.BlinkingUnderline.Show() == vt.BlinkingUnderline, "blinking underline show should be non-op") VerifyF(t, !vt.BlinkingUnderline.Hide().IsVisible(), "blinking underline hide should be hidden") VerifyF(t, vt.BlinkingUnderline.Hide().Show() == vt.BlinkingUnderline, "blinking underline hide and show should be inverse") VerifyF(t, vt.BlinkingUnderline.IsBlinking(), "blinking underline should be blinking") VerifyF(t, vt.BlinkingUnderline.Blink().IsBlinking(), "blinking underline blink should work") VerifyF(t, vt.BlinkingUnderline.Steady() == vt.SteadyUnderline, "blinking underline steady should be steady underline") VerifyF(t, vt.BlinkingUnderline.Blink() == vt.BlinkingUnderline, "blinking underline should be blinking underline") VerifyF(t, vt.BlinkingUnderline.Steady().Blink() == vt.BlinkingUnderline, "blinking underline steady and blink should be inverse") } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/emulate_test.go000066400000000000000000001403611520475227200250600ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "encoding/base64" "fmt" "testing" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/vt" ) // This file implements various tests of the emulator. Much of these tests // are "borrowed" (ported from) the tests from Ghostty - https://ghostty.org/docs/vt // Note that Ghostty's tests assume that STTY modes to expand LF to CF LF are in // effect (or ANSI mode 20.) We don't assume that, and add the CR explicitly. // TestDECSTBMv1 tests full screen scroll region func TestDECSTBMv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "ABC\r\n") WriteF(t, term, "DEF\r\n") WriteF(t, term, "GHI\r\n") WriteF(t, term, "\033[r") // scroll region top/bottom WriteF(t, term, "\033[T") // scroll down one // |c_______| // |ABC_____| // |DEF_____| // |GHI_____| CheckPos(t, term, 0, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 1, 1, "B") CheckContent(t, term, 2, 1, "C") CheckContent(t, term, 0, 2, "D") CheckContent(t, term, 1, 2, "E") CheckContent(t, term, 2, 2, "F") CheckContent(t, term, 0, 3, "G") CheckContent(t, term, 1, 3, "H") CheckContent(t, term, 2, 3, "I") } // TestDECSTBMv2 top only func TestDECSTBMv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "ABC\r\n") WriteF(t, term, "DEF\r\n") WriteF(t, term, "GHI\r\n") WriteF(t, term, "\033[2;2r") // scroll region top/bottom WriteF(t, term, "\033[T") // scroll down one // |________| // |ABC_____| // |DEF_____| // |GHI_____| CheckPos(t, term, 0, 3) // did not move CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 1, 1, "B") CheckContent(t, term, 2, 1, "C") CheckContent(t, term, 0, 2, "D") CheckContent(t, term, 1, 2, "E") CheckContent(t, term, 2, 2, "F") CheckContent(t, term, 0, 3, "G") CheckContent(t, term, 1, 3, "H") CheckContent(t, term, 2, 3, "I") } // TestDECSTBMv3 top and bottom func TestDECSTBMv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "ABC\r\n") WriteF(t, term, "DEF\r\n") WriteF(t, term, "GHI\r\n") WriteF(t, term, "\033[1;2r") // scroll region top/bottom WriteF(t, term, "\033[T") // scroll down one // |________| // |ABC_____| // |GHI_____| // |________| CheckPos(t, term, 0, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 1, 1, "B") CheckContent(t, term, 2, 1, "C") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "H") CheckContent(t, term, 2, 2, "I") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestDECSTBMv4 top == bottom func TestDECSTBMv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "ABC\r\n") WriteF(t, term, "DEF\r\n") WriteF(t, term, "GHI\r\n") WriteF(t, term, "\033[2;2r") WriteF(t, term, "\033[T") // |________| // |ABC_____| // |DEF_____| // |GHI_____| CheckPos(t, term, 0, 3) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 1, 1, "B") CheckContent(t, term, 2, 1, "C") CheckContent(t, term, 0, 2, "D") CheckContent(t, term, 1, 2, "E") CheckContent(t, term, 2, 2, "F") CheckContent(t, term, 0, 3, "G") CheckContent(t, term, 1, 3, "H") CheckContent(t, term, 2, 3, "I") } // TestDECSLRMv1 tests full screen right and left margins. func TestDECSLRMv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[H") WriteF(t, term, "\033[J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[s") WriteF(t, term, "\033[X") CheckPos(t, term, 0, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 0, 1, "D") CheckContent(t, term, 1, 1, "E") CheckContent(t, term, 2, 1, "F") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "H") CheckContent(t, term, 2, 2, "I") } // TODO: DECSLRMv3 left and right this makes use of insert line // TODO: DECSLRMv4 left and right equal // TODO: add tests for actual left and right scrolling! // TestRIv1 top of screen, no scroll func TestRIv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "A\r\n") WriteF(t, term, "B\r\n") WriteF(t, term, "C\r\n") WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033M") WriteF(t, term, "X") // |Xc______| // |A_______| // |B_______| // |C_______| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "B") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "C") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestRIv2 not top of screen, no scroll func TestRIv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "A\r\n") WriteF(t, term, "B\r\n") WriteF(t, term, "C\r\n") WriteF(t, term, "\033[2;1H") WriteF(t, term, "\033M") WriteF(t, term, "X") // |Xc______| // |B_______| // |C_______| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "B") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "C") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestRIv3 scroll region func TestRIv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "A\r\n") WriteF(t, term, "B\r\n") WriteF(t, term, "C\r\n") WriteF(t, term, "\033[2;3r") WriteF(t, term, "\033[2;1H") WriteF(t, term, "\033M") // |A_______| // |c_______| // |B_______| // |________| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "B") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestRIv4 outside scroll region - goes to top, does not scroll func TestRIv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "A\r\n") WriteF(t, term, "B\r\n") WriteF(t, term, "C\r\n") WriteF(t, term, "\033[2;3r") WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033M") // |A_______| // |B_______| // |C_______| // |________| CheckPos(t, term, 0, 0) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "B") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "C") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TODO: RIv5 - left right scroll regions (when we implement left/right regions) // TODO: RIv6 - outside left/right scroll regions (when we implement left/right regions) // TestINDv1 no scroll region, top of screen func TestINDv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "A") WriteF(t, term, "\033D") WriteF(t, term, "X") // |A_______| // |_Xc_____| // |________| // |________| CheckPos(t, term, 2, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "X") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestINDv2 no scroll region, bottom of screen func TestINDv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") // clear screen WriteF(t, term, "\033[4;1H") WriteF(t, term, "A") WriteF(t, term, "\033D") WriteF(t, term, "X") // |________| // |________| // |A_______| // |_Xc_____| CheckPos(t, term, 2, 3) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "X") CheckContent(t, term, 2, 3, "") } // TestINDv3 inside scroll region func TestINDv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") WriteF(t, term, "\033[1;3r") WriteF(t, term, "A") WriteF(t, term, "\033D") WriteF(t, term, "X") // |A_______| // |_Xc_____| // |________| // |________| CheckPos(t, term, 2, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "X") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestINDv4 bottom of scroll region func TestINDv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") WriteF(t, term, "\033[1;3r") WriteF(t, term, "\033[4;1H") WriteF(t, term, "B") WriteF(t, term, "\033[3;1H") WriteF(t, term, "A") WriteF(t, term, "\033D") WriteF(t, term, "X") // |________| // |A_______| // |_Xc_____| // |B_______| CheckPos(t, term, 2, 2) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "X") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "B") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestINDv5 bottom of screen with scroll region func TestINDv5(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") WriteF(t, term, "\033[1;3r") WriteF(t, term, "\033[3;1H") WriteF(t, term, "A") WriteF(t, term, "\033[4;1H") WriteF(t, term, "\033D") WriteF(t, term, "X") // |________| // |________| // |A_______| // |________| // |Xc______| CheckPos(t, term, 1, 4) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") CheckContent(t, term, 0, 4, "X") CheckContent(t, term, 1, 4, "") CheckContent(t, term, 2, 4, "") } // TestINDv6 tests IND outside of left and right scroll region. func TestINDv6(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[1;3r") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\033[3;3H") WriteF(t, term, "A") WriteF(t, term, "\033[3;1H") WriteF(t, term, "\033D") WriteF(t, term, "X") // |________| // |________| // |XcA_____| CheckPos(t, term, 1, 2) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "X") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "A") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestINDv7 tests IND inside of left and right scroll region. func TestINDv7(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") // move to top-left WriteF(t, term, "\033[0J") WriteF(t, term, "111111\n\r") WriteF(t, term, "222222\n\r") WriteF(t, term, "333333\n\r") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[1;3s") WriteF(t, term, "\033[1;3r") WriteF(t, term, "\033[3;1H") WriteF(t, term, "\033D") // |222111__| // |333222__| // |c__333__| CheckPos(t, term, 0, 2) CheckContent(t, term, 0, 0, "2") CheckContent(t, term, 1, 0, "2") CheckContent(t, term, 2, 0, "2") CheckContent(t, term, 3, 0, "1") CheckContent(t, term, 4, 0, "1") CheckContent(t, term, 5, 0, "1") CheckContent(t, term, 0, 1, "3") CheckContent(t, term, 1, 1, "3") CheckContent(t, term, 2, 1, "3") CheckContent(t, term, 3, 1, "2") CheckContent(t, term, 4, 1, "2") CheckContent(t, term, 5, 1, "2") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 3, 2, "3") CheckContent(t, term, 4, 2, "3") CheckContent(t, term, 5, 2, "3") } // TestCUDv1 - cursor down func TestCUDv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "A") WriteF(t, term, "\033[2B") WriteF(t, term, "X") // |A_______| // |________| // |_Xc_____| // |________| CheckPos(t, term, 2, 2) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "X") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCUDv2 - cursor down above bottom margin func TestCUDv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\n\n\n\n") WriteF(t, term, "\033[1;3r") WriteF(t, term, "A") WriteF(t, term, "\033[5B") WriteF(t, term, "X") // |A_______| // |________| // |_Xc_____| // |________| CheckPos(t, term, 2, 2) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "X") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCUDv3 - cursor down below bottom margin func TestCUDv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[1;3r") WriteF(t, term, "A") WriteF(t, term, "\033[4;1H") WriteF(t, term, "\033[5B") WriteF(t, term, "X") // |A_______| // |________| // |________| // |________| // |Xc______| CheckPos(t, term, 1, 4) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") CheckContent(t, term, 0, 4, "X") CheckContent(t, term, 1, 4, "") CheckContent(t, term, 2, 4, "") } // TestCUUv1 tests cursor up. func TestCUUv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[3;H") WriteF(t, term, "A") WriteF(t, term, "\033[2A") WriteF(t, term, "X") // |_Xc_____| // |________| // |A_______| CheckPos(t, term, 2, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "X") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") } // TestCUUv2 tests cursor up below the top margin. func TestCUUv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[2;4r") WriteF(t, term, "\033[3;1H") WriteF(t, term, "A") WriteF(t, term, "\033[5A") WriteF(t, term, "X") // |________| // |_Xc_____| // |A_______| // |________| CheckPos(t, term, 2, 1) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "X") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCUUv3 tests cursor up above the top margin. func TestCUUv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[3;5r") WriteF(t, term, "\033[3;1H") WriteF(t, term, "A") WriteF(t, term, "\033[2;1H") WriteF(t, term, "\033[5A") WriteF(t, term, "X") // |Xc______| // |________| // |A_______| // |________| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCNLv1 - cursor next line func TestCNLv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "A") WriteF(t, term, "\033[2E") WriteF(t, term, "X") // |A_______| // |________| // |Xc_____| // |________| CheckPos(t, term, 1, 2) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "X") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCNLv2 - cursor next line above bottom margin func TestCNLv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\n\n\n\n") WriteF(t, term, "\033[1;3r") WriteF(t, term, "A") WriteF(t, term, "\033[5E") WriteF(t, term, "X") // |A_______| // |________| // |Xc______| // |________| CheckPos(t, term, 1, 2) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "X") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCNLv3 - cursor next line bottom margin func TestCNLv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[1;3r") WriteF(t, term, "A") WriteF(t, term, "\033[4;3H") WriteF(t, term, "\033[5E") WriteF(t, term, "X") // |A_______| // |________| // |________| // |________| // |Xc______| CheckPos(t, term, 1, 4) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") CheckContent(t, term, 0, 4, "X") CheckContent(t, term, 1, 4, "") CheckContent(t, term, 2, 4, "") } // TestCPLv1 tests cursor previous line. func TestCPLv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[3;H") WriteF(t, term, "A") WriteF(t, term, "\033[2F") WriteF(t, term, "X") // |Xc______| // |________| // |A_______| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") } // TestCPLv2 tests cursor previous line below the top margin. func TestCPLv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[2;4r") WriteF(t, term, "\033[3;1H") WriteF(t, term, "A") WriteF(t, term, "\033[5F") WriteF(t, term, "X") // |________| // |Xc______| // |A_______| // |________| CheckPos(t, term, 1, 1) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "X") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestCPLv3 tests cursor previous line above the top margin. func TestCPLv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H") WriteF(t, term, "\033[0J") WriteF(t, term, "\033[3;5r") WriteF(t, term, "\033[3;1H") WriteF(t, term, "A") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[5F") WriteF(t, term, "X") // |Xc______| // |________| // |A_______| // |________| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 0, 2, "A") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") } // TestILv1 tests inserting a line. func TestILv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[L") // |ABC_____| // |c_______| // |DEF_____| // |GHI_____| // |________| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 0, 2, "D") CheckContent(t, term, 1, 2, "E") CheckContent(t, term, 2, 2, "F") CheckContent(t, term, 3, 2, "") CheckContent(t, term, 0, 3, "G") CheckContent(t, term, 1, 3, "H") CheckContent(t, term, 2, 3, "I") CheckContent(t, term, 3, 3, "") } // TestILv2 tests insert line outside of the scroll region. func TestILv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "\033[3;4r") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[L") // |ABC_____| // |DEF_____| // |GHI_____| // |________| // |________| CheckPos(t, term, 1, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 0, 1, "D") CheckContent(t, term, 1, 1, "E") CheckContent(t, term, 2, 1, "F") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "H") CheckContent(t, term, 2, 2, "I") CheckContent(t, term, 3, 2, "") } // TestILv3 tests insert line with top and bottom scroll regions. func TestILv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "123\n\r") WriteF(t, term, "\033[1;3r") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[L") // |ABC_____| // |c_______| // |DEF_____| // |123_____| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 0, 2, "D") CheckContent(t, term, 1, 2, "E") CheckContent(t, term, 2, 2, "F") CheckContent(t, term, 3, 2, "") CheckContent(t, term, 0, 3, "1") CheckContent(t, term, 1, 3, "2") CheckContent(t, term, 2, 3, "3") CheckContent(t, term, 3, 3, "") } // TestILv4 tests insert line with left and right margins. func TestILv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC123\n\r") WriteF(t, term, "DEF456\n\r") WriteF(t, term, "GHI789\n\r") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[2;4s") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[L") // |ABC123__| // |Dc__56__| // |GEF489__| // |_HI7____| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "1") CheckContent(t, term, 4, 0, "2") CheckContent(t, term, 5, 0, "3") CheckContent(t, term, 0, 1, "D") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 4, 1, "5") CheckContent(t, term, 5, 1, "6") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "E") CheckContent(t, term, 2, 2, "F") CheckContent(t, term, 3, 2, "4") CheckContent(t, term, 4, 2, "8") CheckContent(t, term, 5, 2, "9") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "H") CheckContent(t, term, 2, 3, "I") CheckContent(t, term, 3, 3, "7") CheckContent(t, term, 4, 3, "") CheckContent(t, term, 5, 3, "") } // TestDLv1 tests a simple delete line. func TestDLv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[M") // |ABC_____| // |GHI_____| // |________| // |________| // |________| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 0, 1, "G") CheckContent(t, term, 1, 1, "H") CheckContent(t, term, 2, 1, "I") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 2, 3, "") } // TestDLv2 delete line outside of the scroll region. func TestDLv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "\033[3;4r") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[M") // |ABC_____| // |DEF_____| // |GHI_____| // |________| // |________| CheckPos(t, term, 1, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 0, 1, "D") CheckContent(t, term, 1, 1, "E") CheckContent(t, term, 2, 1, "F") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "H") CheckContent(t, term, 2, 2, "I") CheckContent(t, term, 3, 2, "") } // TestDLv3 delete line with top and bottom scroll regions. func TestDLv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC\n\r") WriteF(t, term, "DEF\n\r") WriteF(t, term, "GHI\n\r") WriteF(t, term, "123\n\r") WriteF(t, term, "\033[1;3r") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[M") // |ABC_____| // |GHI_____| // |________| // |123_____| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 0, 1, "G") CheckContent(t, term, 1, 1, "H") CheckContent(t, term, 2, 1, "I") CheckContent(t, term, 3, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 3, 2, "") CheckContent(t, term, 0, 3, "1") CheckContent(t, term, 1, 3, "2") CheckContent(t, term, 2, 3, "3") CheckContent(t, term, 3, 3, "") } // TestDLv4 delete line with left and right margins. func TestDLv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC123\n\r") WriteF(t, term, "DEF456\n\r") WriteF(t, term, "GHI789\n\r") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[2;4s") WriteF(t, term, "\033[2;2H") WriteF(t, term, "\033[M") // |ABC123__| // |DHI756__| // |G___89__| // |________| CheckPos(t, term, 0, 1) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "1") CheckContent(t, term, 4, 0, "2") CheckContent(t, term, 5, 0, "3") CheckContent(t, term, 0, 1, "D") CheckContent(t, term, 1, 1, "H") CheckContent(t, term, 2, 1, "I") CheckContent(t, term, 3, 1, "7") CheckContent(t, term, 4, 1, "5") CheckContent(t, term, 5, 1, "6") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "") CheckContent(t, term, 2, 2, "") CheckContent(t, term, 3, 2, "") CheckContent(t, term, 4, 2, "8") CheckContent(t, term, 5, 2, "9") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 1, 3, "") CheckContent(t, term, 2, 3, "") CheckContent(t, term, 3, 3, "") CheckContent(t, term, 4, 3, "") CheckContent(t, term, 5, 3, "") } // TestDCv1 tests a simple delete character. func TestDCv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC123") WriteF(t, term, "\033[3G") WriteF(t, term, "\033[2P") // |AB23____| // |________| // |________| // |________| CheckPos(t, term, 2, 0) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "2") CheckContent(t, term, 3, 0, "3") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") } // TestDCv2 tests delete character SGR state. func TestDCv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC123") WriteF(t, term, "\033[3G") WriteF(t, term, "\033[41m") WriteF(t, term, "\x1b[1m") WriteF(t, term, "\033[2P") // |AB23____| // |________| // |________| // |________| CheckPos(t, term, 2, 0) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "2") CheckContent(t, term, 3, 0, "3") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") CheckAttrs(t, term, 6, 0, vt.Plain) CheckAttrs(t, term, 7, 0, vt.Plain) CheckColors(t, term, 6, 0, color.Silver, color.Maroon) CheckColors(t, term, 7, 0, color.Silver, color.Maroon) } // TestDCv3 tests delete outside the left/right scroll region. func TestDCv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC123") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\x1b[2G") WriteF(t, term, "\033[P") // |ABC123__| // |________| // |________| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "1") CheckContent(t, term, 4, 0, "2") CheckContent(t, term, 5, 0, "3") CheckContent(t, term, 6, 0, "") } // TestDCv4 tests delete inside the left/right scroll region. func TestDCv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC123") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\x1b[4G") WriteF(t, term, "\033[P") // |ABC2_3__| // |________| // |________| // |________| CheckPos(t, term, 3, 0) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "2") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "3") CheckContent(t, term, 6, 0, "") } // TestDCv5 tests delete character splitting a wide character. func TestDCv5(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "A橋123") WriteF(t, term, "\x1b[3G") WriteF(t, term, "\033[P") // |A_123___| // |________| // |________| // |________| CheckPos(t, term, 2, 0) CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "1") CheckContent(t, term, 3, 0, "2") CheckContent(t, term, 4, 0, "3") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") } // TestICHv1 tests insert character without a scroll region, fitting on screen. func TestICHv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "ABC") WriteF(t, term, "\033[1G") WriteF(t, term, "\x1b[2@") WriteF(t, term, "X") // |XcABC___| // |________| // |________| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "A") CheckContent(t, term, 3, 0, "B") CheckContent(t, term, 4, 0, "C") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") } // TestICHv2 tests insert character SGR state. func TestICHv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "ABC") WriteF(t, term, "\033[1G") WriteF(t, term, "\033[41m\033[7m") WriteF(t, term, "\x1b[2@") WriteF(t, term, "X") // |XcABC___| // |________| // |________| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "A") CheckContent(t, term, 3, 0, "B") CheckContent(t, term, 4, 0, "C") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") CheckAttrs(t, term, 0, 0, vt.Reverse) CheckAttrs(t, term, 1, 0, vt.Reverse) CheckAttrs(t, term, 2, 0, vt.Plain) CheckAttrs(t, term, 3, 0, vt.Plain) CheckAttrs(t, term, 4, 0, vt.Plain) CheckColors(t, term, 0, 0, color.Silver, color.Maroon) CheckColors(t, term, 1, 0, color.Silver, color.Maroon) CheckColors(t, term, 2, 0, color.Silver, color.Black) CheckColors(t, term, 3, 0, color.Silver, color.Black) CheckColors(t, term, 4, 0, color.Silver, color.Black) } // TestICHv3 tests insert character shifting off screen func TestICHv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "\033[8G") WriteF(t, term, "\033[2D") WriteF(t, term, "ABC") WriteF(t, term, "\033[2D") WriteF(t, term, "\033[2@") WriteF(t, term, "X") // |_____XcA| // |________| // |________| // |________| CheckPos(t, term, 6, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "X") CheckContent(t, term, 6, 0, "") CheckContent(t, term, 7, 0, "A") } // TestICHv4 tests insert character inside left and right scroll regions. func TestICHv4(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\033[3G") WriteF(t, term, "ABC") WriteF(t, term, "\033[3G") WriteF(t, term, "\033[2@") WriteF(t, term, "X") // |__XcA___| // |________| // |________| // |________| CheckPos(t, term, 3, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "X") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 4, 0, "A") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") CheckContent(t, term, 7, 0, "") } // TestICHv5 tests insert character outside left and right scroll regions. func TestICHv5(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\033[3G") WriteF(t, term, "ABC") WriteF(t, term, "\033[1G") WriteF(t, term, "\033[2@") WriteF(t, term, "X") // |XcABC___| // |________| // |________| // |________| CheckPos(t, term, 1, 0) CheckContent(t, term, 0, 0, "X") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "A") CheckContent(t, term, 3, 0, "B") CheckContent(t, term, 4, 0, "C") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") CheckContent(t, term, 7, 0, "") } // TestICHv5a tests insert character outside left and right scroll regions resets auto-wrap. func TestICHv5a(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[1;1H\033[0J") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\033[3G") WriteF(t, term, "ABC") WriteF(t, term, "\033[8G") WriteF(t, term, "X") WriteF(t, term, "\033[2@") WriteF(t, term, "Y") WriteF(t, term, "Z") // |__ABC__Y| // |Zc______| // |________| // |________| CheckPos(t, term, 1, 1) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "A") CheckContent(t, term, 3, 0, "B") CheckContent(t, term, 4, 0, "C") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") CheckContent(t, term, 7, 0, "Y") CheckContent(t, term, 0, 1, "Z") } // TestICHv6 tests insert character splitting a wide character on the right of the screen. func TestICHv6(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[8G") WriteF(t, term, "\033[1D") WriteF(t, term, "橋") WriteF(t, term, "\033[2D") WriteF(t, term, "\033[@") WriteF(t, term, "X") // |_____Xc_| CheckPos(t, term, 6, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "X") CheckContent(t, term, 6, 0, "") CheckContent(t, term, 7, 0, "") } // TestICHv6a tests insert character splitting a wide character at the right margin. func TestICHv6a(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[4G") WriteF(t, term, "橋") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\033[3G") WriteF(t, term, "\033[@") WriteF(t, term, "X") // |__Xc____| CheckPos(t, term, 3, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "X") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") } // TestICHv6b tests insert character splitting a wide character at the left margin. func TestICHv6b(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[2G") WriteF(t, term, "橋") WriteF(t, term, "\033[?69h") WriteF(t, term, "\033[3;5s") WriteF(t, term, "\033[3G") WriteF(t, term, "\033[@") WriteF(t, term, "X") // |__Xc____| CheckPos(t, term, 3, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 1, 0, "") CheckContent(t, term, 2, 0, "X") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 6, 0, "") } // TestDECSCUSRv1 tests setting the cursor. func TestDECSCUSRv1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[ q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock, "") WriteF(t, term, "\033[0 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock, "") WriteF(t, term, "\033[1 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock, "") WriteF(t, term, "\033[2 q") VerifyF(t, term.Backend().GetCursor() == vt.SteadyBlock, "") WriteF(t, term, "\033[3 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingUnderline, "") WriteF(t, term, "\033[4 q") VerifyF(t, term.Backend().GetCursor() == vt.SteadyUnderline, "") WriteF(t, term, "\033[5 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBar, "") WriteF(t, term, "\033[6 q") VerifyF(t, term.Backend().GetCursor() == vt.SteadyBar, "") } // TestDECSCUSRv2 tests setting the cursor while hidden. func TestDECSCUSRv2(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[?25l") WriteF(t, term, "\033[ q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock.Hide(), "") WriteF(t, term, "\033[0 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock.Hide(), "") WriteF(t, term, "\033[1 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock.Hide(), "") WriteF(t, term, "\033[2 q") VerifyF(t, term.Backend().GetCursor() == vt.SteadyBlock.Hide(), "") WriteF(t, term, "\033[3 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingUnderline.Hide(), "") WriteF(t, term, "\033[4 q") VerifyF(t, term.Backend().GetCursor() == vt.SteadyUnderline.Hide(), "") WriteF(t, term, "\033[5 q") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBar.Hide(), "") WriteF(t, term, "\033[6 q") VerifyF(t, term.Backend().GetCursor() == vt.SteadyBar.Hide(), "") } // TestDECSCUSRv3 tests setting the blink private mode. func TestDECSCUSRv3(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033[0 q") WriteF(t, term, "\033[?12l") VerifyF(t, term.Backend().GetCursor() == vt.SteadyBlock, "") WriteF(t, term, "\033[?12h") VerifyF(t, term.Backend().GetCursor() == vt.BlinkingBlock, "") } // TestOSC8v1 tests hyperlinks. func TestOSC8v1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\033]8;bob=junk:id=123:junk;https://example.com\033\\") WriteF(t, term, "Hello") WriteF(t, term, "\033]8;;\033\\") WriteF(t, term, " World!") term.Drain() for col := range Col(10) { cell := term.GetCell(Coord{X: col, Y: 0}) if col < 5 { if u, i := cell.S.Url(); u != "https://example.com" || i != "123" { t.Errorf("wrong data at column %d: url %q id %q", col, u, i) } } else { if u, i := cell.S.Url(); u != "" || i != "" { t.Errorf("wrong data at column %d: url %q id %q", col, u, i) } } } } // TestOSC52v1 tests pasting and retrieving the clipboard. func TestOSC52v1(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5}) defer MustClose(t, term) MustStart(t, term) str := "Hello, World." enc := make([]byte, base64.StdEncoding.EncodedLen(len(str))) base64.StdEncoding.Encode(enc, []byte(str)) WriteF(t, term, "\033]52;c;%s\033\\", enc) VerifyF(t, string(term.Backend().GetClipboard()) == str, "clipboard contents did not match (%s != %s)", string(term.Backend().GetClipboard()), str) WriteF(t, term, "\033]52;c;?\033\\") response := ReadF(t, term) expect := fmt.Sprintf("\033]52;c;%s\033\\", enc) VerifyF(t, response == expect, "retrieved value did not match (%q != %q)", response, expect) // any invalid base64 clears the clipboard WriteF(t, term, "\033]52;c;junk1\033\\") VerifyF(t, string(term.Backend().GetClipboard()) == "", "") // empty clears the clipboard term.Backend().SetClipboard([]byte("something")) WriteF(t, term, "\033]52;c;\033\\") VerifyF(t, string(term.Backend().GetClipboard()) == "", "") // but a bogus sequence without two fields does nothing term.Backend().SetClipboard([]byte("something")) WriteF(t, term, "\033]52;x\033\\") VerifyF(t, string(term.Backend().GetClipboard()) == "something", "") } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/event_test.go000066400000000000000000000123411520475227200245410ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "testing" "github.com/gdamore/tcell/v3/vt" ) func TestMouse1006(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[?1006$p") result := ReadF(t, term) want := "\x1b[?1006;2$y" VerifyF(t, result == want, "wrong mode report: %q != %q", result, want) // turn on mouse reports WriteF(t, term, "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h") // simple mouse click term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button1, Down: true, Motion: false, Mod: vt.ModNone, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button1, Down: false, Motion: false, Mod: vt.ModNone, }) result = ReadF(t, term) want = "\x1b[<0;3;4M\x1b[<0;3;4m" VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) // modified drag (10x10 square) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button2, Down: true, Motion: false, Mod: vt.ModLShift, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 12, Y: 13}, Button: vt.Button2, Down: false, Motion: true, Mod: vt.ModRShift, }) result = ReadF(t, term) want = "\x1b[<6;3;4M\x1b[<38;13;14m" VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) } func TestMouseX10(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[?9$p") result := ReadF(t, term) want := "\x1b[?9;2$y" VerifyF(t, result == want, "wrong mode report: %q != %q", result, want) // turn on mouse reports WriteF(t, term, "\x1b[?9h") // simple mouse click term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button1, Down: true, Motion: false, Mod: vt.ModNone, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button1, Down: false, Motion: false, Mod: vt.ModNone, }) result = ReadF(t, term) want = "\x1b[M #$" VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) // button 3 mouse click term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button3, Down: true, Motion: false, Mod: vt.ModNone, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button3, Down: false, Motion: false, Mod: vt.ModNone, }) result = ReadF(t, term) want = "\x1b[M!#$" VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) // modified drag (10x10 square) - we only see the start term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button2, Down: true, Motion: false, Mod: vt.ModLShift, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 12, Y: 13}, Button: vt.Button2, Down: false, Motion: true, Mod: vt.ModLShift, }) result = ReadF(t, term) want = "\x1b[M\"#$" VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) } // TestMouse1000 tests basic mouse mode (VT200, no tracking) func TestMouse1000(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[?1000$p") result := ReadF(t, term) want := "\x1b[?1000;2$y" VerifyF(t, result == want, "wrong mode report: %q != %q", result, want) // turn on mouse reports WriteF(t, term, "\x1b[?1000h") // simple mouse click term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button1, Down: true, Motion: false, Mod: vt.ModNone, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button1, Down: false, Motion: false, Mod: vt.ModNone, }) result = ReadF(t, term) want = "\x1b[M #$\x1b[M##$" VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) // modified drag (10x10 square) - we only see the start term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 2, Y: 3}, Button: vt.Button2, Down: true, Motion: false, Mod: vt.ModLShift, }) term.MouseEvent(vt.MouseEvent{ // should be suppressed Position: Coord{X: 11, Y: 12}, Button: vt.NoButton, Down: false, Motion: true, Mod: vt.ModLShift, }) term.MouseEvent(vt.MouseEvent{ Position: Coord{X: 12, Y: 13}, Button: vt.Button2, Down: false, Motion: true, Mod: vt.ModRShift, }) result = ReadF(t, term) want = "\x1b[M&#$\x1b[M'-." VerifyF(t, result == want, "wrong mouse event: %q != %q", result, want) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/key_test.go000066400000000000000000000370441520475227200242170ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "testing" "time" "github.com/gdamore/tcell/v3/vt" ) // TestScanCodes tests that the keys all have correct scan codes. func TestScanCodes(t *testing.T) { cases := []struct { name string key vt.Key code vt.ScanCode }{ {"A", vt.KeyA, 0x1e}, {"B", vt.KeyB, 0x30}, {"C", vt.KeyC, 0x2e}, {"D", vt.KeyD, 0x20}, {"E", vt.KeyE, 0x12}, {"F", vt.KeyF, 0x21}, {"G", vt.KeyG, 0x22}, {"H", vt.KeyH, 0x23}, {"I", vt.KeyI, 0x17}, {"J", vt.KeyJ, 0x24}, {"K", vt.KeyK, 0x25}, {"L", vt.KeyL, 0x26}, {"M", vt.KeyM, 0x32}, {"N", vt.KeyN, 0x31}, {"O", vt.KeyO, 0x18}, {"P", vt.KeyP, 0x19}, {"Q", vt.KeyQ, 0x10}, {"R", vt.KeyR, 0x13}, {"S", vt.KeyS, 0x1f}, {"T", vt.KeyT, 0x14}, {"U", vt.KeyU, 0x16}, {"V", vt.KeyV, 0x2f}, {"W", vt.KeyW, 0x11}, {"X", vt.KeyX, 0x2d}, {"Y", vt.KeyY, 0x15}, {"Z", vt.KeyZ, 0x2c}, {"1", vt.Key1, 0x02}, {"2", vt.Key2, 0x03}, {"3", vt.Key3, 0x04}, {"4", vt.Key4, 0x05}, {"5", vt.Key5, 0x06}, {"6", vt.Key6, 0x07}, {"7", vt.Key7, 0x08}, {"8", vt.Key8, 0x09}, {"9", vt.Key9, 0x0a}, {"0", vt.Key0, 0x0b}, {"enter", vt.KeyEnter, 0x1c}, {"escape", vt.KeyEsc, 0x01}, {"bs", vt.KeyBackspace, 0x0e}, {"tab", vt.KeyTab, 0x0f}, {"space", vt.KeySpace, 0x39}, {"minus", vt.KeyMinus, 0x0c}, {"equals", vt.KeyEqual, 0x0d}, {"lbrace", vt.KeyLBrace, 0x1a}, {"rbrace", vt.KeyRBrace, 0x1b}, {"backslash", vt.KeyBackslash, 0x2b}, {"semicolon", vt.KeySemi, 0x27}, {"quote", vt.KeyQuote, 0x28}, {"grave", vt.KeyGrave, 0x29}, {"comma", vt.KeyComma, 0x33}, {"period", vt.KeyPeriod, 0x34}, {"slash", vt.KeySlash, 0x35}, {"capsLock", vt.KeyCapsLock, 0x3a}, {"f1", vt.KeyF1, 0x3b}, {"f2", vt.KeyF2, 0x3c}, {"f3", vt.KeyF3, 0x3d}, {"f4", vt.KeyF4, 0x3e}, {"f5", vt.KeyF5, 0x3f}, {"f6", vt.KeyF6, 0x40}, {"f7", vt.KeyF7, 0x41}, {"f8", vt.KeyF8, 0x42}, {"f9", vt.KeyF9, 0x43}, {"f10", vt.KeyF10, 0x44}, {"f11", vt.KeyF11, 0x57}, {"f12", vt.KeyF12, 0x58}, {"prtScr", vt.KeyPrtScr, 0x54}, {"scrLock", vt.KeyScrLock, 0x46}, {"pause", vt.KeyPause, 0xe046}, {"ins", vt.KeyInsert, 0xe052}, {"home", vt.KeyHome, 0xe047}, {"pgUp", vt.KeyPgUp, 0xe049}, {"del", vt.KeyDelete, 0xe053}, {"end", vt.KeyEnd, 0xe04f}, {"pgDn", vt.KeyPgDn, 0xe051}, {"right", vt.KeyRight, 0xe04d}, {"left", vt.KeyLeft, 0xe04b}, {"down", vt.KeyDown, 0xe050}, {"up", vt.KeyUp, 0xe048}, {"numLock", vt.KeyNumLock, 0x45}, {"div", vt.KeyPadDiv, 0xe035}, {"mul", vt.KeyPadMul, 0x37}, {"sub", vt.KeyPadSub, 0x4a}, {"add", vt.KeyPadAdd, 0x4e}, {"enter", vt.KeyPadEnter, 0xe01c}, {"pad1", vt.KeyPad1, 0x4f}, {"pad2", vt.KeyPad2, 0x50}, {"pad3", vt.KeyPad3, 0x51}, {"pad4", vt.KeyPad4, 0x4b}, {"pad5", vt.KeyPad5, 0x4c}, {"pad6", vt.KeyPad6, 0x4d}, {"pad7", vt.KeyPad7, 0x47}, {"pad8", vt.KeyPad8, 0x48}, {"pad9", vt.KeyPad9, 0x49}, {"pad0", vt.KeyPad0, 0x52}, {"padDecimal", vt.KeyPadDec, 0x53}, {"isoBackSlash", vt.KeyIsoBackSlash, 0x56}, {"padEqual", vt.KeyPadEqual, 0x59}, {"f13", vt.KeyF13, 0x64}, {"f14", vt.KeyF14, 0x65}, {"f15", vt.KeyF15, 0x66}, {"f16", vt.KeyF16, 0x67}, {"f17", vt.KeyF17, 0x68}, {"f18", vt.KeyF18, 0x69}, {"f19", vt.KeyF19, 0x6a}, {"f20", vt.KeyF20, 0x6b}, {"f21", vt.KeyF21, 0x6c}, {"f22", vt.KeyF22, 0x6d}, {"f23", vt.KeyF23, 0x6e}, {"f24", vt.KeyF24, 0x76}, {"padComma", vt.KeyPadComma, 0x7e}, {"padIsoSlash", vt.KeyIsoSlash, 0x73}, {"hiragana", vt.KeyHiragana, 0x70}, {"yen", vt.KeyYen, 0x7d}, {"convert", vt.KeyConvert, 0x79}, {"noConvert", vt.KeyNonConvert, 0x7b}, {"leftCtrl", vt.KeyLCtrl, 0x1d}, {"leftShift", vt.KeyLShift, 0x2a}, {"leftAlt", vt.KeyLAlt, 0x38}, {"leftMeta", vt.KeyLMeta, 0xe05b}, {"rightCtrl", vt.KeyRCtrl, 0xe01d}, {"rightShift", vt.KeyRShift, 0x36}, {"rightAlt", vt.KeyRAlt, 0xe038}, {"rightMeta", vt.KeyRMeta, 0xe05c}, {"menu", vt.KeyMenu, 0xe05d}, } for i := range cases { t.Run(cases[i].name, func(t *testing.T) { VerifyF(t, cases[i].key.ScanCode() == cases[i].code, "Scan code %x did not match %x", cases[i].key.ScanCode(), cases[i].code) }) } // Let's also make sure that we have no duplicate scan codes. seen := make(map[vt.ScanCode]vt.Key) for i := range cases { sc := cases[i].key.ScanCode() if other, ok := seen[sc]; ok { t.Errorf("Duplicate scan %x code for key %x and %x", sc, cases[i].key, other) } seen[sc] = cases[i].key } } // TestBaseKeys tests that all the "base" keys have correct values and correct shifted values. // These are the values used for the Kitty protocol. func TestBaseKeys(t *testing.T) { cases := []struct { name string key vt.Key base vt.BaseKey shifted rune }{ {"A", vt.KeyA, 'a', 'A'}, {"B", vt.KeyB, 'b', 'B'}, {"C", vt.KeyC, 'c', 'C'}, {"D", vt.KeyD, 'd', 'D'}, {"E", vt.KeyE, 'e', 'E'}, {"F", vt.KeyF, 'f', 'F'}, {"G", vt.KeyG, 'g', 'G'}, {"H", vt.KeyH, 'h', 'H'}, {"I", vt.KeyI, 'i', 'I'}, {"J", vt.KeyJ, 'j', 'J'}, {"K", vt.KeyK, 'k', 'K'}, {"L", vt.KeyL, 'l', 'L'}, {"M", vt.KeyM, 'm', 'M'}, {"N", vt.KeyN, 'n', 'N'}, {"O", vt.KeyO, 'o', 'O'}, {"P", vt.KeyP, 'p', 'P'}, {"Q", vt.KeyQ, 'q', 'Q'}, {"R", vt.KeyR, 'r', 'R'}, {"S", vt.KeyS, 's', 'S'}, {"T", vt.KeyT, 't', 'T'}, {"U", vt.KeyU, 'u', 'U'}, {"V", vt.KeyV, 'v', 'V'}, {"W", vt.KeyW, 'w', 'W'}, {"X", vt.KeyX, 'x', 'X'}, {"Y", vt.KeyY, 'y', 'Y'}, {"Z", vt.KeyZ, 'z', 'Z'}, {"1", vt.Key1, '1', '!'}, {"2", vt.Key2, '2', '@'}, {"3", vt.Key3, '3', '#'}, {"4", vt.Key4, '4', '$'}, {"5", vt.Key5, '5', '%'}, {"6", vt.Key6, '6', '^'}, {"7", vt.Key7, '7', '&'}, {"8", vt.Key8, '8', '*'}, {"9", vt.Key9, '9', '('}, {"0", vt.Key0, '0', ')'}, {"minus", vt.KeyMinus, '-', '_'}, {"equal", vt.KeyEqual, '=', '+'}, {"lbrace", vt.KeyLBrace, '[', '{'}, {"backslash", vt.KeyBackslash, '\\', '|'}, {"rbrace", vt.KeyRBrace, ']', '}'}, {"semi", vt.KeySemi, ';', ':'}, {"backslash-iso", vt.KeyIsoBackSlash, '\\', '|'}, {"comma", vt.KeyComma, ',', '<'}, {"period", vt.KeyPeriod, '.', '>'}, {"slash", vt.KeySlash, '/', '?'}, {"yen", vt.KeyYen, '¥', '|'}, } for i := range cases { t.Run(cases[i].name, func(t *testing.T) { base := cases[i].key.KittyBase() shift := base.Shifted() VerifyF(t, base == cases[i].base, "Base code %x did not match %x", base, cases[i].base) VerifyF(t, shift == cases[i].shifted, "Base code %q %x did not match %q %x", string(shift), shift, string(cases[i].shifted), cases[i].shifted) }) } } // TestKeyRepeat tests simple key repeat func TestKeyRepeat(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Keep the initial repeat quick, but make later repeats very slow so the test // does not depend on sub-100ms timer precision. term.SetRepeat(time.Millisecond*50, time.Second) term.KeyPress(vt.KeyX) time.Sleep(100 * time.Millisecond) term.KeyPress(vt.KeyX) term.KeyRelease(vt.KeyX) term.KeyRelease(vt.KeyCapsLock) CheckRead(t, term, "xx") } // TestKeyRepeatDisabled tests simple key repeat func TestKeyRepeatDisabled(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Keep the initial repeat quick, but make later repeats very slow so the test // only needs to distinguish repeated from non-repeated input. term.SetRepeat(time.Millisecond*50, time.Second) WriteF(t, term, "\x1b[?8l") term.KeyPress(vt.KeyX) time.Sleep(100 * time.Millisecond) term.KeyPress(vt.KeyX) term.KeyRelease(vt.KeyX) CheckRead(t, term, "x") } // TestKeyRepeatCapsLock ensures that caps lock does not repeat func TestKeyRepeatCapsLock(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Use a large repeat interval so the test is not sitting on a timing boundary. // We only need to prove that caps lock does not emit its own repeat. term.SetRepeat(time.Millisecond*50, time.Millisecond*250) term.KeyPress(vt.KeyCapsLock) term.KeyPress(vt.KeyZ) time.Sleep(90 * time.Millisecond) term.KeyPress(vt.KeyZ) term.KeyRelease(vt.KeyZ) term.KeyRelease(vt.KeyCapsLock) CheckRead(t, term, "ZZ") // 0 ms, 50 ms } // TestKeyRepeatNoAlt ensures that alt keys do not repeat func TestKeyRepeatNoAlt(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Keep the initial repeat quick, but make later repeats very slow so the test // does not sit on a timer boundary. term.SetRepeat(time.Millisecond*50, time.Second) term.KeyPress(vt.KeyLAlt) term.KeyPress(vt.KeyZ) time.Sleep(100 * time.Millisecond) term.KeyPress(vt.KeyZ) term.KeyRelease(vt.KeyZ) term.KeyRelease(vt.KeyLAlt) CheckRead(t, term, "\x1bz") } // TestKeyRepeatShift ensures that shifted keys still work as long as repeat is held down. func TestKeyRepeatShift(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Use a large repeat interval so the test is not sitting on a timing boundary. // We only need to prove that shifted keys keep their shifted value while repeating. term.SetRepeat(time.Millisecond*50, time.Millisecond*250) term.KeyPress(vt.KeyLShift) term.KeyPress(vt.Key1) time.Sleep(90 * time.Millisecond) term.KeyPress(vt.Key1) term.KeyRelease(vt.Key1) term.KeyRelease(vt.KeyLShift) CheckRead(t, term, "!!") // 0 ms, 50 ms } // TestKeyRepeatShiftRelease ensures that releasing shift breaks repeat. func TestKeyRepeatShiftRelease(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Use a large repeat interval so the test is not sitting on a timing boundary. // We only need to prove that releasing shift changes the repeated value. term.SetRepeat(time.Millisecond*50, time.Millisecond*250) term.KeyPress(vt.KeyLShift) term.KeyPress(vt.Key2) term.KeyRelease(vt.KeyLShift) time.Sleep(90 * time.Millisecond) term.KeyPress(vt.Key2) term.KeyRelease(vt.Key2) CheckRead(t, term, "@2") // 0 ms, 50 ms } func TestKeyRepeatCursor(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) // Keep the initial repeat quick, but make later repeats very slow so the test // does not depend on coarse timer behavior. term.SetRepeat(time.Millisecond*50, time.Second) term.KeyPress(vt.KeyRight) time.Sleep(100 * time.Millisecond) term.KeyPress(vt.KeyRight) term.KeyRelease(vt.KeyRight) term.KeyRelease(vt.KeyRight) CheckRead(t, term, "\x1b[C\x1b[C") } func TestKeyWin32(t *testing.T) { // Win32 input mode is ESC [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ term := vt.NewMockTerm() defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "%s", vt.PmWin32Input.Enable()) var want string term.KeyTap(vt.KeySpace) want = "\x1b[32;57;32;1;0;1_" + "\x1b[32;57;32;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyLAlt, vt.KeyG) want = "\x1b[164;56;0;1;2;1_" + // LAlt down "\x1b[71;34;103;1;2;1_" + // G down "\x1b[71;34;103;0;2;1_" + // G up "\x1b[164;56;0;0;0;1_" // LAlt up CheckRead(t, term, want) term.KeyTap(vt.KeyRAlt, vt.Key1) want = "\x1b[165;57400;0;1;1;1_" + // RAlt down "\x1b[49;2;49;1;1;1_" + // 1 down "\x1b[49;2;49;0;1;1_" + // 1 up "\x1b[165;57400;0;0;0;1_" // RAlt up CheckRead(t, term, want) term.KeyTap(vt.KeyRCtrl, vt.KeyF1) want = "\x1b[163;57373;0;1;4;1_" + // RCtrl down "\x1b[112;59;0;1;4;1_" + // F1 down "\x1b[112;59;0;0;4;1_" + // F1 up "\x1b[163;57373;0;0;0;1_" // RCtrl up CheckRead(t, term, want) // NB: Windows has no "back tab" notion term.KeyTap(vt.KeyLShift, vt.KeyTab) want = "\x1b[160;42;0;1;16;1_\x1b[9;15;9;1;16;1_\x1b[9;15;9;0;16;1_\x1b[160;42;0;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyNumLock) term.KeyTap(vt.KeyPad0) term.KeyTap(vt.KeyNumLock) want = "\x1b[144;69;0;1;32;1_" + // num lock down "\x1b[144;69;0;0;32;1_" + // num lock up "\x1b[45;82;48;1;32;1_" + // key pad 0 down "\x1b[45;82;48;0;32;1_" + // keypad 0 up "\x1b[144;69;0;1;0;1_" + // num lock down "\x1b[144;69;0;0;0;1_" // num lock up CheckRead(t, term, want) term.KeyTap(vt.KeyCapsLock) term.KeyTap(vt.KeyJ) term.KeyTap(vt.KeyCapsLock) want = "\x1b[20;58;0;1;128;1_" + // caps lock down "\x1b[20;58;0;0;128;1_" + // caps lock up "\x1b[74;36;74;1;128;1_" + // J down "\x1b[74;36;74;0;128;1_" + // J up "\x1b[20;58;0;1;0;1_" + // caps lock down "\x1b[20;58;0;0;0;1_" // caps lock up CheckRead(t, term, want) term.KeyTap(vt.KeyPadEnter) want = "\x1b[0;57372;13;1;0;1_\x1b[0;57372;13;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyPadDiv) want = "\x1b[111;57397;47;1;0;1_\x1b[111;57397;47;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyInsert) want = "\x1b[45;57426;0;1;0;1_\x1b[45;57426;0;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyDelete) want = "\x1b[46;57427;0;1;0;1_\x1b[46;57427;0;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyPgUp) want = "\x1b[33;57417;0;1;0;1_\x1b[33;57417;0;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyPgDn) want = "\x1b[34;57425;0;1;256;1_\x1b[34;57425;0;0;256;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyHome) want = "\x1b[36;57415;0;1;0;1_\x1b[36;57415;0;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyEnd) want = "\x1b[35;57423;0;1;0;1_\x1b[35;57423;0;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyEnter) want = "\x1b[13;28;13;1;0;1_\x1b[13;28;13;0;0;1_" CheckRead(t, term, want) term.KeyTap(vt.KeyLMeta) want = "\x1b[91;57435;0;1;0;1_\x1b[91;57435;0;0;0;1_" CheckRead(t, term, want) } func TestKeyWin32NoRepeat(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) // Keep the first repeat available, but avoid asserting an exact number of // repeats from short sleeps on platforms with coarse timers. term.SetRepeat(time.Millisecond*50, time.Second) MustStart(t, term) WriteF(t, term, "%s", vt.PmWin32Input.Enable()) WriteF(t, term, "%s", vt.PmAutoRepeat.Enable()) term.KeyPress(vt.KeyPause) time.Sleep(time.Millisecond * 100) term.KeyPress(vt.KeyPause) term.KeyRelease(vt.KeyPause) want := "\x1b[19;57414;0;1;0;1_\x1b[19;57414;0;0;0;1_" CheckRead(t, term, want) } func TestKeyWin32Repeat(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) // Keep the first repeat available, but avoid asserting an exact number of // repeats from short sleeps on platforms with coarse timers. term.SetRepeat(time.Millisecond*50, time.Second) MustStart(t, term) WriteF(t, term, "%s", vt.PmWin32Input.Enable()) WriteF(t, term, "%s", vt.PmAutoRepeat.Enable()) term.KeyPress(vt.KeyA) time.Sleep(time.Millisecond * 100) term.KeyPress(vt.KeyA) term.KeyRelease(vt.KeyA) want := "\x1b[65;30;97;1;0;1_\x1b[65;30;97;1;0;1_\x1b[65;30;97;0;0;1_" CheckRead(t, term, want) } func TestKeyWin32NoRepeatMode(t *testing.T) { term := vt.NewMockTerm() defer MustClose(t, term) // Keep the first repeat available, but make later repeats slow so this only // verifies that auto-repeat mode suppresses repeat events. term.SetRepeat(time.Millisecond*50, time.Second) MustStart(t, term) WriteF(t, term, "%s", vt.PmWin32Input.Enable()) WriteF(t, term, "%s", vt.PmAutoRepeat.Disable()) term.KeyPress(vt.KeyB) time.Sleep(time.Millisecond * 100) term.KeyPress(vt.KeyB) term.KeyRelease(vt.KeyB) want := "\x1b[66;48;98;1;0;1_\x1b[66;48;98;0;0;1_" CheckRead(t, term, want) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/mock_test.go000066400000000000000000001336711520475227200243630ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "strings" "testing" "time" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/vt" ) // TestCursorMove tests several aspects of cursor movement. func TestCursorMovement(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) if size, err := term.WindowSize(); err != nil { t.Fatalf("failed getting window size: %v", err) } else if size.Height != 3 || size.Width != 5 { t.Fatalf("wrong window size X %d Y %d", size.Width, size.Height) } WriteF(t, term, "\x1b[2;3H") CheckPos(t, term, 2, 1) WriteF(t, term, "\x1b[20A") // up 20 CheckPos(t, term, 2, 0) WriteF(t, term, "\x1b[20B") // down 20 CheckPos(t, term, 2, 2) WriteF(t, term, "\x1b[A") // up 1 CheckPos(t, term, 2, 1) WriteF(t, term, "\x1b[2C") // right 2 CheckPos(t, term, 4, 1) WriteF(t, term, "\x1b[3D") // left 3 CheckPos(t, term, 1, 1) WriteF(t, term, "\x1b[100D") // left 100 CheckPos(t, term, 0, 1) // Now try the next line and previous line WriteF(t, term, "\x1b[2;3H") CheckPos(t, term, 2, 1) WriteF(t, term, "\x1b[1E") CheckPos(t, term, 0, 2) WriteF(t, term, "\x1b[2;3H") CheckPos(t, term, 2, 1) WriteF(t, term, "\x1b[1F") CheckPos(t, term, 0, 0) WriteF(t, term, "\x1b9") CheckPos(t, term, 1, 0) WriteF(t, term, "\x1b6") CheckPos(t, term, 0, 0) } // TestDECALN tests the DEC alignment test (screen filled with 'E'). func TestDECALN(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b#8") for y := range Row(3) { for x := range Col(5) { CheckAttrs(t, term, x, y, vt.Plain) CheckContent(t, term, x, y, "E") } } // clear screen WriteF(t, term, "\x1b[H\x1b[J") for y := range Row(3) { for x := range Col(5) { CheckAttrs(t, term, x, y, vt.Plain) CheckContent(t, term, x, y, "") } } WriteF(t, term, "\x1b[1m\x1b#8") // bold, DECALN for y := range Row(3) { for x := range Col(5) { CheckAttrs(t, term, x, y, vt.Plain) CheckContent(t, term, x, y, "E") } } } // TestBell tests the bell. func TestBell(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) if term.Bells() != 0 { t.Errorf("wrong bell count: %d", term.Bells()) } WriteF(t, term, "\x07") if term.Bells() != 1 { t.Errorf("wrong bell count: %d", term.Bells()) } WriteF(t, term, "\x07") if term.Bells() != 2 { t.Errorf("wrong bell count: %d", term.Bells()) } } // TestPrimaryDA tests primary device attributes using several mechanisms. func TestPrimaryDA(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) buf := make([]byte, 32) WriteF(t, term, "\x1b[c") n, err := term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result := string(buf[:n]) if !strings.HasSuffix(result, "c") { t.Errorf("Missing suffix 'c': %q", result) } if !strings.HasPrefix(result, "\x1b[?63") { t.Errorf("Missing prefix '\x1b[?63': %q", result) } // Legacy version buf = make([]byte, 32) // to start over WriteF(t, term, "\x1bZ") n, err = term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result = string(buf[:n]) if !strings.HasSuffix(result, "c") { t.Errorf("Missing suffix 'c': %q", result) } if !strings.HasPrefix(result, "\x1b[?63") { t.Errorf("Missing prefix '\x1b[?63': %q", result) } } // TestExtendedAttr requests the terminal ID using CSI > q. func TestExtendedAttr(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) buf := make([]byte, 64) WriteF(t, term, "\x1b[>q") n, err := term.Read(buf) AssertF(t, err == nil, "read failed: %v", err) result := string(buf[:n]) VerifyF(t, strings.HasSuffix(result, "\x1b\\"), "missing suffix ST: %q", result) VerifyF(t, strings.HasPrefix(result, "\x1bP>|"), "missing prefix 'ESC P>|': %q", result) VerifyF(t, strings.Contains(result, "TCellMock 1.0"), "missing terminal identification: %q", result) } // TestCursorReport verifies that cursor position reporting works. func TestCursorReport(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[5;10H") // fifth row, tenth column CheckPos(t, term, 9, 4) WriteF(t, term, "\x1b[6n") // cursor position report buf := make([]byte, 32) n, err := term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result := string(buf[:n]) if result != "\x1b[5;10R" { t.Errorf("wrong report: %q", result) } buf = make([]byte, 32) // move the cursor back one WriteF(t, term, "\b\x1b[6n") CheckPos(t, term, 8, 4) n, err = term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result = string(buf[:n]) if result != "\x1b[5;9R" { t.Errorf("wrong report: %q", result) } } // TestAnsiModes tests the private mode feature. func TestAnsiModes(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[20$p") // query for newline mode WriteF(t, term, "\x1b[20h") // turn it on WriteF(t, term, "\x1b[20$p") // should read back on WriteF(t, term, "\x1b[20l") // turn it back off WriteF(t, term, "\x1b[20$p") // should read back off buf := make([]byte, 128) n, err := term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result := string(buf[:n]) want := "\x1b[20;2$y" + "\x1b[20;1$y" + "\x1b[20;2$y" if result != want { t.Errorf("wrong response: %q != %q", result, want) } } // TestPrivateModes tests the private mode feature. func TestPrivateModes(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[?7$p") // query for auto-margin (should start on by default) WriteF(t, term, "\x1b[?7l") // turn it off WriteF(t, term, "\x1b[?7$p") // should read back positive WriteF(t, term, "\x1b[?7h") // put it back on WriteF(t, term, "\x1b[?7$p") // should read back negative WriteF(t, term, "\x1b[?1919$p") // read invalid mode WriteF(t, term, "\x1b[?1919h\x1b[?1919l") // togle invalid mode WriteF(t, term, "\x1b[?1919$p") // read invalid mode one more time buf := make([]byte, 128) n, err := term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result := string(buf[:n]) want := "\x1b[?7;1$y" + "\x1b[?7;2$y" + "\x1b[?7;1$y" + "\x1b[?1919;0$y" + "\x1b[?1919;0$y" if result != want { t.Errorf("wrong response: %q != %q", result, want) } // Lets also test the cursor (show vs hide) WriteF(t, term, "\x1b[?25$p") WriteF(t, term, "\x1b[?25l") WriteF(t, term, "\x1b[?25$p") WriteF(t, term, "\x1b[?25h") WriteF(t, term, "\x1b[?25$p") buf = make([]byte, 128) n, err = term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result = string(buf[:n]) want = "\x1b[?25;1$y" + "\x1b[?25;2$y" + "\x1b[?25;1$y" if result != want { t.Errorf("wrong response: %q != %q", result, want) } } // TestAutoMargin tests the auto margin feature. func TestAutoMargin(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) // default is auto-margin is enabled WriteF(t, term, "\x1b[2J") // clear the screen WriteF(t, term, "\x1b[1;80HAB") CheckPos(t, term, 1, 1) if s := string(term.GetCell(Coord{X: 79, Y: 0}).C); s != "A" { t.Errorf("last column wrong: %q", s) } if s := string(term.GetCell(Coord{X: 0, Y: 1}).C); s != "B" { t.Errorf("auto wrap did not work: %q", s) } // now turn it off WriteF(t, term, "\x1b[?7l") // mess with 3rd row WriteF(t, term, "\x1b[3;80HCD") CheckPos(t, term, 79, 2) if s := string(term.GetCell(Coord{X: 79, Y: 2}).C); s != "D" { t.Errorf("last column wrong: %q", s) } // turn it back on WriteF(t, term, "\x1b[?7h") // demonstrate that writing to the last column does not advance (pending) WriteF(t, term, "\x1b[1;80HA") CheckPos(t, term, 79, 0) // but one more character does advance WriteF(t, term, "\x1b[1;80HAB") CheckPos(t, term, 1, 1) // tab does not advance, but leaves pending state WriteF(t, term, "\x1b[1;80HA\t") CheckPos(t, term, 79, 0) WriteF(t, term, "\x1b[1;80HA\tb") CheckPos(t, term, 1, 1) // up or down movement resets the pending state WriteF(t, term, "\x1b[1;80HA\x1b[AB") CheckPos(t, term, 79, 0) WriteF(t, term, "\x1b[1;80HA\x1b[BB") CheckPos(t, term, 79, 1) // forward also resets pending state (which is clipped) WriteF(t, term, "\x1b[1;80HA\x1b[CB") CheckPos(t, term, 79, 0) WriteF(t, term, "\x1b[1;80HA\x1b[CBC") CheckPos(t, term, 1, 1) // explicit column also resets pending state (which is clipped) WriteF(t, term, "\x1b[1;80HA\x1b[80GB") CheckPos(t, term, 79, 0) WriteF(t, term, "\x1b[1;80HA\x1b[80GBC") CheckPos(t, term, 1, 1) // newline of course as well (and also VF and FF) WriteF(t, term, "\x1b[1;80HA\nB") CheckPos(t, term, 79, 1) WriteF(t, term, "\x1b[1;80HA\fB") CheckPos(t, term, 79, 1) WriteF(t, term, "\x1b[1;80HA\vB") CheckPos(t, term, 79, 1) // and also index WriteF(t, term, "\x1b[1;80HA\x1bDB") CheckPos(t, term, 79, 1) // and also reverse index WriteF(t, term, "\x1b[2;80HA\x1bMB") CheckPos(t, term, 79, 0) } // TestUnicode tests basic placement of unicode glyphs. // For now it assumes that the terminal itself supports unicode / latin 1. func TestUnicode(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[2J") // clear the screen WriteF(t, term, "\x1b[2;2H") CheckPos(t, term, 1, 1) WriteF(t, term, "åßcπ") CheckPos(t, term, 5, 1) if s := string(term.GetCell(Coord{X: 1, Y: 1}).C); s != "å" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 2, Y: 1}).C); s != "ß" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 3, Y: 1}).C); s != "c" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 4, Y: 1}).C); s != "π" { t.Errorf("decode error wrong: %q", s) } } // TestUnicodeWide tests a wide unicode glyph. func TestUnicodeWide(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b#8") // fill it with E's (so we can see that wide clears the next cell) WriteF(t, term, "\x1b[2;2H") CheckPos(t, term, 1, 1) WriteF(t, term, "å宽cπ") CheckPos(t, term, 6, 1) if s := string(term.GetCell(Coord{X: 1, Y: 1}).C); s != "å" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 2, Y: 1}).C); s != "宽" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 3, Y: 1}).C); s != "" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 4, Y: 1}).C); s != "c" { t.Errorf("decode error wrong: %q", s) } if s := string(term.GetCell(Coord{X: 5, Y: 1}).C); s != "π" { t.Errorf("decode error wrong: %q", s) } } // TestKeyEventLegacy tests key events when using the default legacy key protocol. func TestKeyEventLegacy(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0)) defer MustClose(t, term) MustStart(t, term) term.KeyTap(vt.KeyA) term.KeyTap(vt.KeyLShift, vt.KeyB) term.KeyTap(vt.KeyEnter) term.KeyTap(vt.KeyRCtrl, vt.KeyI) term.KeyTap(vt.KeyEsc) buf := make([]byte, 256) n, err := term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result := string(buf[:n]) want := "aB\r\x09\x1B" if result != want { t.Errorf("key responses failed: %q != %q", result, want) } // SS3 based F-keys clear(buf) term.KeyTap(vt.KeyF1) // SS3 P term.KeyTap(vt.KeyLShift, vt.KeyF1) // CSI 1 ; 2 P term.KeyTap(vt.KeyLCtrl, vt.KeyF2) // CSI 1 ; 5 Q term.KeyTap(vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyF3) // ESC CSI 1 ; 6 R term.KeyTap(vt.KeyRAlt, vt.KeyRCtrl, vt.KeyF4) // ESC CSI 1 ; 5 S want = "\x1bOP" want += "\x1b[1;2P" want += "\x1b[1;5Q" want += "\x1b\x1b[1;6R" want += "\x1b\x1b[1;5S" n, err = term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) } result = string(buf[:n]) if result != want { t.Errorf("key responses failed: %q != %q", result, want) } // CSI based F-keys buf = make([]byte, 256) term.KeyTap(vt.KeyF5) // CSI 15 ~ term.KeyTap(vt.KeyRShift, vt.KeyF6) // CSI 17 ; 2 ~ term.KeyTap(vt.KeyLCtrl, vt.KeyF7) // CSI 18 ; 5 ~ term.KeyTap(vt.KeyLAlt, vt.KeyRCtrl, vt.KeyLShift, vt.KeyF8) // ESC CSI 19 ; 6 ~ term.KeyTap(vt.KeyRAlt, vt.KeyLCtrl, vt.KeyF9) // ESC CSI 20 ; 5 ~ term.KeyTap(vt.KeyF20) // CSI 34 ~ term.KeyTap(vt.KeyF15) // CSI 28 ~ term.KeyTap(vt.KeyMenu) // CSI 29 ~ want = "\x1b[15~" want += "\x1b[17;2~" want += "\x1b[18;5~" want += "\x1b\x1b[19;6~" want += "\x1b\x1b[20;5~" want += "\x1b[34~" want += "\x1b[28~" want += "\x1b[29~" n, err = term.Read(buf) AssertF(t, err == nil, "failed read: %v", err) result = string(buf[:n]) VerifyF(t, result == want, "key responses failed: %q != %q", result, want) // Misc other keys clear(buf) term.KeyTap(vt.KeyEnter) // \r term.KeyTap(vt.KeyTab) // \t term.KeyTap(vt.KeyLShift, vt.KeyTab) // CSI Z term.KeyTap(vt.KeyLCtrl, vt.KeyM) // \r term.KeyTap(vt.KeyLCtrl, vt.KeyL) // \f term.KeyTap(vt.KeyBackspace) // \x7f term.KeyTap(vt.KeyRCtrl, vt.KeyBackspace) // \x08 term.KeyTap(vt.KeyRCtrl, vt.KeyLShift, vt.KeyBackspace) // none term.KeyTap(vt.KeyRCtrl, vt.KeySpace) // \x00 term.KeyTap(vt.KeySpace) // ' ' term.KeyTap(vt.KeyRAlt, vt.KeyA) // \x1b a term.KeyTap(vt.KeyRHyper, vt.KeyA) // none term.KeyTap(vt.KeyRMeta, vt.KeyA) // none term.KeyTap(vt.KeyRAlt, vt.KeyLCtrl, vt.KeyJ) // \x1b\n term.KeyTap(vt.KeyRCtrl, vt.KeyL) // \x0c term.KeyTap(vt.KeyLCtrl, vt.KeyLBrace) // \x0c want = "\r\t\x1b[Z\r\f\x7f\x08\x00 \x1ba\x1b\n\x0c\x1b" n, err = term.Read(buf) AssertF(t, err == nil, "failed read: %v", err) result = string(buf[:n]) VerifyF(t, result == want, "key responses failed: %q != %q", result, want) // Legacy control key mappings (weird ones) // Declining a few of the strange ones (control-?) clear(buf) term.KeyTap(vt.KeyLCtrl, vt.Key8) // \x7F term.KeyTap(vt.KeyLCtrl, vt.Key4) // \x1c term.KeyTap(vt.KeyLCtrl, vt.Key7) // \x1f term.KeyTap(vt.Key7) // 7 term.KeyTap(vt.KeyLShift, vt.KeySlash) // ? term.KeyTap(vt.KeyRCtrl, vt.KeyLBrace) // \x1b want = "\x7f\x1c\x1f7?\x1b" n, err = term.Read(buf) AssertF(t, err == nil, "failed read: %v", err) result = string(buf[:n]) VerifyF(t, result == want, "key responses failed: %q != %q", result, want) // Application cursor keys term.KeyTap(vt.KeyUp) WriteF(t, term, "\x1b[?1h") term.KeyTap(vt.KeyDown) want = "\x1b[A\x1bOB" n, err = term.Read(buf) AssertF(t, err == nil, "failed read: %v", err) result = string(buf[:n]) VerifyF(t, result == want, "key responses failed: %q != %q", result, want) // Special Tab term.KeyTap(vt.KeyLShift, vt.KeyLCtrl, vt.KeyTab) term.KeyTap(vt.KeySpace) CheckRead(t, term, "\x1b[Z ") } // TestSgrAttr tests a variety of combinations of Sgr settings. func TestSgrAttr(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[H") WriteF(t, term, "\x1b[1mA") // bold CheckAttrs(t, term, 0, 0, vt.Bold) CheckContent(t, term, 0, 0, "A") WriteF(t, term, "\x1b[2mB") // dim CheckAttrs(t, term, 1, 0, vt.Dim) CheckContent(t, term, 1, 0, "B") WriteF(t, term, "\x1b[22mC") // clear both CheckAttrs(t, term, 2, 0, vt.Plain) CheckContent(t, term, 2, 0, "C") WriteF(t, term, "\x1b[3;2mD") // italic, dim CheckAttrs(t, term, 3, 0, vt.Italic|vt.Dim) CheckContent(t, term, 3, 0, "D") WriteF(t, term, "\x1b[22mE") // remove dim, should leave italic CheckAttrs(t, term, 4, 0, vt.Italic) CheckContent(t, term, 4, 0, "E") WriteF(t, term, "\x1b[23mF") // clear italic CheckAttrs(t, term, 5, 0, vt.Plain) CheckContent(t, term, 5, 0, "F") WriteF(t, term, "\x1b[3;4mG") // simple underline CheckAttrs(t, term, 6, 0, vt.Italic|vt.Underline) CheckContent(t, term, 6, 0, "G") WriteF(t, term, "\x1b[21mH") // double underline (ECMA) CheckAttrs(t, term, 7, 0, vt.Italic|vt.DoubleUnderline) CheckContent(t, term, 7, 0, "H") WriteF(t, term, "\x1b[4mI") // simple underline CheckAttrs(t, term, 8, 0, vt.Italic|vt.Underline) CheckContent(t, term, 8, 0, "I") WriteF(t, term, "\x1b[4:2mJ") // double underline CheckAttrs(t, term, 9, 0, vt.Italic|vt.DoubleUnderline) CheckContent(t, term, 9, 0, "J") WriteF(t, term, "\x1b[4:3mK") // curly underline CheckAttrs(t, term, 10, 0, vt.Italic|vt.CurlyUnderline) CheckContent(t, term, 10, 0, "K") WriteF(t, term, "\x1b[4:4mL") // dotted underline CheckAttrs(t, term, 11, 0, vt.Italic|vt.DottedUnderline) CheckContent(t, term, 11, 0, "L") WriteF(t, term, "\x1b[4:5mM") // dashed underline CheckAttrs(t, term, 12, 0, vt.Italic|vt.DashedUnderline) CheckContent(t, term, 12, 0, "M") WriteF(t, term, "\x1b[4:9mN") // junk treats as plain underline CheckAttrs(t, term, 13, 0, vt.Italic|vt.Underline) CheckContent(t, term, 13, 0, "N") WriteF(t, term, "\x1b[4:5;24mO") // clustering, clear it CheckAttrs(t, term, 14, 0, vt.Italic) CheckContent(t, term, 14, 0, "O") WriteF(t, term, "\x1b[0;9;7;53mP") // clear, strike-through, reverse, over-lined CheckAttrs(t, term, 15, 0, vt.StrikeThrough|vt.Reverse|vt.Overline) CheckContent(t, term, 15, 0, "P") WriteF(t, term, "\x1b[5;27;29;55mQ") CheckAttrs(t, term, 16, 0, vt.Blink) CheckContent(t, term, 16, 0, "Q") WriteF(t, term, "\x1b[25mR") CheckAttrs(t, term, 17, 0, vt.Plain) CheckContent(t, term, 17, 0, "R") } // TestSgrColor8 tests simple ECMA 48 ANSI color (only 8 possible color values.) func TestSgrColor8(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[36;42m\x1b#8") CheckColors(t, term, 0, 0, color.Teal, color.Green) WriteF(t, term, "\x1b[H\x1b[39mA") CheckColors(t, term, 0, 0, color.Silver, color.Green) WriteF(t, term, "\x1b[49mA") CheckColors(t, term, 1, 0, color.Silver, color.Black) // verify zero clears colors, first write some non zero colors WriteF(t, term, "\x1b[36;42mD") CheckColors(t, term, 2, 0, color.Teal, color.Green) // then send zero, should go to default colors WriteF(t, term, "\x1b[0mA") CheckColors(t, term, 3, 0, color.Silver, color.Black) } // TestSgrMalformedParam verifies that malformed SGR parameters are ignored // individually without dropping the rest of the sequence. func TestSgrMalformedParam(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[31; 4 { CheckContent(t, term, col, row, "E") CheckAttrs(t, term, col, row, vt.Plain) CheckColors(t, term, col, row, color.Silver, color.Black) } else { CheckContent(t, term, col, row, "") CheckAttrs(t, term, col, row, vt.Plain) CheckColors(t, term, col, row, color.XTerm1, color.XTerm2) } } CheckPos(t, term, 4, 1) // erase entire line WriteF(t, term, "\x1b[3;5H") WriteF(t, term, "\x1b[2K") // cursor is at 4,2 CheckPos(t, term, 4, 2) for col := range Col(10) { row := Row(2) CheckContent(t, term, col, row, "") CheckAttrs(t, term, col, row, vt.Plain) CheckColors(t, term, col, row, color.XTerm1, color.XTerm2) } CheckPos(t, term, 4, 2) } // TestNewLineScroll tests scrolling with a new line. // This is one of the most fundamental operations for a terminal. func TestNewLineScroll(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[H\x1b[J") // home and clear WriteF(t, term, "\x1b[1;1HA") WriteF(t, term, "\x1b[2;1HB") WriteF(t, term, "\x1b[5;1HC") // first column on last row CheckContent(t, term, 0, 4, "C") WriteF(t, term, "\n") // new line should scroll CheckPos(t, term, 1, 4) CheckContent(t, term, 0, 0, "B") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 0, 4, "") CheckContent(t, term, 0, 3, "C") } // TestNewLineScrollNoBlitter tests scrolling with a new line, // using the fallback copy for backends without Blit support. func TestNewLineScrollNoBlitter(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5}, vt.MockOptNoBlit{}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[H\x1b[J") // home and clear WriteF(t, term, "\x1b[1;1HA") WriteF(t, term, "\x1b[2;1HB") WriteF(t, term, "\x1b[5;1HC") // first column on last row CheckContent(t, term, 0, 4, "C") WriteF(t, term, "\n") // new line should scroll CheckPos(t, term, 1, 4) CheckContent(t, term, 0, 0, "B") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 0, 4, "") CheckContent(t, term, 0, 3, "C") } func TestNewLineModes(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 4}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[H\x1b[J") WriteF(t, term, "ABC\n") CheckPos(t, term, 3, 1) term.KeyTap(vt.KeyEnter) WriteF(t, term, "\x1b[20h") term.KeyTap(vt.KeyEnter) WriteF(t, term, "DEF") CheckPos(t, term, 6, 1) WriteF(t, term, "\n") CheckPos(t, term, 0, 2) WriteF(t, term, "GHI") CheckPos(t, term, 3, 2) WriteF(t, term, "\x1b[20l") WriteF(t, term, "\n") CheckPos(t, term, 3, 3) term.KeyTap(vt.KeyEnter) // |ABC_____| // |___DEF__| // |GHI_____| // |___c____| // // input stream contains \r\r\n\r CheckContent(t, term, 0, 0, "A") CheckContent(t, term, 1, 0, "B") CheckContent(t, term, 2, 0, "C") CheckContent(t, term, 3, 0, "") CheckContent(t, term, 4, 0, "") CheckContent(t, term, 5, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 1, 1, "") CheckContent(t, term, 2, 1, "") CheckContent(t, term, 3, 1, "D") CheckContent(t, term, 4, 1, "E") CheckContent(t, term, 5, 1, "F") CheckContent(t, term, 0, 2, "G") CheckContent(t, term, 1, 2, "H") CheckContent(t, term, 2, 2, "I") CheckContent(t, term, 3, 2, "") CheckContent(t, term, 4, 2, "") CheckContent(t, term, 5, 2, "") result := ReadF(t, term) want := "\r\r\n\r" VerifyF(t, result == want, "response incorrect: %q != %q", result, want) } // TestScrollUp tests scrolling up. The cursor position is retained. func TestScrollUp(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[H\x1b[J") // home and clear WriteF(t, term, "\x1b[1;1HA") WriteF(t, term, "\x1b[2;1HB") WriteF(t, term, "\x1b[5;1HC") // first column on last row CheckContent(t, term, 0, 4, "C") WriteF(t, term, "\x1b[5;5H") // fifth column on last row CheckPos(t, term, 4, 4) WriteF(t, term, "\x1bD") CheckPos(t, term, 4, 4) CheckContent(t, term, 0, 0, "B") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 0, 4, "") CheckContent(t, term, 0, 3, "C") WriteF(t, term, "\x1bE") // this is like a newline CheckPos(t, term, 0, 4) CheckContent(t, term, 0, 2, "C") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 0, 4, "") WriteF(t, term, "\x1b[H\x1bJ") WriteF(t, term, "\x1b[1;1HA") WriteF(t, term, "\x1b[2;1HB") WriteF(t, term, "\x1b[3;1HC") WriteF(t, term, "\x1b[4;1HD") WriteF(t, term, "\x1b[5;1HE") WriteF(t, term, "\x1b[3;3H") WriteF(t, term, "\x1b[3S") // scroll in place, leaves cursor where it is CheckContent(t, term, 0, 0, "D") CheckContent(t, term, 0, 1, "E") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 0, 4, "") CheckPos(t, term, 2, 2) } // TestScrollDown tests scrolling down. The cursor position is retained. func TestScrollDown(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5}) defer MustClose(t, term) MustStart(t, term) WriteF(t, term, "\x1b[H\x1b[J") // home and clear WriteF(t, term, "\x1b[1;1HA") WriteF(t, term, "\x1b[2;1HB") WriteF(t, term, "\x1b[4;1HC") // first column on penultimate row WriteF(t, term, "\x1b[5;1HD") // first column on last row CheckContent(t, term, 0, 3, "C") CheckContent(t, term, 0, 4, "D") WriteF(t, term, "\x1b[1;5H") // fifth column on first row CheckPos(t, term, 4, 0) WriteF(t, term, "\x1bM") CheckPos(t, term, 4, 0) CheckContent(t, term, 0, 0, "") CheckContent(t, term, 0, 1, "A") CheckContent(t, term, 0, 2, "B") CheckContent(t, term, 0, 3, "") CheckContent(t, term, 0, 4, "C") WriteF(t, term, "\x1b[H\x1bJ") WriteF(t, term, "\x1b[1;1HA") WriteF(t, term, "\x1b[2;1HB") WriteF(t, term, "\x1b[3;1HC") WriteF(t, term, "\x1b[4;1HD") WriteF(t, term, "\x1b[5;1HE") WriteF(t, term, "\x1b[3;3H") WriteF(t, term, "\x1b[3T") // scroll in place, leaves cursor where it is CheckContent(t, term, 0, 0, "") CheckContent(t, term, 0, 1, "") CheckContent(t, term, 0, 2, "") CheckContent(t, term, 0, 3, "A") CheckContent(t, term, 0, 4, "B") CheckPos(t, term, 2, 2) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/uslayout_test.go000066400000000000000000000512321520475227200253070ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "slices" "testing" "github.com/gdamore/tcell/v3/vt" "github.com/gdamore/tcell/v3/vt/layouts/us" ) func TestGetLayout(t *testing.T) { l := vt.GetLayout("US International") if l == nil { t.Fatalf("no keyboard layout found") } if l != us.UsInternational { t.Errorf("got the wrong layout: %s", l.Name) } layouts := vt.Layouts() VerifyF(t, slices.Contains(layouts, "US International"), "layout not present") } func TestUsAltGr(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) term.SetLayout(vt.GetLayout("US International")) MustStart(t, term) cases := []struct { name string keys []vt.Key want string }{ {"AltGr-0", []vt.Key{vt.KeyRAlt, vt.Key0}, "’"}, {"AltGr-1", []vt.Key{vt.KeyRAlt, vt.Key1}, "¡"}, {"AltGr-2", []vt.Key{vt.KeyRAlt, vt.Key2}, "²"}, {"AltGr-3", []vt.Key{vt.KeyRAlt, vt.Key3}, "³"}, {"AltGr-4", []vt.Key{vt.KeyRAlt, vt.Key4}, "¤"}, {"AltGr-5", []vt.Key{vt.KeyRAlt, vt.Key5}, "€"}, {"AltGr-6", []vt.Key{vt.KeyRAlt, vt.Key6}, "¼"}, {"AltGr-7", []vt.Key{vt.KeyRAlt, vt.Key7}, "½"}, {"AltGr-8", []vt.Key{vt.KeyRAlt, vt.Key8}, "¾"}, {"AltGr-9", []vt.Key{vt.KeyRAlt, vt.Key9}, "‘"}, {"AltGr-A", []vt.Key{vt.KeyRAlt, vt.KeyA}, "á"}, {"AltGr-C", []vt.Key{vt.KeyRAlt, vt.KeyC}, "©"}, {"AltGr-D", []vt.Key{vt.KeyRAlt, vt.KeyD}, "ð"}, {"AltGr-E", []vt.Key{vt.KeyRAlt, vt.KeyE}, "é"}, {"AltGr-I", []vt.Key{vt.KeyRAlt, vt.KeyI}, "í"}, {"AltGr-L", []vt.Key{vt.KeyRAlt, vt.KeyL}, "ø"}, {"AltGr-M", []vt.Key{vt.KeyRAlt, vt.KeyM}, "µ"}, {"AltGr-N", []vt.Key{vt.KeyRAlt, vt.KeyN}, "ñ"}, {"AltGr-O", []vt.Key{vt.KeyRAlt, vt.KeyO}, "ó"}, {"AltGr-P", []vt.Key{vt.KeyRAlt, vt.KeyP}, "ö"}, {"AltGr-Q", []vt.Key{vt.KeyRAlt, vt.KeyQ}, "ä"}, {"AltGr-R", []vt.Key{vt.KeyRAlt, vt.KeyR}, "®"}, {"AltGr-S", []vt.Key{vt.KeyRAlt, vt.KeyS}, "ß"}, {"AltGr-T", []vt.Key{vt.KeyRAlt, vt.KeyT}, "þ"}, {"AltGr-U", []vt.Key{vt.KeyRAlt, vt.KeyU}, "ú"}, {"AltGr-W", []vt.Key{vt.KeyRAlt, vt.KeyW}, "å"}, {"AltGr-Y", []vt.Key{vt.KeyRAlt, vt.KeyY}, "ü"}, {"AltGr-Z", []vt.Key{vt.KeyRAlt, vt.KeyZ}, "æ"}, {"AltGr-Semi", []vt.Key{vt.KeyRAlt, vt.KeySemi}, "¶"}, {"AltGr-Equal", []vt.Key{vt.KeyRAlt, vt.KeyEqual}, "×"}, {"AltGr-Comma", []vt.Key{vt.KeyRAlt, vt.KeyComma}, "ç"}, {"AltGr-Minus", []vt.Key{vt.KeyRAlt, vt.KeyMinus}, "¥"}, {"AltGr-Slash", []vt.Key{vt.KeyRAlt, vt.KeySlash}, "¿"}, {"AltGr-LBrace", []vt.Key{vt.KeyRAlt, vt.KeyLBrace}, "«"}, {"AltGr-Backslash", []vt.Key{vt.KeyRAlt, vt.KeyBackslash}, "¬"}, {"AltGr-RBrace", []vt.Key{vt.KeyRAlt, vt.KeyRBrace}, "»"}, {"AltGr-Quote", []vt.Key{vt.KeyRAlt, vt.KeyQuote}, "´"}, {"S-AltGr-0", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key0}, ""}, {"S-AltGr-1", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key1}, "¹"}, {"S-AltGr-2", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key2}, ""}, {"S-AltGr-3", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key3}, ""}, {"S-AltGr-4", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key4}, "£"}, {"S-AltGr-5", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key5}, ""}, {"S-AltGr-6", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key6}, ""}, {"S-AltGr-7", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key7}, ""}, {"S-AltGr-8", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key8}, ""}, {"S-AltGr-9", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.Key9}, ""}, {"S-AltGr-A", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyA}, "Á"}, {"S-AltGr-B", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyB}, ""}, {"S-AltGr-C", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyC}, "¢"}, {"S-AltGr-D", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyD}, "Ð"}, {"S-AltGr-E", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyE}, "É"}, {"S-AltGr-F", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyF}, ""}, {"S-AltGr-G", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyG}, ""}, {"S-AltGr-H", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyH}, ""}, {"S-AltGr-I", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyI}, "Í"}, {"S-AltGr-J", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyJ}, ""}, {"S-AltGr-K", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyK}, ""}, {"S-AltGr-L", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyL}, "Ø"}, {"S-AltGr-M", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyM}, ""}, {"S-AltGr-N", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyN}, "Ñ"}, {"S-AltGr-O", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyO}, "Ó"}, {"S-AltGr-P", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyP}, "Ö"}, {"S-AltGr-Q", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyQ}, "Ä"}, {"S-AltGr-R", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyR}, ""}, {"S-AltGr-S", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyS}, "§"}, {"S-AltGr-T", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyT}, "Þ"}, {"S-AltGr-U", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyU}, "Ú"}, {"S-AltGr-V", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyV}, ""}, {"S-AltGr-W", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyW}, "Å"}, {"S-AltGr-X", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyX}, ""}, {"S-AltGr-Y", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyY}, "Ü"}, {"S-AltGr-Z", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyZ}, "Æ"}, {"S-AltGr-Semi", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeySemi}, "°"}, {"S-AltGr-Equal", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyEqual}, "÷"}, {"S-AltGr-Comma", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyComma}, "Ç"}, {"S-AltGr-Minus", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyMinus}, ""}, {"S-AltGr-Slash", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeySlash}, ""}, {"S-AltGr-LBrace", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyLBrace}, ""}, {"S-AltGr-RBrace", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyRBrace}, ""}, {"S-AltGr-BackSlash", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyBackslash}, "¦"}, {"S-AltGr-Quote", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyQuote}, "¨"}, {"S-AltGr-IsoBackSlash", []vt.Key{vt.KeyRAlt, vt.KeyLShift, vt.KeyIsoBackSlash}, ""}, {"Alt-Ctrl-0", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key0}, "’"}, {"Alt-Ctrl-1", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key1}, "¡"}, {"Alt-Ctrl-2", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key2}, "²"}, {"Alt-Ctrl-3", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key3}, "³"}, {"Alt-Ctrl-4", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key4}, "¤"}, {"Alt-Ctrl-5", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key5}, "€"}, {"Alt-Ctrl-6", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key6}, "¼"}, {"Alt-Ctrl-7", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key7}, "½"}, {"Alt-Ctrl-8", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key8}, "¾"}, {"Alt-Ctrl-9", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.Key9}, "‘"}, {"Alt-Ctrl-A", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyA}, "á"}, {"Alt-Ctrl-C", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyC}, "©"}, {"Alt-Ctrl-D", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyD}, "ð"}, {"Alt-Ctrl-E", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyE}, "é"}, {"Alt-Ctrl-I", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyI}, "í"}, {"Alt-Ctrl-L", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyL}, "ø"}, {"Alt-Ctrl-M", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyM}, "µ"}, {"Alt-Ctrl-N", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyN}, "ñ"}, {"Alt-Ctrl-O", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyO}, "ó"}, {"Alt-Ctrl-P", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyP}, "ö"}, {"Alt-Ctrl-Q", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyQ}, "ä"}, {"Alt-Ctrl-R", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyR}, "®"}, {"Alt-Ctrl-S", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyS}, "ß"}, {"Alt-Ctrl-T", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyT}, "þ"}, {"Alt-Ctrl-U", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyU}, "ú"}, {"Alt-Ctrl-W", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyW}, "å"}, {"Alt-Ctrl-Y", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyY}, "ü"}, {"Alt-Ctrl-Z", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyZ}, "æ"}, {"Alt-Ctrl-Semi", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeySemi}, "¶"}, {"Alt-Ctrl-Equal", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyEqual}, "×"}, {"Alt-Ctrl-Comma", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyComma}, "ç"}, {"Alt-Ctrl-Minus", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyMinus}, "¥"}, {"Alt-Ctrl-Slash", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeySlash}, "¿"}, {"Alt-Ctrl-LBrace", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLBrace}, "«"}, {"Alt-Ctrl-Backslash", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyBackslash}, "¬"}, {"Alt-Ctrl-RBrace", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyRBrace}, "»"}, {"Alt-Ctrl-Quote", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyQuote}, "´"}, {"S-Alt-Ctrl-0", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key0}, ""}, {"S-Alt-Ctrl-1", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key1}, "¹"}, {"S-Alt-Ctrl-2", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key2}, ""}, {"S-Alt-Ctrl-3", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key3}, ""}, {"S-Alt-Ctrl-4", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key4}, "£"}, {"S-Alt-Ctrl-5", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key5}, ""}, {"S-Alt-Ctrl-6", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key6}, ""}, {"S-Alt-Ctrl-7", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key7}, ""}, {"S-Alt-Ctrl-8", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key8}, ""}, {"S-Alt-Ctrl-9", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.Key9}, ""}, {"S-Alt-Ctrl-A", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyA}, "Á"}, {"S-Alt-Ctrl-B", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyB}, ""}, {"S-Alt-Ctrl-C", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyC}, "¢"}, {"S-Alt-Ctrl-D", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyD}, "Ð"}, {"S-Alt-Ctrl-E", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyE}, "É"}, {"S-Alt-Ctrl-F", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyF}, ""}, {"S-Alt-Ctrl-G", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyG}, ""}, {"S-Alt-Ctrl-H", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyH}, ""}, {"S-Alt-Ctrl-I", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyI}, "Í"}, {"S-Alt-Ctrl-J", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyJ}, ""}, {"S-Alt-Ctrl-K", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyK}, ""}, {"S-Alt-Ctrl-L", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyL}, "Ø"}, {"S-Alt-Ctrl-M", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyM}, ""}, {"S-Alt-Ctrl-N", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyN}, "Ñ"}, {"S-Alt-Ctrl-O", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyO}, "Ó"}, {"S-Alt-Ctrl-P", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyP}, "Ö"}, {"S-Alt-Ctrl-Q", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyQ}, "Ä"}, {"S-Alt-Ctrl-R", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyR}, ""}, {"S-Alt-Ctrl-S", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyS}, "§"}, {"S-Alt-Ctrl-T", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyT}, "Þ"}, {"S-Alt-Ctrl-U", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyU}, "Ú"}, {"S-Alt-Ctrl-V", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyV}, ""}, {"S-Alt-Ctrl-W", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyW}, "Å"}, {"S-Alt-Ctrl-X", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyX}, ""}, {"S-Alt-Ctrl-Y", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyY}, "Ü"}, {"S-Alt-Ctrl-Z", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyZ}, "Æ"}, {"S-Alt-Ctrl-Semi", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeySemi}, "°"}, {"S-Alt-Ctrl-Equal", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyEqual}, "÷"}, {"S-Alt-Ctrl-Comma", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyComma}, "Ç"}, {"S-Alt-Ctrl-Minus", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyMinus}, ""}, {"S-Alt-Ctrl-Slash", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeySlash}, ""}, {"S-Alt-Ctrl-LBrace", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyLBrace}, ""}, {"S-Alt-Ctrl-RBrace", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyRBrace}, ""}, {"S-Alt-Ctrl-BackSlash", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyBackslash}, "¦"}, {"S-Alt-Ctrl-Quote", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyQuote}, "¨"}, {"S-Alt-Ctrl-IsoBackSlash", []vt.Key{vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyIsoBackSlash}, ""}, } for i := range cases { t.Run(cases[i].name, func(t *testing.T) { term.KeyTap(cases[i].keys...) term.KeyTap(vt.KeySpace) // add a vanilla space to force output if empty CheckRead(t, term, cases[i].want+" ") }) } } func TestDeadKeys(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) term.SetLayout(vt.GetLayout("US International")) MustStart(t, term) term.KeyTap(vt.KeyGrave) term.KeyTap(vt.KeyA) want := "à" result := ReadF(t, term) VerifyF(t, want == result, "wrong read %q != %q", result, want) cases := []struct { name string keys []vt.Key key []vt.Key want string }{ {"Carat", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeySpace}, "^"}, {"Carat-a", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyA}, "â"}, {"Carat-e", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyE}, "ê"}, {"Carat-i", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyI}, "î"}, {"Carat-o", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyO}, "ô"}, {"Carat-u", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyU}, "û"}, {"Carat-A", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyLShift, vt.KeyA}, "Â"}, {"Carat-E", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyLShift, vt.KeyE}, "Ê"}, {"Carat-I", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyLShift, vt.KeyI}, "Î"}, {"Carat-O", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyLShift, vt.KeyO}, "Ô"}, {"Carat-U", []vt.Key{vt.KeyLShift, vt.Key6}, []vt.Key{vt.KeyLShift, vt.KeyU}, "Û"}, {"Acute", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeySpace}, "'"}, {"Acute-a", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyA}, "á"}, {"Acute-c", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyC}, "ç"}, {"Acute-e", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyE}, "é"}, {"Acute-i", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyI}, "í"}, {"Acute-o", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyO}, "ó"}, {"Acute-u", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyU}, "ú"}, {"Acute-y", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyY}, "ý"}, {"Acute-A", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyA}, "Á"}, {"Acute-C", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyC}, "Ç"}, {"Acute-E", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyE}, "É"}, {"Acute-I", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyI}, "Í"}, {"Acute-O", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyO}, "Ó"}, {"Acute-U", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyU}, "Ú"}, {"Acute-Y", []vt.Key{vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyY}, "Ý"}, {"Quotes", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeySpace}, "\""}, {"Quotes-a", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyA}, "ä"}, {"Quotes-e", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyE}, "ë"}, {"Quotes-i", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyI}, "ï"}, {"Quotes-o", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyO}, "ö"}, {"Quotes-u", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyU}, "ü"}, {"Quotes-y", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyY}, "ÿ"}, {"Quotes-A", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyA}, "Ä"}, {"Quotes-E", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyE}, "Ë"}, {"Quotes-I", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyI}, "Ï"}, {"Quotes-O", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyO}, "Ö"}, {"Quotes-U", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyU}, "Ü"}, {"Quotes-Y", []vt.Key{vt.KeyLShift, vt.KeyQuote}, []vt.Key{vt.KeyLShift, vt.KeyY}, "Ÿ"}, {"Grave", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeySpace}, "`"}, {"Grave-a", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyA}, "à"}, {"Grave-e", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyE}, "è"}, {"Grave-i", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyI}, "ì"}, {"Grave-o", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyO}, "ò"}, {"Grave-u", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyU}, "ù"}, {"Grave-A", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyA}, "À"}, {"Grave-E", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyE}, "È"}, {"Grave-I", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyI}, "Ì"}, {"Grave-O", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyO}, "Ò"}, {"Grave-U", []vt.Key{vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyU}, "Ù"}, {"Tilde-a", []vt.Key{vt.KeyLShift, vt.KeyGrave}, []vt.Key{vt.KeyA}, "ã"}, {"Tilde-n", []vt.Key{vt.KeyLShift, vt.KeyGrave}, []vt.Key{vt.KeyN}, "ñ"}, {"Tilde-o", []vt.Key{vt.KeyLShift, vt.KeyGrave}, []vt.Key{vt.KeyO}, "õ"}, {"Tilde-A", []vt.Key{vt.KeyLShift, vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyA}, "Ã"}, {"Tilde-N", []vt.Key{vt.KeyLShift, vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyN}, "Ñ"}, {"Tilde-O", []vt.Key{vt.KeyLShift, vt.KeyGrave}, []vt.Key{vt.KeyLShift, vt.KeyO}, "Õ"}, } for i := range cases { t.Run(cases[i].name, func(t *testing.T) { term.KeyTap(cases[i].keys...) term.KeyTap(cases[i].key...) term.KeyTap(vt.KeySpace) // add a vanilla space to force output if empty CheckRead(t, term, cases[i].want+" ") }) } } func TestUsCapsLock(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) term.SetLayout(vt.GetLayout("US International")) MustStart(t, term) cases := []struct { name string keys []vt.Key caps string base string }{ {"A", []vt.Key{vt.KeyA}, "A", "a"}, {"B", []vt.Key{vt.KeyB}, "B", "b"}, {"1", []vt.Key{vt.Key1}, "1", "!"}, {"Z", []vt.Key{vt.KeyZ}, "Z", "z"}, {"plus", []vt.Key{vt.KeyEqual}, "=", "+"}, } for i := range cases { t.Run(cases[i].name, func(t *testing.T) { // set caps lock term.KeyTap(vt.KeyCapsLock) term.KeyTap(cases[i].keys...) term.KeyTap(vt.KeySpace) CheckRead(t, term, cases[i].caps+" ") term.KeyTap(append([]vt.Key{vt.KeyLShift}, cases[i].keys...)...) term.KeyTap(vt.KeySpace) CheckRead(t, term, cases[i].base+" ") // release caps lock term.KeyTap(vt.KeyCapsLock) }) } } func TestUsNumLock(t *testing.T) { term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4}) defer MustClose(t, term) term.SetLayout(vt.GetLayout("US International")) MustStart(t, term) cases := []struct { name string keys []vt.Key lock string app string num string }{ {"1", []vt.Key{vt.KeyPad1}, "1", "\x1bOF", "1"}, {"2", []vt.Key{vt.KeyPad2}, "2", "\x1b[B", "2"}, {"3", []vt.Key{vt.KeyPad3}, "3", "\x1b[6~", "3"}, {"4", []vt.Key{vt.KeyPad4}, "4", "\x1b[D", "4"}, {"5", []vt.Key{vt.KeyPad5}, "5", "\x1b[E", "5"}, {"6", []vt.Key{vt.KeyPad6}, "6", "\x1b[C", "6"}, {"7", []vt.Key{vt.KeyPad7}, "7", "\x1bOH", "7"}, {"8", []vt.Key{vt.KeyPad8}, "8", "\x1b[A", "8"}, {"9", []vt.Key{vt.KeyPad9}, "9", "\x1b[5~", "9"}, {"0", []vt.Key{vt.KeyPad0}, "0", "\x1b[2~", "0"}, {"dec", []vt.Key{vt.KeyPadDec}, ".", "\x1b[3~", "."}, {"add", []vt.Key{vt.KeyPadAdd}, "+", "\x1bOk", "+"}, {"sub", []vt.Key{vt.KeyPadSub}, "-", "\x1bOm", "-"}, {"mul", []vt.Key{vt.KeyPadMul}, "*", "\x1bOj", "*"}, {"div", []vt.Key{vt.KeyPadDiv}, "/", "\x1bOo", "/"}, {"equal", []vt.Key{vt.KeyPadEqual}, "=", "\x1bOX", "="}, {"enter", []vt.Key{vt.KeyPadEnter}, "\r", "\x1bOM", "\r"}, } for i := range cases { t.Run(cases[i].name, func(t *testing.T) { // set num lock term.KeyTap(vt.KeyNumLock) // set application mode WriteF(t, term, "\x1b=") term.KeyTap(cases[i].keys...) term.KeyTap(vt.KeySpace) CheckRead(t, term, cases[i].lock+" ") // clear application mode - should not change (NumLock takes precedence) WriteF(t, term, "\x1b>") term.KeyTap(cases[i].keys...) term.KeyTap(vt.KeySpace) CheckRead(t, term, cases[i].lock+" ") // release num lock term.KeyTap(vt.KeyNumLock) // set application mode WriteF(t, term, "\x1b=") term.KeyTap(cases[i].keys...) term.KeyTap(vt.KeySpace) CheckRead(t, term, cases[i].app+" ") WriteF(t, term, "\x1b>") term.KeyTap(cases[i].keys...) term.KeyTap(vt.KeySpace) CheckRead(t, term, cases[i].num+" ") }) } } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/tests/util.go000066400000000000000000000073361520475227200233460ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tests import ( "fmt" "testing" "github.com/gdamore/tcell/v3/color" "github.com/gdamore/tcell/v3/vt" ) type MockTerm = vt.MockTerm type Row = vt.Row type Col = vt.Col type Coord = vt.Coord type Attr = vt.Attr // WriteF writes the string, and ensures it is fully flushed // before returning. func WriteF(t *testing.T, term MockTerm, str string, args ...any) { t.Helper() b := fmt.Appendf(nil, str, args...) for len(b) != 0 { if n, err := term.Write(b); err != nil { t.Fatalf("Failed to write: %v", err) } else { b = b[n:] } } if err := term.Drain(); err != nil { t.Fatalf("Failed to flush: %v", err) } } // ReadF reads content from the term and returns it as a string. func ReadF(t *testing.T, term MockTerm) string { t.Helper() buf := make([]byte, 128) n, err := term.Read(buf) if err != nil { t.Errorf("failed read: %v", err) return "" } return string(buf[:n]) } // VerifyF validates the condition, printing the message on failure. func VerifyF(t *testing.T, b bool, fmt string, args ...any) { t.Helper() if !b { t.Errorf("validation failure: "+fmt, args...) } } // AssertF validates the condition, and aborts the test if it fails. func AssertF(t *testing.T, b bool, fmt string, args ...any) { t.Helper() if !b { t.Fatalf("validation failure: "+fmt, args...) } } // MustClose closes or calls Fatalf. func MustClose(t *testing.T, term MockTerm) { t.Helper() err := term.Close() AssertF(t, err == nil, "close failed: %v", err) } // MustStart starts the terminal or calls Fatalf. func MustStart(t *testing.T, term MockTerm) { t.Helper() err := term.Start() AssertF(t, err == nil, "start failed: %v", err) } // CheckPos is verifies the current cursor position of terminal. func CheckPos(t *testing.T, term MockTerm, x Col, y Row) { t.Helper() VerifyF(t, term.Pos().X == x && term.Pos().Y == y, "bad position %d,%d (expected %d,%d)", term.Pos().X, term.Pos().Y, x, y) } // CheckContent verifies the content at a given cell of the terminal. func CheckContent(t *testing.T, term MockTerm, x Col, y Row, s string) { t.Helper() if actual := string(term.GetCell(Coord{X: x, Y: y}).C); actual != s { t.Errorf("bad content %d,%d (expected %q got %q)", x, y, s, actual) } } // CheckAttrs verifies the attributes of a given cell. func CheckAttrs(t *testing.T, term MockTerm, x Col, y Row, a Attr) { t.Helper() if actual := term.GetCell(Coord{X: x, Y: y}).S.Attr(); actual != a { t.Errorf("bad attr %d,%d (expected %x got %x)", x, y, a, actual) } } // CheckColors verifies the colors of a given cell. func CheckColors(t *testing.T, term MockTerm, x Col, y Row, fg color.Color, bg color.Color) { t.Helper() if actual := term.GetCell(Coord{X: x, Y: y}).S.Fg(); actual != fg { t.Errorf("bad foreground %d,%d (expected %s got %s)", x, y, fg.String(), actual.String()) } if actual := term.GetCell(Coord{X: x, Y: y}).S.Bg(); actual != bg { t.Errorf("bad background %d,%d (expected %s got %s)", x, y, bg.String(), actual.String()) } } // CheckRead verifies that a read matches. func CheckRead(t *testing.T, term MockTerm, want string) { t.Helper() result := ReadF(t, term) VerifyF(t, want == result, "wrong read %q != %q", result, want) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/vt.go000066400000000000000000000016631520475227200216550ustar00rootroot00000000000000// Copyright 2025 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package vt provides common definitions for VT derived terminals and applications. // This includes the venerable VT100, XTerm, and newer emulators such as Kitty and // the Windows Terminal. // // This package is still under development and direct access to any of the interfaces // here is not guaranteed to be stable yet. Caveat emptor. package vt golang-github-gdamore-tcell.v3-3.4.0+dfsg/vt/width.go000066400000000000000000000013021520475227200223310ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vt import "github.com/gdamore/tcell/v3/internal/widthutil" var textWidthOptions = widthutil.Options() golang-github-gdamore-tcell.v3-3.4.0+dfsg/width_bench_test.go000066400000000000000000000027201520475227200241030ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tcell import ( "strings" "testing" "github.com/clipperhouse/displaywidth" ) var ( mixedWidthText = strings.Repeat("Hello, 世界 👩‍🚀 e\u0301 — π «ambiguous» ", 8) ansiWidthText = strings.Repeat("\x1b[31mHello\x1b[0m, 世界 👩‍🚀 e\u0301 \x1b]0;title\x07 ", 8) ) func BenchmarkStringWidthMixedCurrent(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = currentMixedWidth(mixedWidthText) } } func BenchmarkStringWidthANSIControl(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = currentANSIWidth(ansiWidthText) } } func currentMixedWidth(s string) int { return displaywidth.Options{EastAsianWidth: true}.String(s) } func currentANSIWidth(s string) int { return displaywidth.Options{ EastAsianWidth: true, ControlSequences: true, ControlSequences8Bit: true, }.String(s) } golang-github-gdamore-tcell.v3-3.4.0+dfsg/wscreen.go000066400000000000000000000107251520475227200222400ustar00rootroot00000000000000// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build js && wasm // +build js,wasm package tcell import ( "errors" "io" "sync" "syscall/js" "github.com/gdamore/tcell/v3/tty" ) // initialize installs the browser-backed TTY used by tScreen on js/wasm. func (t *tScreen) initialize() error { if t.tty == nil { t.tty = newBrowserTty() } if t.term == "" { t.term = "ghostty-truecolor" } return nil } func getCharset() string { return "UTF-8" } type browserTty struct { mu sync.Mutex cond *sync.Cond started bool drained bool closed bool input []byte resizeQ chan<- bool writeFunc js.Value sizeFunc js.Value closeFuncs []js.Func } func newBrowserTty() *browserTty { t := &browserTty{} t.cond = sync.NewCond(&t.mu) return t } func (t *browserTty) Start() error { t.mu.Lock() defer t.mu.Unlock() if t.started { return nil } global := js.Global() t.writeFunc = global.Get("tcellWrite") t.sizeFunc = global.Get("tcellWindowSize") if t.writeFunc.Type() != js.TypeFunction || t.sizeFunc.Type() != js.TypeFunction { return errors.New("tcell wasm terminal host is not installed") } onData := js.FuncOf(func(this js.Value, args []js.Value) any { if len(args) == 0 { return nil } if args[0].InstanceOf(global.Get("Uint8Array")) { data := make([]byte, args[0].Get("byteLength").Int()) js.CopyBytesToGo(data, args[0]) t.enqueue(data) } else { t.enqueue([]byte(args[0].String())) } return nil }) onResize := js.FuncOf(func(this js.Value, args []js.Value) any { t.mu.Lock() resizeQ := t.resizeQ t.mu.Unlock() if resizeQ != nil { select { case resizeQ <- true: default: } } return nil }) t.closeFuncs = []js.Func{onData, onResize} global.Set("tcellRead", onData) global.Set("tcellResize", onResize) t.started = true t.drained = false t.closed = false return nil } func (t *browserTty) Stop() error { t.mu.Lock() t.started = false t.drained = false funcs := t.closeFuncs t.closeFuncs = nil t.cond.Broadcast() t.mu.Unlock() js.Global().Set("tcellRead", js.Undefined()) js.Global().Set("tcellResize", js.Undefined()) for _, fn := range funcs { fn.Release() } return nil } func (t *browserTty) Drain() error { t.mu.Lock() t.input = nil t.drained = true t.cond.Broadcast() t.mu.Unlock() return nil } func (t *browserTty) NotifyResize(resizeQ chan<- bool) { t.mu.Lock() t.resizeQ = resizeQ t.mu.Unlock() } func (t *browserTty) WindowSize() (tty.WindowSize, error) { var ws tty.WindowSize t.mu.Lock() sizeFunc := t.sizeFunc t.mu.Unlock() if sizeFunc.Type() != js.TypeFunction { ws.Width = 80 ws.Height = 24 return ws, nil } size := sizeFunc.Invoke() ws.Width = size.Get("cols").Int() ws.Height = size.Get("rows").Int() ws.PixelWidth = size.Get("pixelWidth").Int() ws.PixelHeight = size.Get("pixelHeight").Int() if ws.Width == 0 { ws.Width = 80 } if ws.Height == 0 { ws.Height = 24 } return ws, nil } func (t *browserTty) Read(b []byte) (int, error) { t.mu.Lock() defer t.mu.Unlock() for len(t.input) == 0 && t.started && !t.drained && !t.closed { t.cond.Wait() } if t.closed { return 0, io.EOF } if (!t.started || t.drained) && len(t.input) == 0 { return 0, io.EOF } n := copy(b, t.input) t.input = t.input[n:] return n, nil } func (t *browserTty) Write(b []byte) (int, error) { t.mu.Lock() writeFunc := t.writeFunc started := t.started t.mu.Unlock() if !started || writeFunc.Type() != js.TypeFunction { return 0, io.ErrClosedPipe } data := js.Global().Get("Uint8Array").New(len(b)) js.CopyBytesToJS(data, b) writeFunc.Invoke(data) return len(b), nil } func (t *browserTty) Close() error { t.mu.Lock() if t.closed { t.mu.Unlock() return nil } t.closed = true t.started = false t.drained = false t.cond.Broadcast() t.mu.Unlock() return t.Stop() } func (t *browserTty) enqueue(data []byte) { t.mu.Lock() if t.started && !t.closed { t.input = append(t.input, data...) t.cond.Broadcast() } t.mu.Unlock() }