Compare commits

..
366 Commits
Author SHA1 Message Date
Daniel Imms 2e02c37e52 Merge pull request #4756 from Tyriar/tyriar/5_3__2
Disable image addon publishing
2023-09-07 10:58:11 -07:00
Daniel Imms 71ee3c7a33 Disable image addon publishing 2023-09-07 10:57:21 -07:00
Daniel Imms 009add2b4a Merge pull request #4755 from Tyriar/tyriar/5_3
v5.3.0 and bump all addons
2023-09-07 10:54:11 -07:00
Daniel Imms fb4b662756 v5.3.0 and bump all addons 2023-09-07 10:46:35 -07:00
Daniel Imms 7b52b8ee45 Merge pull request #4754 from tisilent/DomRenderer-Focus-Cursor
When DomRenderer focus,render the cursor line.
2023-09-07 08:53:26 -07:00
tisilent c03cb2b016 When DomRenderer focus,render the cursor line. 2023-09-07 23:44:36 +08:00
Daniel Imms 29dd30a98d Merge pull request #4735 from Tyriar/playwright
Start @playwright/test migration
2023-09-03 07:19:20 -07:00
Daniel Imms 68c572345b Remove debugger statement 2023-09-02 09:19:17 -07:00
Daniel Imms 5e628d7fc6 Use arrow functions 2023-09-02 09:11:15 -07:00
Daniel Imms 93aae670ba Readme explaining test/api 2023-09-02 08:56:59 -07:00
Daniel Imms 7c7ac86681 Run inner pwsh to unzip on Winodws 2023-09-02 08:53:20 -07:00
Daniel Imms 076ab0f49b Combine unzip and ls -R 2023-09-02 08:44:32 -07:00
Daniel Imms 5fc1c04bd6 Use bash on all OS' 2023-09-02 08:41:59 -07:00
Daniel Imms 525d498e9f Fix unzip on Windows 2023-09-02 08:40:57 -07:00
Daniel Imms 2b5ebb2b7b Use unified unzip on unit tests 2023-09-02 08:33:01 -07:00
Daniel Imms 2269e92c88 Fix syntax 2023-09-02 08:30:51 -07:00
Daniel Imms 7c434a0566 Merge unzip steps 2023-09-02 08:30:14 -07:00
Daniel Imms 31bbeca2d8 Add webkit 2023-09-02 08:26:46 -07:00
Daniel Imms 158f7a17a6 Use list reporter 2023-09-02 08:26:42 -07:00
Daniel Imms 8d0fde2d7f Remove only 2023-09-02 08:22:56 -07:00
Daniel Imms 4cd4597082 Browser-specific playwright job 2023-09-02 08:19:59 -07:00
Daniel Imms 1624854bd7 Migrate Terminal api tests to playwright 2023-09-02 08:18:02 -07:00
Daniel Imms c099cb2a3d Add browser-specific npm scripts 2023-09-02 08:17:15 -07:00
Daniel Imms 1c57b98c9a Migrate InputHandler api tests to playwright 2023-09-02 07:19:32 -07:00
Daniel Imms a29ab32876 Migrate MouseTracking api tests to playwright 2023-09-02 07:10:47 -07:00
Daniel Imms 91456fff04 More proxy impl 2023-09-02 06:06:51 -07:00
Daniel Imms 13af55f9fc Add onWriteParsed event to proxy 2023-09-02 06:00:33 -07:00
Daniel Imms 66454d378b Add todos 2023-09-01 08:50:39 -07:00
Daniel Imms 92d09f2ea1 Clean up, fix errors, add todo 2023-09-01 08:50:03 -07:00
Daniel Imms fac5e5f02d Improve types of proxy object to extend base interfaces 2023-09-01 08:46:14 -07:00
Daniel Imms 4ed97cae3a Merge branch 'master' into playwright 2023-09-01 07:46:35 -07:00
Daniel Imms 7bd9f8ee38 Revert image addon options type name 2023-08-28 02:52:03 -07:00
Daniel Imms 2dd4dfda80 Merge pull request #4729 from tisilent/circle-line-custom-powerline-glyph
Update circle line custom powerline glyph.
2023-08-28 02:28:41 -07:00
Daniel Imms f7ac0d878b Center new semi-circle line glyphs 2023-08-28 02:20:31 -07:00
Daniel Imms fe2723b585 Merge branch 'master' into circle-line-custom-powerline-glyph 2023-08-28 02:13:56 -07:00
tisilent 7fecec6599 Offset outward 2023-08-28 16:54:27 +08:00
Daniel Imms 5b25c65521 Migrate Parser tests to playwright/test 2023-08-27 09:11:47 -07:00
Daniel Imms cb977ab6b6 Remove CharWidth.test.ts from api (in playwright) 2023-08-27 09:03:55 -07:00
Daniel Imms 2b21f27ac2 Fix demo build script 2023-08-27 09:00:48 -07:00
Daniel Imms ec984d25db Install --with-deps 2023-08-27 08:56:40 -07:00
Daniel Imms 7000447915 Build demo step 2023-08-27 08:54:04 -07:00
Daniel Imms 486d61736b Start demo 2023-08-27 08:50:24 -07:00
Daniel Imms a4683ab874 dist -> /dist 2023-08-27 08:44:24 -07:00
Daniel Imms b26c53d7e2 Make client dist a build artifact 2023-08-27 08:28:23 -07:00
Daniel Imms 86b8744838 Install playwright deps 2023-08-27 08:22:54 -07:00
Daniel Imms 6f04e00ea1 Add test-playwright-parallel job 2023-08-27 08:16:53 -07:00
Daniel Imms 9eadaf7079 Feature detect WebAssembly instead 2023-08-27 08:10:58 -07:00
Daniel Imms fbebabe2e6 Add @playwright/test support 2023-08-27 08:08:52 -07:00
Daniel Imms 48184cc94f Merge pull request #4734 from xtermjs/coverage
Bring back unit test coverage report
2023-08-27 07:49:05 -07:00
Daniel Imms 923c28cb77 Use node 18 in coverage 2023-08-27 07:43:11 -07:00
Daniel Imms 03c1906f13 Have coverage depend on build 2023-08-27 07:33:46 -07:00
Daniel Imms 2b7b4d6fea Upgrade playwright 2023-08-27 07:31:20 -07:00
Daniel Imms abae7b6210 Merge pull request #4733 from Tyriar/mutable
Adopt MutableDisposable for most usages of pattern `<var>?.dispose(); <var> = undefined`
2023-08-27 07:29:02 -07:00
Daniel Imms 12a862c277 Bring back unit test coverage report 2023-08-27 07:28:04 -07:00
Daniel Imms 38c6af9b46 Fix _renderer access in api test 2023-08-27 07:22:08 -07:00
Daniel Imms 2eaa9b3b69 Fix remaining issues with MutableDisposable usage 2023-08-27 07:15:51 -07:00
Daniel Imms 37b0e4018c More adoption of MutableDisposable 2023-08-27 07:03:27 -07:00
Daniel Imms 57db72d287 More adoption of MutableDisposable 2023-08-27 06:55:16 -07:00
Daniel Imms ec02bab97c Use MutableDisposable in CursorRenderLayer 2023-08-27 06:46:49 -07:00
Daniel Imms 94ed3e800a Don't dispose of texture atlas in canvas addon
This would cause problems if the atlas is shared between multiple
terminals.
2023-08-27 06:44:48 -07:00
Daniel Imms 1e1ccba238 Use MutableDisposable in BaseRenderLayer 2023-08-27 06:44:11 -07:00
Daniel Imms a0d0c923f3 Merge pull request #4732 from Tyriar/texture_utilization
Improve texture atlas utilization and fix glyph corruption when merging
2023-08-26 15:46:52 -07:00
Daniel Imms aba16687b0 Fix high pressure texture atlas corruption
The corruption was caused by confusion with page indexes when
merging pages. The fix is to simplify it by deleting all pages
and then just adding the new page.

Fixes #4534
Fixes #4351
2023-08-26 10:23:09 -07:00
Daniel Imms 797a71695f Improve texture atlas utilization
- Changed the threshold at which pages start to merge from MAX/2 to
  MAX.
- If a glyph is about to create a new page that will be merged,
  check if the existing row fits when ignoring the row pixel
  threshold. This will improve texture utilization by using the
  available space before the page is merged and becomes static.
2023-08-26 09:37:08 -07:00
Daniel Imms 55c5a8560c Merge pull request #4727 from jerch/fix_4720
remove inwasm burden
2023-08-25 07:50:45 -07:00
tisilent 9e768a4c7c Update circle line custom powerline glyph. 2023-08-25 18:37:48 +08:00
jerch d367eb41d2 Merge branch 'master' into fix_4720 2023-08-24 17:32:08 +02:00
Jörg Breitbart 7c164efdcc fix CI scripts 2023-08-24 17:30:52 +02:00
Jörg Breitbart bd76cdbd20 resort wasm to xterm-wasm-parts 2023-08-24 17:24:25 +02:00
Daniel Imms 40e51aba6b Merge pull request #4726 from Tyriar/no_confusing
Warn on eslint rule @typescript-eslint/no-confusing-void-expression
2023-08-24 06:41:18 -07:00
Daniel Imms f9dc26d413 Merge branch 'master' into no_confusing 2023-08-24 06:33:43 -07:00
Daniel Imms 2fe082beea Warn on eslint rule @typescript-eslint/no-confusing-void-expression 2023-08-24 06:33:11 -07:00
Daniel Imms 2835976b81 Merge pull request #4725 from Tyriar/no_useless_ctor
Warn on eslint rule @typescript-eslint/no-useless-constructor
2023-08-24 06:30:59 -07:00
Daniel Imms 1968e8ced4 Merge branch 'master' into no_useless_ctor 2023-08-24 06:24:54 -07:00
Daniel Imms 1965daff50 Warn on eslint rule @typescript-eslint/no-useless-constructor 2023-08-24 06:24:25 -07:00
Daniel Imms 0e3db591c0 Merge pull request #4724 from Tyriar/webpack_eslint
@ts-check webpack files, fix vscode language of eslint file
2023-08-23 16:50:23 -07:00
Daniel Imms 224902af9f Merge branch 'master' into webpack_eslint 2023-08-23 16:37:37 -07:00
Daniel Imms 131f578367 Merge pull request #4723 from Tyriar/demo_tscheck
Use ts-check in server and start
2023-08-23 16:37:29 -07:00
Daniel Imms 6b794285c6 Remove esbuild
Accidentally added
2023-08-23 16:32:22 -07:00
Daniel Imms e78f85bf65 ts-check webpack configs 2023-08-23 16:29:45 -07:00
Daniel Imms 9be0b6d918 Force eslintrc to use jsonc language 2023-08-23 16:29:38 -07:00
Daniel Imms 3aa31111de Merge branch 'master' into demo_tscheck 2023-08-23 16:26:07 -07:00
Daniel Imms 81baad6acd Prefer const 2023-08-23 16:26:00 -07:00
Daniel Imms 25ef47a78f Use express-ws types 2023-08-23 16:25:15 -07:00
Daniel Imms 7d7d593fee Merge pull request #4722 from Tyriar/client_warnings
Fix demo client.ts warnings
2023-08-23 16:22:28 -07:00
Daniel Imms 766ffde4ca Merge pull request #4721 from Tyriar/complex_options
Fix demo warnings by adding complex options
2023-08-23 16:22:20 -07:00
Daniel Imms 4534b56c1f Use ts-check in server and start 2023-08-23 16:20:55 -07:00
Daniel Imms ee0da5e4c1 Fix demo client.ts warnings 2023-08-23 16:03:25 -07:00
Daniel Imms e5d60c0895 Fix demo warnings by adding complex options 2023-08-23 16:00:16 -07:00
Daniel Imms 2268ae3967 Merge pull request #4718 from Tyriar/no_unused
Add no unused variables lint warning
2023-08-23 04:57:55 -07:00
Daniel Imms 921da1a2ea Ignore no-unused-vars in .api, organize all remaining imports 2023-08-23 04:47:53 -07:00
Daniel Imms ebd4092bee Ignore no-unused-vars in args 2023-08-23 04:42:44 -07:00
Daniel Imms b6fd7a2b93 Ignore no-unused-vars in test files 2023-08-23 04:42:21 -07:00
Daniel Imms 210ba8109c Add no unused variables lint warning
This shows a warning while compiling, so demo bundling will still happen but
it will be rejected when a PR is made.
2023-08-23 04:34:55 -07:00
Daniel Imms 317ccdac66 Merge pull request #4716 from xtermjs/Tyriar-patch-1
Fix typo in ci.yml
2023-08-22 07:33:02 -07:00
Daniel Imms 43a3251bee Fix typo in ci.yml 2023-08-22 07:32:51 -07:00
Daniel Imms 94ab56542f Merge pull request #4715 from Tyriar/waiton
Parallelize required tests and build jobs
2023-08-22 07:31:25 -07:00
Daniel Imms 92f1881dfe Polish job names
This matches the npm script names closer
2023-08-22 07:21:27 -07:00
Daniel Imms ec4c018f73 Remove windows from fast 2023-08-22 07:15:02 -07:00
Daniel Imms 2c25dd1970 Fix invalid names 2023-08-22 07:14:11 -07:00
Daniel Imms 06fbe69b09 Split out integration tests too 2023-08-22 07:05:55 -07:00
Daniel Imms 4bd154e7dc Split fast and slow unit tests to free agents 2023-08-22 07:04:12 -07:00
Daniel Imms 516756fad8 Fix install step 2023-08-22 07:01:27 -07:00
Daniel Imms 2b3cfb39a1 Merge branch 'master' into waiton 2023-08-22 06:58:01 -07:00
Daniel Imms e9cbb3e547 Add job name 2023-08-22 06:57:20 -07:00
Daniel Imms 1744801867 Try parallelize tests and build 2023-08-22 06:52:57 -07:00
Daniel Imms 64cb9a3c62 Merge pull request #4714 from Tyriar/4713
Don't smooth scroll when it's a no-op
2023-08-22 06:49:52 -07:00
Daniel Imms 0e947788e0 Don't smooth scroll when it's a no-op
Fixes #4713
2023-08-22 06:39:59 -07:00
Daniel Imms 60b59db94f Merge pull request #4709 from Tyriar/tyriar/logger
Fix error that can occur when loading canvas addon
2023-08-22 06:27:06 -07:00
Daniel Imms 6d7032a906 Merge pull request #4710 from Tyriar/4704
Move addon install to presetup
2023-08-22 06:23:10 -07:00
Daniel Imms b452fc9e85 Merge pull request #4712 from Tyriar/timeout
Put reasonable GH action timeouts in place
2023-08-22 06:22:55 -07:00
Daniel Imms dac4dec0ad Put reasonable GH action timeouts in place
Fixes #4711
2023-08-22 06:13:52 -07:00
Daniel Imms eae79e93f3 Install addons in CI 2023-08-22 06:11:54 -07:00
Daniel Imms ee33765932 Move addon install to presetup
Fixes #4704
2023-08-22 06:04:33 -07:00
Daniel Imms 8af83fef85 Fix error that can occur when loading canvas addon 2023-08-22 06:00:44 -07:00
Daniel Imms 6f0a5c9f0c Merge pull request #4695 from PerBothner/demo
demo: auto-resize and other tweaks
2023-08-20 09:31:06 -07:00
Daniel Imms 1529e0bf04 Remove unused variable 2023-08-20 07:33:15 -07:00
Daniel Imms 6021e6cf81 Merge branch 'master' into demo 2023-08-20 07:29:31 -07:00
Daniel Imms 427d33c7b7 Merge pull request #4700 from Tyriar/ci_concise
Make GH actions strategy more concise
2023-08-19 15:41:37 -07:00
Daniel Imms 95f332cb13 Merge pull request #4699 from Tyriar/slow_tests
Improve slow tests
2023-08-19 15:34:08 -07:00
Daniel Imms db4567599b Make GH actions strategy more concise 2023-08-19 15:28:44 -07:00
Daniel Imms 0667c7aedd Merge pull request #4698 from Tyriar/codeql
Revert codeql file name change
2023-08-19 15:22:46 -07:00
Daniel Imms 1c5596de66 Split up Base64Decoder decoding tests
Fixes #4696
2023-08-19 15:21:42 -07:00
Daniel Imms 905707284d Up parser unti test timeout 10s -> 30s 2023-08-19 15:18:44 -07:00
Daniel Imms 67f46a54b8 Split up StringToUtf32 decoder tests
Part of #4696
2023-08-19 15:17:11 -07:00
Daniel Imms b8db3853f1 Merge pull request #4697 from xtermjs/Tyriar-patch-1
Remove Azure Pipelines badge
2023-08-19 15:07:15 -07:00
Daniel Imms a9015c0ee6 Revert codeql file name change 2023-08-19 15:05:32 -07:00
Daniel Imms 60db1e7f82 Remove Azure Pipelines badge 2023-08-19 14:57:25 -07:00
Daniel Imms 6dae811dae Migrate to GitHub Actions
Fixes #3297
2023-08-19 14:47:35 -07:00
Per Bothner a20599efb4 Merge branch 'master' into demo 2023-08-19 09:15:30 -07:00
jerch 27b8d73f66 Merge pull request #4694 from jerch/fix_bce_demo
fix spaces in bce demo
2023-08-19 17:43:36 +02:00
Per Bothner 025e8505d5 implement autoResize for terminal in demo
This is optional (but the default): You can still explicitly set COLSxROWS,
but "auto" uses a ResizeObserver sets the size based on window size.
2023-08-19 08:28:43 -07:00
Jörg Breitbart 16f5900252 fix spaces in demo 2023-08-19 17:28:27 +02:00
Per Bothner e2d699cb32 Styling tweaks for demo
* Make terminal take 2/3 of the width, and the options etc get 1/3
* Remove options height limit, which could lead to annoying nested
  verical scroll regions.  (An inner region for the options inside a
  scrollable viewport.)
2023-08-19 08:23:31 -07:00
Daniel Imms 156f75628a Merge pull request #4692 from Tyriar/2117
Stub out all CSI api tests
2023-08-19 06:25:57 -07:00
Daniel Imms 8fad65d1d5 Stub out all CSI tests
Part of #2117
2023-08-19 06:11:29 -07:00
Daniel Imms 0867804504 Prefer arrow function 2023-08-19 05:33:49 -07:00
Daniel Imms f0900fdc5f Tweak format of CSI test names to make it easier to see terminating char 2023-08-19 05:32:41 -07:00
Daniel Imms 0be744b567 Merge pull request #4691 from Tyriar/4690
Improve lifecycle of WebLinksAddon.api, test all country tlds
2023-08-19 05:22:14 -07:00
Daniel Imms 243b0bb626 Add all country tlds to tests 2023-08-19 05:07:41 -07:00
Daniel Imms 0b32b42134 Improve lifecycle of WebLinksAddon.api
Part of #4690
2023-08-19 05:00:45 -07:00
Daniel Imms ef1cd4318b Merge pull request #4688 from Tyriar/cursor_style
Simplify cursor style types in webgl
2023-08-19 04:51:15 -07:00
Per Bothner 6e39d03a52 Some html/css cleanps in demo
* id="grid" should be id="class" (since "grid" is not unique in the document).
* Remove space after 'id=' (multiple places).
* Group 3 texture-atlas elements in new <div class="texture-atlas-container">.
2023-08-18 16:30:36 -07:00
Daniel Imms 7b7dbd36d6 Merge pull request #4689 from Tyriar/4685
Track CursorBlinkStateManager in MutableDisposable
2023-08-18 14:38:05 -07:00
Daniel Imms d03b5c9481 Track CursorBlinkStateManager in MutableDisposable
Fixes #4685
2023-08-18 14:26:56 -07:00
Daniel Imms 93e060aeb2 Add MutableDisposable tests 2023-08-18 14:24:50 -07:00
Daniel Imms a84351ad0c Add MutableDisposable
Part of #4685
2023-08-18 14:15:56 -07:00
Daniel Imms 25a78c77f2 Simplify cursor style types in webgl
Fixes #4686
2023-08-18 14:11:17 -07:00
Daniel Imms a35fa611ff Merge pull request #4687 from Tyriar/tracing
Add trace log level, @traceCall decorator and use in webgl
2023-08-18 11:14:52 -07:00
Daniel Imms 0b54c9a5d6 Add trace log level, traceCall decorator and use in webgl
- New 'trace' log level in the API, you should expect very spammy logs when
  trace is on and filter them down. As opposed to 'debug' which should be more
  readable.
- traceCall decorator logs the args and return value of the decorated method.
  Wecan use this on key functions in various parts of the code to assist
  debugging issues that are hard to reproduce such as #4665.
- Decorated a few webgl render methods for #4665.
- There's an early exit in traceCall when not on the trace log level so the
  runtime impact when not in trace is negligable.

Part of #4665
2023-08-18 11:04:23 -07:00
Daniel Imms 3b3b23a2fd Merge pull request #4683 from Tyriar/tyriar/minor_cleanup
Remove unneeded comments, headless options
2023-08-17 09:25:31 -07:00
Daniel Imms 7b874473d3 Remove unneeded comments, headless options 2023-08-17 09:10:38 -07:00
Daniel Imms 9d7b9293eb Merge pull request #4681 from PerBothner/dom-selection
Fix bug in selection handling in dom renderer
2023-08-17 09:01:12 -07:00
Per Bothner 957e3e0f08 Fix bug in selection handling in dom renderer 2023-08-16 15:34:35 -07:00
Daniel Imms 2f0a7eee8d Merge pull request #4679 from Tyriar/4663
Reduce repetition with internal terminal types and inherit docs from API
2023-08-15 08:49:40 -07:00
Daniel Imms 1a3e14a24c Merge pull request #4680 from Tyriar/crash
Revert Stylesheet change
2023-08-15 08:49:05 -07:00
Daniel Imms a6b67fb8a6 Remove unused headless types file 2023-08-15 08:39:20 -07:00
Daniel Imms 843bdc4d55 Revert "Merge pull request #4611 from SimonSiefke/constructed-stylesheets"
This reverts commit 5889344dd2, reversing
changes made to 679c149164.
2023-08-15 08:33:42 -07:00
Daniel Imms 2aee19f0e1 Revert "Workaround crash that can happen in Electron/Chromium"
This reverts commit 2ca0ccbe0c.
2023-08-15 08:31:34 -07:00
Daniel Imms 9f3be5e28d Simplify terminal interfaces
Fixes #4663
2023-08-15 07:49:31 -07:00
Daniel Imms f69f803f0f Implement public APIs internally to inherit docs
Part of #4663
2023-08-15 07:46:51 -07:00
Daniel Imms 2eca26b210 Merge pull request #4678 from Tyriar/vscode189753
Workaround crash that can happen in Electron/Chromium
2023-08-15 07:01:28 -07:00
Daniel Imms 2ca0ccbe0c Workaround crash that can happen in Electron/Chromium
Part of microsoft/vscode#189753
2023-08-15 06:46:35 -07:00
Daniel Imms c908eeed5c Merge pull request #4667 from Dennnnny/fix-transparent-foreground-color
Fix transparent foreground color
2023-08-14 12:11:04 -07:00
Daniel Imms caa1223e5c Merge pull request #4677 from Tyriar/fix_headless
Fix headless types regression
2023-08-14 07:18:50 -07:00
Daniel Imms 62cf3d39e1 Fix headless types regression
From the API lint PR
2023-08-14 07:15:20 -07:00
Daniel Imms 2437b68d5d Merge pull request #4676 from Tyriar/4097_2
Remove redundant condition
2023-08-14 06:01:07 -07:00
Daniel Imms 09fc798e80 Remove redundant condition
Part of #4097
2023-08-14 05:47:25 -07:00
Daniel Imms 7ae6febedc Merge pull request #4673 from Tyriar/4097
DOM: Render selection color instead of cell bg
2023-08-14 05:34:14 -07:00
Daniel Imms 39ae0f45fd Fix selectionForeground dom tests 2023-08-14 04:43:55 -07:00
dennnnny 855e911398 Fix transparent foreground color 2023-08-14 09:49:18 +08:00
Daniel Imms 73a77cda3d DOM: Render selection color instead of cell bg
Summary:

- Selection cell reuse has been added to the DOM renderer
- Selection bg override is now correctly set correctly
- Selection elements 'pretend' to be a decoration in order to render bg

Fixes #4097
2023-08-13 13:56:51 -07:00
Daniel Imms 6c9346011d Merge pull request #4672 from Tyriar/4262
Enforce half minimum contrast ratio for dim text
2023-08-13 13:14:45 -07:00
Daniel Imms 28c4732abc Fix ThemeService.ctor test 2023-08-13 13:04:40 -07:00
Daniel Imms 1cef30cca0 Enforce half minimum contrast ratio for dim text
Provided MCR is not too high, this ensures dim text differs from normal
text and has a reasonable difference in contrast.

Fixes #4262
2023-08-13 11:27:57 -07:00
Daniel Imms af787e1c85 Merge pull request #4671 from Tyriar/4652
Fix SortedList.values iteration and general dec lifecycle fixes
2023-08-13 10:53:20 -07:00
Daniel Imms 70f6fa2bc9 Fix SortedList.values iteration and general dec lifecycle fixes
Fixes #4652
2023-08-13 10:42:45 -07:00
dennnnny bef87403db Fix force transparent foreground colors to be opaque 2023-08-13 17:52:49 +08:00
Daniel Imms 5890029f71 Merge pull request #4669 from Tyriar/api_lint
Lint API .d.ts files
2023-08-12 10:32:53 -07:00
Daniel Imms 1f4fe0a8c0 Lint API .d.ts files
Similar to #4668, this brings linting to the API files with a cut down
set of rules. One of the bigger ones is comment length is restricted to 80,
this was done as opposed to 100 for regular code to reduce the chance of
wrapping or API going off screen regardless of resolution or window/browser
size
2023-08-12 09:33:35 -07:00
Daniel Imms 519f6a3d4a Merge pull request #4668 from Tyriar/comment_lint
Restrict comments to 100 characters
2023-08-12 09:18:14 -07:00
Daniel Imms 530f758905 Restrict comments to 100 characters
This is a rule I try enforce in PR but it's a bit of a hassle, this will
give a lint warning when it happens resulting in:

- Yellow underline in supported editors when eslint is installed
- Compilation will succeed since it's a warning not an error
- CI will fail as it does not allow lint warnings

Special case comments are ignored which includes @vt comments, table
comments and commented out code.
2023-08-12 09:04:41 -07:00
dennnnny fb7349bc87 Fix transparent foreground color 2023-08-12 08:39:39 +08:00
Daniel Imms dc390ab03a Merge pull request #4657 from tisilent/add-cursorInactiveStyle-option
Add cursorInactiveStyle option
2023-08-11 13:16:51 -07:00
Daniel Imms 6f6cc6a686 Remove string mapping, improve types, standardize on 'outline' 2023-08-11 13:09:31 -07:00
Daniel Imms 136e2f2afe Improve API wording 2023-08-11 13:07:04 -07:00
Daniel Imms 73bb72dbcf Merge pull request #4666 from Tyriar/nvm
Add nvmrc using node 16
2023-08-11 10:03:41 -07:00
Daniel Imms 4b2f774f53 Merge pull request #4664 from tisilent/fix-canvas-underline-cutoff
Fix canvas underline is cut off.
2023-08-11 10:01:28 -07:00
Daniel Imms 0a4151fbfa Add nvmrc using node 16
node-pty doesn't seem to work 18 yet and VS Code has moved over to be based
on 18 so that's my main (don't ask me why it's working there but not in this
repo)
2023-08-11 09:52:15 -07:00
Daniel Imms 07d187151e Update src/browser/renderer/shared/TextureAtlas.ts 2023-08-11 09:50:22 -07:00
Daniel Imms 47f606f0df Merge pull request #4662 from Tyriar/inwasm
Add functional yarn wasm
2023-08-11 05:09:25 -07:00
Daniel Imms 18125dad84 Add functional yarn wasm
This is what I tried after seeing the error so it will improve the #4646
experience until a real fix is in place

Part of #4646
2023-08-11 04:58:07 -07:00
tisilent f317b96262 Fix canvas underline is cut off. 2023-08-11 17:58:29 +08:00
tisilent c3d8c2a919 Update test and fix 2023-08-11 15:47:50 +08:00
tisilent 5c1e6dce99 Add block to cursorInactiveStyle,
change line to bar
2023-08-11 11:36:40 +08:00
tisilent 0321bcb715 Remove special classes.
Add xterm-cursor-outline class.
2023-08-11 09:40:06 +08:00
Daniel Imms bb5d61fb14 Merge pull request #4659 from SvanT/dispose-blink-state-manager
Dispose blink state manager
2023-08-10 10:27:24 -07:00
Svante Boberg 3b3012353f Use disposable registration 2023-08-10 19:04:20 +02:00
Svante Boberg adad53d526 Dispose blink state manager 2023-08-10 18:47:04 +02:00
Daniel Imms 1b4460e37d Merge pull request #4655 from Tyriar/tyriar/4645
Fix API facade memory leaks
2023-08-10 07:29:14 -07:00
tisilent 482dad3036 Add cursorInactiveStyle option 2023-08-10 16:11:04 +08:00
Daniel Imms 7276027861 Merge pull request #4651 from jerch/fix_4642
fix decorations on DOM renderer
2023-08-09 16:08:23 -07:00
Daniel Imms 62055522fb Fix API facade memory leaks
We need to register/dispose any event created in the facades, otherwise
embedders need to dispose of the listeners manually.

Fixes #4645
2023-08-09 15:55:31 -07:00
jerch 0366aa2217 Merge branch 'master' into fix_4642 2023-08-10 00:24:03 +02:00
Jörg Breitbart 2b79b7b29f faster SortedList._search 2023-08-10 00:12:00 +02:00
Daniel Imms 02243173c2 Merge pull request #4648 from tisilent/fix-double-underline
fix double underline, upwards
2023-08-09 06:45:03 -07:00
Jörg Breitbart a84a95055d exclude decorated cells from merging 2023-08-09 14:20:08 +02:00
tisilent b8bf35be17 Add restrictToCellHeight,Restore abnormal format 2023-08-09 11:53:30 +08:00
tisilent 3ba579f680 fix double underline, upwards 2023-08-08 18:36:44 +08:00
Daniel Imms 4f41070b45 Merge pull request #4638 from Tyriar/4603
Fix reset and clear leaving viewport in a bad state
2023-08-02 11:55:14 -07:00
Daniel Imms d2031955ec Remove error from MockViewport.reset 2023-08-02 11:42:40 -07:00
Daniel Imms 91c08ed48e Fix clear leaving viewport in a bad state
Fixes #4603
2023-08-02 11:30:43 -07:00
Daniel Imms acf53f69e0 Ensure viewport is correctly reset on term.reset
Part of #4603
2023-08-02 11:18:00 -07:00
Daniel Imms bb237f109a Merge pull request #4637 from meganrogge/merogge/fix-acc-tree
fix a11y tree
2023-08-02 08:30:06 -07:00
meganrogge 44d4ec83fc revert change 2023-08-02 08:17:18 -07:00
meganrogge 33124a31bd fix #4571 2023-08-02 08:14:16 -07:00
Daniel Imms 1e90aab2a6 Merge pull request #4636 from Tyriar/4588
Add ignoreBracketedPasteMode option
2023-08-01 14:40:39 -07:00
Daniel Imms 44ced27bee Add ignoreBracketedPasteMode option
Fixes #4588
2023-08-01 13:58:52 -07:00
Daniel Imms c3def09c38 Merge pull request #4631 from jerch/missing_image_changes
image addon fixes
2023-08-01 12:38:59 -07:00
Daniel Imms afff339d28 Merge pull request #4605 from jerch/faster_dom
Faster DOM renderer
2023-08-01 12:32:56 -07:00
Jörg Breitbart b7a597f925 use offsetWidth for measuring 2023-08-01 19:53:41 +02:00
Jörg Breitbart ae84ee39f3 Merge branch 'master' into faster_dom 2023-08-01 19:21:30 +02:00
jerch 73e60586c9 Merge pull request #4584 from dlech/fix-underline-style
use border-bottom instead of box-shadow for underline cursor in DOM renderer
2023-08-01 19:15:41 +02:00
Jörg Breitbart b56bc77dcc make font variant positions explicit with const enum 2023-08-01 18:35:48 +02:00
Jörg Breitbart 32a852c0bf remove private binding of document in width cache ctor 2023-08-01 18:30:06 +02:00
Jörg Breitbart 40e622dfc8 change comment format 2023-08-01 18:29:00 +02:00
Jörg Breitbart 994861a6fa change repeat variable to const enum 2023-08-01 18:25:29 +02:00
Jörg Breitbart b770431102 Merge branch 'master' into faster_dom 2023-08-01 18:18:31 +02:00
Jörg Breitbart 2fd108c864 Merge branch 'missing_image_changes' of github.com:jerch/xterm.js into missing_image_changes 2023-08-01 18:12:50 +02:00
Jörg Breitbart 914f7be644 add image addon to publish 2023-08-01 18:11:03 +02:00
jerch 1fdb8c006d Merge branch 'master' into missing_image_changes 2023-08-01 17:42:34 +02:00
Daniel Imms b46404b139 Merge pull request #4568 from xzfc/webgl-cursor
Render the cursor in the WebGL canvas
2023-08-01 08:41:16 -07:00
Daniel Imms 1dfd14fbc0 Fix canvas count test 2023-08-01 08:28:59 -07:00
Jörg Breitbart a1ddc629dc make tests less data intensive 2023-08-01 17:26:02 +02:00
Daniel Imms 9cfc2c881c Merge pull request #4626 from Tyriar/tyriar/light_cursor
Fix demo light theme cursor colors
2023-08-01 08:08:29 -07:00
Jörg Breitbart b7f9db69ca update readme 2023-08-01 16:51:56 +02:00
Daniel Imms 60eb4f3385 Create ICursorRenderModel 2023-08-01 07:47:27 -07:00
Daniel Imms 41e97ec042 Always render cursor even if model didn't change
This ensures underline/bar cursors get rendered
2023-08-01 07:43:27 -07:00
Jörg Breitbart fe838726b0 transfer changes from 0.4.3 2023-08-01 16:43:14 +02:00
Daniel Imms 1cfce3d0b6 Fix demo light theme cursor colors 2023-08-01 06:37:00 -07:00
Daniel Imms f64cb1566d Correctly apply cursor colors in new renderer 2023-08-01 06:31:58 -07:00
Daniel Imms 9b204c012a Remove bad logs 2023-08-01 06:19:54 -07:00
Daniel Imms 6b33ac1e85 Share cursor blink state manager between canvas and webgl
This also focused the manager's creation on renderer start up, to ensure blink
works immediately.
2023-08-01 06:13:21 -07:00
Daniel Imms 18e578bd09 Merge branch 'master' into webgl-cursor 2023-08-01 05:53:45 -07:00
Daniel Imms e8a925b6af Merge pull request #4583 from jerch/fix_mouse_tracking
fix mouse tracking behavior
2023-08-01 05:50:20 -07:00
Daniel Imms db6e0cd2ef Merge pull request #4482 from tisilent/#4121
Add smoothScroll to scrollLines
2023-08-01 05:48:26 -07:00
Daniel Imms f6738ec354 Merge pull request #4577 from jerch/image_addon
move image addon to base repo
2023-08-01 05:40:48 -07:00
Daniel Imms 1067ec19a8 Implement smooth scroll for pages/top/bottom, add viewport request event 2023-08-01 05:31:30 -07:00
Daniel Imms e7a7842808 Use viewport source in tests 2023-08-01 05:11:19 -07:00
Daniel Imms ce3bb35c8c Merge pull request #4366 from arekouzounian/master
Fix for #2488. Use element offsetHeight rather than getBoundingRect
2023-08-01 05:05:42 -07:00
Daniel Imms 2d7b531bba Merge branch 'master' into #4121 2023-08-01 04:58:36 -07:00
Daniel Imms 2d81490ac4 Merge pull request #4387 from LabhanshAgrawal/ligatures-electron
Enable ligatures addon on electron
2023-08-01 04:57:16 -07:00
Daniel Imms 5939d72d8f Merge branch 'master' into master 2023-08-01 04:54:05 -07:00
Daniel Imms d1bd4973ad Merge pull request #4348 from silamon/ansiheadlessretry
SerializeAsHTML(): Fallback to default ansi colors when running headless terminal
2023-08-01 04:52:43 -07:00
Jörg Breitbart f407f0c5a6 unit tests 2023-07-31 21:24:39 +02:00
Jörg Breitbart 40eccfa61a Merge branch 'faster_dom' of github.com:jerch/xterm.js into faster_dom 2023-07-31 20:07:56 +02:00
Jörg Breitbart c79aa542c9 cache optimizations 2023-07-31 20:07:40 +02:00
jerch 366ffedf08 Merge branch 'master' into faster_dom 2023-07-31 18:01:16 +02:00
Jörg Breitbart c796514de8 apply weight settings in width cache 2023-07-31 16:49:58 +02:00
Jörg Breitbart e75ec5b51f revert changes to selection service 2023-07-31 16:09:44 +02:00
Jörg Breitbart 702c3578a3 re-add explicit span stylesheet 2023-07-30 19:03:54 +02:00
Jörg Breitbart 0eecf8187d Merge branch 'faster_dom' of github.com:jerch/xterm.js into faster_dom 2023-07-30 18:46:49 +02:00
Jörg Breitbart e9058b7498 implement default letter spacing 2023-07-30 18:46:33 +02:00
Daniel Imms 25b266e809 Merge pull request #4618 from Tyriar/4614_color_index
Add IntRange type for ColorIndex
2023-07-30 07:04:14 -07:00
Daniel Imms 2e638598e4 Simplify condition 2023-07-30 06:55:45 -07:00
jerch 8562635231 Merge branch 'master' into faster_dom 2023-07-30 13:37:45 +02:00
Jörg Breitbart b358754b46 respect dpr/charWidth changes, more accurate base measuring 2023-07-30 13:27:28 +02:00
Jörg Breitbart 64d04f0712 optimization: remove document fragment 2023-07-29 23:58:15 +02:00
Daniel Imms ebdafe54aa Merge pull request #4619 from Tyriar/tyriar/4611_2
Avoid using global document, add copyright
2023-07-29 14:50:50 -07:00
Daniel Imms 829ecac8e3 Merge branch 'master' into image_addon 2023-07-29 14:43:46 -07:00
Daniel Imms d22e3e093a Fix compile 2023-07-29 14:42:05 -07:00
Daniel Imms a9662e8b7b Avoid using global document, add copyright
Fixes #4611
2023-07-29 14:38:56 -07:00
Jörg Breitbart 5f98db9b76 re-enable failing test 2023-07-29 18:26:31 +02:00
Jörg Breitbart 664a110fd1 Merge branch 'master' into faster_dom 2023-07-29 18:07:47 +02:00
Jörg Breitbart 3b5044b55b readd doc notes on link underline handling 2023-07-29 17:54:14 +02:00
Jörg Breitbart e848151b50 change to width cache 2023-07-29 17:25:49 +02:00
Jörg Breitbart 91197bd1aa disable font kerning 2023-07-29 15:37:01 +02:00
Jörg Breitbart d1fdaa17d6 apply font variant, some cleanup 2023-07-29 14:16:47 +02:00
Daniel Imms 5660c404f0 Differentiate special color indexes and others 2023-07-28 16:32:05 -07:00
Daniel Imms 79c1fff853 Add IntRange type for ColorIndex
Fixes #4614
2023-07-28 13:51:46 -07:00
Daniel Imms 962c070a75 Merge branch 'master' into webgl-cursor 2023-07-28 13:42:47 -07:00
Jörg Breitbart 5bdd5022b2 some cleanup, use faster str join for classes 2023-07-28 19:33:38 +02:00
Jörg Breitbart 370bad1380 better caching:
- own cache class with flat and holey cache
- use letter spacing
- on-demand measuring
2023-07-28 18:36:01 +02:00
Daniel Imms 17b8247c3d Merge pull request #4617 from xtermjs/Tyriar-patch-1
Mention discussions in CONTRIBUTING.md
2023-07-28 07:44:13 -07:00
Daniel Imms c0d1dec847 Mention discussions in CONTRIBUTING.md 2023-07-28 07:33:35 -07:00
Daniel Imms 5889344dd2 Merge pull request #4611 from SimonSiefke/constructed-stylesheets
feature: use constructed style sheets for dom render so that it works with csp style-src 'self'
2023-07-27 12:45:31 -07:00
Simon Siefke 5f80fe00d5 fix: use replacesync instead of replace 2023-07-27 18:33:30 +02:00
Simon Siefke b225b9a710 fix eslint 2023-07-27 17:39:15 +02:00
Daniel Imms 72c51b19a5 Merge branch 'master' into constructed-stylesheets 2023-07-27 08:28:24 -07:00
Daniel Imms 679c149164 Merge pull request #4612 from SimonSiefke/update-typescript-version
update typescript version to 5.1.6
2023-07-27 08:28:09 -07:00
Simon Siefke 92a18defce remove one more public 2023-07-27 17:16:00 +02:00
Simon Siefke 859c9ec68d update typescript-eslint 2023-07-27 17:11:04 +02:00
Simon Siefke b4aeb1b3e7 fix eslint warnings 2023-07-27 13:29:02 +02:00
Simon Siefke 8f1313519f update typescript version to 5.1.6 2023-07-27 13:19:38 +02:00
Simon Siefke 1c0a25d01b feature: use constructed style sheets for dom render so that it works with csp style-src 'self' 2023-07-27 13:08:22 +02:00
Jörg Breitbart a5df5e7333 first unit tests 2023-07-27 11:25:57 +02:00
Jörg Breitbart 4bd3e1d0c2 remove magic numbers 2023-07-25 13:19:08 +02:00
Jörg Breitbart ad145541af exclude bold&italic variants from merge, limit glyph calc to latin + latin supplement 2023-07-25 12:47:35 +02:00
Jörg Breitbart 37153018b9 fix BCE 2023-07-24 16:06:19 +02:00
Jörg Breitbart 90f1318596 skip failing test for now 2023-07-24 00:01:51 +02:00
Jörg Breitbart 4a5ae78d35 try to fix search test error 2023-07-23 23:52:17 +02:00
Jörg Breitbart 097879b839 faster metrics calc 2023-07-23 23:36:25 +02:00
Jörg Breitbart 2d9ad32d93 fix weblinks api test 2023-07-23 22:25:58 +02:00
Jörg Breitbart 4d257f469b fix linter 2023-07-23 21:12:38 +02:00
Jörg Breitbart 521dc915dc fix weblink tests 2023-07-23 12:36:15 +02:00
Jörg Breitbart 5ea669f091 batch metrics calc 2023-07-23 00:08:40 +02:00
Jörg Breitbart aa6e8d91c2 cleanup 2023-07-22 22:50:05 +02:00
Jörg Breitbart 089dc282c3 fix hover underline 2023-07-22 22:18:12 +02:00
Jörg Breitbart 329b465a63 fix SP for underline/overline 2023-07-22 16:53:13 +02:00
Jörg Breitbart 6785dc19ab exclude combined from merge, fix linter 2023-07-22 16:32:01 +02:00
Jörg Breitbart c515cc2823 fix ext underline handling 2023-07-22 16:20:47 +02:00
Jörg Breitbart 848f89e8a7 fix char under cursor 2023-07-22 15:24:50 +02:00
Jörg Breitbart bd7f76190c fix superfluous selection.refresh calls 2023-07-22 15:12:37 +02:00
Jörg Breitbart 514afcd445 fix bad runtime of merger 2023-07-22 13:29:26 +02:00
Jörg Breitbart 3e13abf974 fix selection handling 2023-07-22 13:01:53 +02:00
Jörg Breitbart 4cfe32f626 make linter happy 2023-07-22 11:08:51 +02:00
Jörg Breitbart f36954764d faster DOM updates 2023-07-22 11:00:12 +02:00
Daniel Imms 1c98037eed Merge pull request #4599 from xtermjs/dependabot/npm_and_yarn/word-wrap-1.2.4
Bump word-wrap from 1.2.3 to 1.2.4
2023-07-18 14:55:34 -07:00
dependabot[bot] 2d732e3a99 Bump word-wrap from 1.2.3 to 1.2.4
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-18 21:27:19 +00:00
Daniel Imms 53d105714a Merge pull request #4596 from lewis-sanchez/lewissanchez/bug/rasterized-glyph-undefined
Add getRasterizedGlyph null and undefined check
2023-07-15 17:56:31 -07:00
Lewis Sanchez 78aad113a6 Minor clean up 2023-07-14 15:07:38 -07:00
Lewis Sanchez d2c28c819f Add getRasterizedGlyph null and undefined check 2023-07-14 11:31:59 -07:00
Daniel Imms 3d75333e95 Merge pull request #4595 from matifali/patch-1
Add Coder to the list
2023-07-14 07:48:40 -07:00
Muhammad Atif Ali 7e5bdb5321 Add Coder Self-Hosted Remote Development Environments to list 2023-07-14 14:08:34 +03:00
Daniel Imms be74969d70 Merge pull request #4589 from xtermjs/dependabot/npm_and_yarn/semver-5.7.2
Bump semver from 5.7.1 to 5.7.2
2023-07-11 04:35:17 -07:00
dependabot[bot] 196f863441 Bump semver from 5.7.1 to 5.7.2
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-11 10:14:24 +00:00
Daniel Imms 823258e311 Merge pull request #4587 from xtermjs/dependabot/npm_and_yarn/tough-cookie-4.1.3
Bump tough-cookie from 4.0.0 to 4.1.3
2023-07-09 08:55:23 -07:00
dependabot[bot] e100a4310b Bump tough-cookie from 4.0.0 to 4.1.3
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.0.0 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.0.0...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-09 10:15:18 +00:00
Daniel Imms 681c912533 Merge pull request #4585 from tomgroenwoldt/add-helix-editor-playground-project
Add helix editor playground project to real world uses
2023-07-06 07:25:42 -07:00
Tom Groenwoldt 6339c09c01 Add helix editor playground project to real world uses 2023-07-06 11:36:23 +02:00
David Lechner 96634298e4 use border-bottom instead of box-shadow for underline cursor
In rare cases, box-shadow can cause aliasing artifacts on the top and
sides of the cursor box when only the bottom should be filled in to
create an underline.

This replaces the box-shadow style with border-bottom which should be
pixel perfect in all cases.

Fixes: https://github.com/xtermjs/xterm.js/issues/4580
2023-07-05 16:48:42 +00:00
Jörg Breitbart 65568f502e fix #4582 2023-07-05 13:56:41 +02:00
tisilent 1fb9f0185a format 2023-06-30 11:45:50 +08:00
tisilent 74c5256d6d using _smoothScroll 2023-06-30 11:44:06 +08:00
Jörg Breitbart 3991b5f251 make linter happy 2023-06-27 17:11:45 +02:00
Jörg Breitbart 9afb924e84 different loop for failing test 2023-06-27 17:06:43 +02:00
Jörg Breitbart 7a82e7370f fix timeout issue again 2023-06-27 16:51:11 +02:00
Jörg Breitbart 709e9fd01a remove left over .only 2023-06-27 16:20:33 +02:00
Jörg Breitbart e16f87f973 use pollFor in failing test 2023-06-27 16:20:11 +02:00
Jörg Breitbart d4ddac704c more waits for remote browser under heavy CI load 2023-06-27 15:57:07 +02:00
Jörg Breitbart bc92d61616 remove left over .only 2023-06-27 15:41:54 +02:00
Jörg Breitbart 4884d81704 fix another heavy CI load timeout issue 2023-06-27 15:35:52 +02:00
Jörg Breitbart d99bdb7710 fix timeout issue in CI 2023-06-27 15:15:51 +02:00
Jörg Breitbart eaa265b4fa add image options and tests to demo 2023-06-27 15:04:55 +02:00
Jörg Breitbart 8fa3cae618 integrate addon into demo 2023-06-27 14:04:27 +02:00
Jörg Breitbart 0027b27ea3 add wasm builds 2023-06-27 13:49:49 +02:00
Jörg Breitbart 2bcc653293 remove only from api test 2023-06-27 13:43:02 +02:00
Jörg Breitbart ebfff5efee initial move to base repo 2023-06-27 13:35:59 +02:00
Daniel Imms ac0207bf2e Merge pull request #4570 from naufalafif/cloudtutor-io-patch-1
add cloudtutor.io to real world uses
2023-06-19 07:52:33 -07:00
naufal afif 8068880aab add cloudtutor.io to real world uses 2023-06-19 10:26:12 +08:00
Albert Safin 064fe0cfd9 Render the cursor in the WebGL canvas 2023-06-18 06:34:14 +00:00
Daniel Imms 096fe17135 Merge pull request #4565 from Tyriar/tyriar/readonly_log
Improve safety of log service
2023-06-16 10:42:47 -07:00
Daniel Imms 4d501b38b8 Improve safety of log service 2023-06-16 10:30:54 -07:00
Daniel Imms f79e9a1951 Merge pull request #4564 from Tyriar/bind_loggers
Bind logger calls
2023-06-16 09:30:04 -07:00
Daniel Imms 64a62fb0c1 Bind logger calls 2023-06-16 09:19:23 -07:00
Daniel Imms 2c45989fea Merge pull request #4563 from Tyriar/logger
Add logger delegate option
2023-06-16 08:34:13 -07:00
Daniel Imms f779e4ddc0 Add logger delegate option
This allows embedders to route the logs whereever they want, when this
is set the xterm.js prefix is excluded.
2023-06-16 08:23:22 -07:00
Daniel Imms 7be0016e45 Merge branch 'master' into #4121 2023-06-10 14:02:21 -07:00
xie jialong 努力鸭 f616ebb083 Merge branch 'master' into #4121 2023-06-07 19:23:01 +08:00
tisilent ce2d115087 Move logic to Viewport 2023-06-06 20:14:24 +08:00
tisilent b7d73f63cd Add smoothScroll to scrollLines 2023-04-19 15:48:12 +08:00
Labhansh Agrawal a8cec5f02f Enable ligatures addon on electron 2023-02-23 23:49:40 +05:30
Arek Ouzounian 79d2f738a2 fixed linter warnings 2023-01-06 15:22:02 -08:00
Arek Ouzounian 7701557b99 Fix for #2488. Use element offsetHeight rather than
getBoundingRect
2023-01-06 14:56:20 -08:00
Simon Lamon ed8c2269cb Add proposed fix 2022-12-23 17:00:48 +00:00
Simon Lamon f1b47485cd Revert "Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal""
This reverts commit 3a81ef54ca.
2022-12-23 16:47:31 +00:00
198 changed files with 12248 additions and 5972 deletions
+42 -4
View File
@@ -13,11 +13,14 @@
"src/headless/tsconfig.json",
"test/api/tsconfig.json",
"test/benchmark/tsconfig.json",
"test/playwright/tsconfig.json",
"addons/xterm-addon-attach/src/tsconfig.json",
"addons/xterm-addon-attach/test/tsconfig.json",
"addons/xterm-addon-canvas/src/tsconfig.json",
"addons/xterm-addon-fit/src/tsconfig.json",
"addons/xterm-addon-fit/test/tsconfig.json",
"addons/xterm-addon-image/src/tsconfig.json",
"addons/xterm-addon-image/test/tsconfig.json",
"addons/xterm-addon-ligatures/src/tsconfig.json",
"addons/xterm-addon-search/src/tsconfig.json",
"addons/xterm-addon-search/test/tsconfig.json",
@@ -34,6 +37,7 @@
"sourceType": "module"
},
"ignorePatterns": [
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js"
@@ -87,7 +91,12 @@
],
"@typescript-eslint/naming-convention": [
"warn",
{ "selector": "default", "format": ["camelCase"] },
{ "selector": "default", "format": ["camelCase"],
"filter": {
"regex": "^[a-z]",
"match": true
}
},
// variableLike
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
@@ -96,7 +105,12 @@
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
// memberLike - Allow enum-like objects to use UPPER_CASE
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"],
"filter": {
"regex": "^[a-z]",
"match": true
}
},
// restrict on* naming for events only
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": {
"regex": "^on[A-Z].+",
@@ -114,6 +128,11 @@
{ "selector": "typeLike", "format": ["PascalCase"] },
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
],
"@typescript-eslint/no-confusing-void-expression": [
"warn",
{ "ignoreArrowShorthand": true }
],
"@typescript-eslint/no-useless-constructor": "warn",
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"@typescript-eslint/quotes": [
@@ -146,6 +165,16 @@
"jsdoc/check-param-names": 1,
"jsdoc/no-multi-asterisks": 1,
"keyword-spacing": "warn",
"max-len": [
"warn",
{
"code": 1000, // Don't enforce for code
"comments": 100,
"ignoreTrailingComments": true,
"ignoreUrls": true,
"ignorePattern": "^ *((?<vt_comment>(//|\\*) @vt)|(?<table_comment>\\* \\| )|(?<commented_code>// ))"
}
],
"new-parens": "warn",
"no-duplicate-imports": "warn",
"no-else-return": [
@@ -189,6 +218,10 @@
],
"no-trailing-spaces": "warn",
"no-unsafe-finally": "warn",
"no-unused-vars": ["warn", {
"vars": "all",
"args": "none"
}],
"no-var": "warn",
"one-var": [
"warn",
@@ -210,9 +243,14 @@
},
"overrides": [
{
"files": ["**/*.test.ts"],
"files": [
"**/*.api.ts",
"**/*.test.ts"
],
"rules": {
"object-curly-spacing": "off"
"object-curly-spacing": "off",
"max-len": "off",
"no-unused-vars": "off"
}
}
]
+104
View File
@@ -0,0 +1,104 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"jsdoc"
],
"rules": {
"no-extra-semi": "error",
"@typescript-eslint/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@typescript-eslint/indent": [
"warn",
2
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": [
"warn",
{ "selector": "typeLike", "format": ["PascalCase"] },
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
],
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"@typescript-eslint/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/semi": [
"warn",
"always"
],
"comma-dangle": [
"warn",
{
"objects": "never",
"arrays": "never",
"functions": "never"
}
],
"curly": [
"warn",
"multi-line"
],
"eol-last": "warn",
"eqeqeq": [
"warn",
"always"
],
"jsdoc/check-alignment": 1,
"jsdoc/check-param-names": 1,
"keyword-spacing": "warn",
"max-len": [
"warn",
{
"code": 1000, // Don't enforce for code
"comments": 80,
"ignoreUrls": true,
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
}
],
"no-irregular-whitespace": "warn",
"no-trailing-spaces": "warn",
"object-curly-spacing": [
"warn",
"always"
],
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"],
"exceptions": ["-"]
}
]
}
}
+300
View File
@@ -0,0 +1,300 @@
name: CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build
run: yarn setup
- name: Zip artifacts
run: |
zip -r compressed-build \
./out/* \
./out-test/* \
./addons/xterm-addon-attach/out/* \
./addons/xterm-addon-attach/out-test/* \
./addons/xterm-addon-canvas/out/* \
./addons/xterm-addon-canvas/out-test/* \
./addons/xterm-addon-fit/out/* \
./addons/xterm-addon-fit/out-test/* \
./addons/xterm-addon-image/out/* \
./addons/xterm-addon-image/out-test/* \
./addons/xterm-addon-ligatures/out/* \
./addons/xterm-addon-ligatures/out-test/* \
./addons/xterm-addon-search/out/* \
./addons/xterm-addon-search/out-test/* \
./addons/xterm-addon-serialize/out/* \
./addons/xterm-addon-serialize/out-test/* \
./addons/xterm-addon-unicode11/out/* \
./addons/xterm-addon-unicode11/out-test/* \
./addons/xterm-addon-web-links/out/* \
./addons/xterm-addon-web-links/out-test/* \
./addons/xterm-addon-webgl/out/* \
./addons/xterm-addon-webgl/out-test/*
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: compressed-build.zip
if-no-files-found: error
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- name: Lint code
run: yarn lint
- name: Lint API
run: yarn lint-api
test-unit-coverage:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Unit test coverage
run: |
yarn test-unit-coverage --forbid-only
EXIT_CODE=$?
./node_modules/.bin/nyc report --reporter=cobertura
exit $EXIT_CODE
test-unit-parallel:
timeout-minutes: 20
strategy:
matrix:
node-version: [18]
runs-on: [ubuntu, macos, windows]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Unit tests
run: yarn test-unit --forbid-only
test-unit:
needs: build
timeout-minutes: 20
strategy:
matrix:
node-version: [16]
runs-on: [ubuntu, macos, windows]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Unit tests
run: yarn test-unit --forbid-only
test-api-parallel:
timeout-minutes: 20
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
runs-on: [ubuntu] # macos is flaky
browser: [chromium, firefox]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- name: Install playwright
run: npx playwright install
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Integration tests (${{ matrix.browser }})
run: yarn test-api-${{ matrix.browser }} --headless --forbid-only
test-playwright-parallel:
timeout-minutes: 20
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
runs-on: [ubuntu] # macos is flaky
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- name: Install playwright
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Build demo
run: yarn build-demo
- name: Integration tests
run: yarn test-playwright-${{ matrix.browser }} --forbid-only
test-api:
needs: build
timeout-minutes: 20
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
runs-on: [windows] # macos is flaky
browser: [chromium, firefox]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- name: Install playwright
run: npx playwright install
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Integration tests (${{ matrix.browser }})
run: yarn test-api-${{ matrix.browser }} --headless --forbid-only
-37
View File
@@ -1,21 +1,9 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '41 17 * * 0'
@@ -33,40 +21,15 @@ jobs:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+30
View File
@@ -0,0 +1,30 @@
name: Release
on:
push:
# If a commit reaches master, assume it has passed CI via PR and publish
# without running tests to save time to publish
branches: [ "master" ]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build
run: yarn setup
- name: Package headless
run: |
yarn package-headless
node ./bin/package_headless.js
- name: Publish to npm
env:
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: node ./bin/publish.js
+1
View File
@@ -0,0 +1 @@
16
+3
View File
@@ -1,4 +1,7 @@
{
"files.associations": {
".eslintrc.json.typings": "jsonc"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
"mochaExplorer.envPath": ".mocha.env",
+4
View File
@@ -28,6 +28,10 @@ opening an issue, read these pointers.
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
## Answering discussion questions
Issues are only meant to track (likely) feature requests and bugs. We use [GitHub Discussions](https://github.com/xtermjs/xterm.js/discussions) for general Q&A as well as discussing possible features. If you want to help out, many questions get asked over at the [discussions page](https://github.com/xtermjs/xterm.js/discussions) which could use an expert as the core team is often stretched thin.
## Contributing code
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
+3 -2
View File
@@ -1,7 +1,5 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
## Features
@@ -216,6 +214,9 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
- [**graSSHopper**](https://grasshopper.coding.kiwi): A simple SSH client with file explorer, history and many more features.
- [**DomTerm**](https://domterm.org/xtermjs.html): Tiles and tabs. Detachable sessions (like tmux). [Remote connections](https://domterm.org/Remoting-over-ssh.html) using a nice ssh wrapper with predictive echo. Qt, Electron, Tauri/Wry, or desktop browser front-ends. Choose between xterm.js engine (faster) or native DomTerm (more functionality and graphics) - or both.
- [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab.
- [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor.
- [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-attach",
"version": "0.8.0",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -5,7 +5,7 @@
import WebSocket = require('ws');
import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-canvas",
"version": "0.4.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -3,24 +3,24 @@
* @license MIT
*/
import { ReadonlyColorSet } from 'browser/Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { isSafari } from 'common/Platform';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData, IDisposable } from 'common/Types';
import { Terminal } from 'xterm';
import { IRenderLayer } from './Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { isSafari } from 'common/Platform';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -37,7 +37,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
protected _charAtlas!: ITextureAtlas;
private _charAtlasDisposable?: IDisposable;
protected _charAtlasDisposable = this.register(new MutableDisposable());
public get canvas(): HTMLCanvasElement { return this._canvas; }
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
@@ -74,7 +74,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this.register(toDisposable(() => {
this._canvas.remove();
this._charAtlas?.dispose();
}));
}
@@ -122,9 +121,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlasDisposable?.dispose();
this._charAtlas = acquireTextureAtlas(this._terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
this._charAtlasDisposable.value = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
this._charAtlas.warmUp();
for (let i = 0; i < this._charAtlas.pages.length; i++) {
this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas);
@@ -368,11 +366,16 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
protected _drawChars(cell: ICellData, x: number, y: number): void {
const chars = cell.getChars();
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
if (!this._charAtlas) {
return;
}
let glyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true);
} else {
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true);
}
if (!glyph.size.x || !glyph.size.y) {
return;
+10 -4
View File
@@ -3,13 +3,14 @@
* @license MIT
*/
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { setTraceLogger } from 'common/services/LogService';
import { IBufferService, IDecorationService, ILogService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
import { CanvasRenderer } from './CanvasRenderer';
export class CanvasAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
@@ -44,8 +45,13 @@ export class CanvasAddon extends Disposable implements ITerminalAddon {
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
const decorationService: IDecorationService = unsafeCore._decorationService;
const logService: ILogService = unsafeCore._logService;
const themeService: IThemeService = unsafeCore._themeService;
// Set trace logger just in case it hasn't been yet which could happen when the addon is
// bundled separately to the core module
setTraceLogger(logService);
this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
@@ -3,12 +3,12 @@
* @license MIT
*/
import { ILinkifier2 } from 'browser/Types';
import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
@@ -3,17 +3,17 @@
* @license MIT
*/
import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { MutableDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { toDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
import { BaseRenderLayer } from './BaseRenderLayer';
interface ICursorState {
x: number;
@@ -23,15 +23,10 @@ interface ICursorState {
width: number;
}
/**
* The time between cursor blinks.
*/
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = this.register(new MutableDisposable());
private _cell: ICellData = new CellData();
constructor(
@@ -57,13 +52,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
'underline': this._renderUnderlineCursor.bind(this),
'outline': this._renderOutlineCursor.bind(this)
};
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this.register(toDisposable(() => {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}));
this._handleOptionsChanged();
}
public resize(dim: IRenderDimensions): void {
@@ -80,30 +73,27 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation();
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
this._handleOptionsChanged();
}
public handleBlur(): void {
this._cursorBlinkStateManager?.pause();
this._cursorBlinkStateManager.value?.pause();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public handleFocus(): void {
this._cursorBlinkStateManager?.resume();
this._cursorBlinkStateManager.value?.resume();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
private _handleOptionsChanged(): void {
if (this._optionsService.rawOptions.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true);
}, this._coreBrowserService);
if (!this._cursorBlinkStateManager.value) {
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => this._render(true), this._coreBrowserService);
}
} else {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
this._cursorBlinkStateManager.clear();
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
@@ -111,14 +101,14 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public handleCursorMove(): void {
this._cursorBlinkStateManager?.restartBlinkAnimation();
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
}
public handleGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isPaused) {
this._render(false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation();
this._cursorBlinkStateManager.value.restartBlinkAnimation();
}
}
@@ -150,7 +140,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._ctx.save();
this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
if (cursorInactiveStyle && cursorInactiveStyle !== 'none') {
this._cursorRenderers[cursorInactiveStyle](cursorX, viewportRelativeCursorY, this._cell);
}
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
@@ -161,7 +154,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
// Don't draw the cursor if it's blinking
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
if (this._cursorBlinkStateManager.value && !this._cursorBlinkStateManager.value.isCursorVisible) {
this._clearCursor();
return;
}
@@ -231,146 +224,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._ctx.restore();
}
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
private _renderOutlineCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
}
class CursorBlinkStateManager {
public isCursorVisible: boolean;
private _animationFrame: number | undefined;
private _blinkStartTimeout: number | undefined;
private _blinkInterval: number | undefined;
/**
* The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers
* multiple times over a short period.
*/
private _animationTimeRestarted: number | undefined;
constructor(
isFocused: boolean,
private _renderCallback: () => void,
private _coreBrowserService: ICoreBrowserService
) {
this.isCursorVisible = true;
if (isFocused) {
this._restartInterval();
}
}
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
public dispose(): void {
if (this._blinkInterval) {
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public restartBlinkAnimation(): void {
if (this.isPaused) {
return;
}
// Save a timestamp so that the restart can be done on the next interval
this._animationTimeRestarted = Date.now();
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}
}
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
if (time > 0) {
this._restartInterval(time);
return;
}
}
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
// Setup the blink interval
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
this._restartInterval(time);
return;
}
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}, BLINK_INTERVAL);
}, timeToStart);
}
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public resume(): void {
// Clear out any existing timers just in case
this.pause();
this._animationTimeRestarted = undefined;
this._restartInterval();
this.restartBlinkAnimation();
}
}
@@ -3,14 +3,14 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
@@ -4,18 +4,16 @@
*/
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { color, css } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { CharData, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData';
import { Content, NULL_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { GridCache } from './GridCache';
/**
* This CharData looks like a null character, which will forc a clear and render
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-fit",
"version": "0.7.0",
"version": "0.8.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
-2
View File
@@ -24,8 +24,6 @@ const MINIMUM_ROWS = 1;
export class FitAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;
constructor() {}
public activate(terminal: Terminal): void {
this._terminal = terminal;
}
+3 -7
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
@@ -41,9 +41,7 @@ describe('FitAddon', () => {
});
describe('proposeDimensions', () => {
afterEach(async () => {
return await unloadFit();
});
afterEach(() => unloadFit());
it('default', async function(): Promise<any> {
await loadFit();
@@ -82,9 +80,7 @@ describe('FitAddon', () => {
});
describe('fit', () => {
afterEach(async () => {
return await unloadFit();
});
afterEach(() => unloadFit());
it('default', async function(): Promise<any> {
await loadFit();
+34
View File
@@ -0,0 +1,34 @@
node_modules/
*.swp
.lock-wscript
lib/
out/
out-test/
out-worker/
.nyc_output/
Makefile.gyp
*.Makefile
*.target.gyp.mk
*.node
example/*.log
docs/
npm-debug.log
/.idea/
.env
build/
.DS_Store
package-lock.json
yarn.lock
# Keep bundled code out of Git
dist/
demo/dist/
# skip benchmark & inwasm folders, except builds
.benchmark/
timeline/
inwasm-sdks/
inwasm-builds/
!inwasm-builds/**/final.wat
!inwasm-builds/**/final.wasm
!inwasm-builds/**/definition.json
+10
View File
@@ -0,0 +1,10 @@
.github
fixture
overwrite
bootstrap.sh
tsconfig.json
webpack.config.js
test
out-test
inwasm-sdks
inwasm-builds
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2019, 2020, 2021, 2022, 2023 The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+231
View File
@@ -0,0 +1,231 @@
## xterm-addon-image
Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP).
![](fixture/example.png)
### Install from npm
```bash
npm install --save xterm-addon-image
```
### Usage
```ts
import { Terminal } from 'xterm';
import { ImageAddon, IImageAddonOptions } from 'xterm-addon-image';
// customize as needed (showing addon defaults)
const customSettings: IImageAddonOptions = {
enableSizeReports: true, // whether to enable CSI t reports (see below)
pixelLimit: 16777216, // max. pixel size of a single image
sixelSupport: true, // enable sixel support
sixelScrolling: true, // whether to scroll on image output
sixelPaletteLimit: 256, // initial sixel palette size
sixelSizeLimit: 25000000, // size limit of a single sixel sequence
storageLimit: 128, // FIFO storage limit in MB
showPlaceholder: true, // whether to show a placeholder for evicted images
iipSupport: true, // enable iTerm IIP support
iipSizeLimit: 20000000 // size limit of a single IIP sequence
}
// initialization
const terminal = new Terminal();
const imageAddon = new ImageAddon(customSettings);
terminal.loadAddon(imageAddon);
```
### General Notes
- *IMPORTANT:* The worker approach as done in previous releases got removed.
The addon contructor no longer expects a worker path as first argument.
- By default the addon will activate these `windowOptions` reports on the terminal:
- getWinSizePixels (CSI 14 t)
- getCellSizePixels (CSI 16 t)
- getWinSizeChars (CSI 18 t)
to help applications getting useful terminal metrics for their image preparations. Set `enableSizeReports` in the constructor options to `false`, if you dont want the addon to alter these terminal settings. This is especially useful, if you have very strict security needs not allowing any terminal reports, or deal with `windowOptions` by other means.
### Operation Modes
- **SIXEL Support**
Set by default, change it with `{sixelSupport: true}`.
- **Scrolling On | Off**
By default scrolling is on, thus an image will advance the cursor at the bottom if needed.
(see cursor positioning).
If scrolling is off, the image gets painted from the top left of the current viewport
and might be truncated if the image exceeds the viewport size.
The cursor position does not change.
You can customize this behavior with the constructor option `{sixelScrolling: false}`
or with `DECSET 80` (off, binary: `\x1b [ ? 80 h`) and
`DECRST 80` (on, binary: `\x1b [ ? 80 l`) during runtime.
- **Cursor Positioning**
If scrolling is set, the cursor will be placed at the first image column of the last image row (VT340 mode).
Other cursor positioning modes as used by xterm or mintty are not supported.
- **SIXEL Palette Handling**
By default the addon limits the palette size to 256 registers (as demanded by the DEC specification).
The limit can be increased to a maximum of 4096 registers (via `sixelPaletteLimit`).
The default palette is a mixture of VT340 colors (lower 16 registers), xterm colors (up to 256) and zeros (up to 4096).
There is no private/shared palette distinction, palette colors are always carried over from a previous sixel image.
Restoring the default palette size and colors is possible with `XTSMGRAPHICS 1 ; 2` (binary: `\x1b[?1;2S`).
It gets also restored automatically on RIS and DECSTR.
Other than on older terminals, the underlying SIXEL library applies colors immediately to individual pixels
(*printer mode*), thus it is technically possible to use more colors in one image than the palette has color slots.
This feature is called *high-color* in libsixel.
A terminal wide shared palette mode with late indexed coloring of the output is not supported,
therefore palette animations cannot be used.
- **SIXEL Raster Attributes Handling**
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extend. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extend. Both is discouraged by the >30 years old specification.
Currently the SIXEL implementation of the addon does not take custom pixel sizes into account, a SIXEL pixel will map 1:1 to a screen pixel.
- **IIP Support (iTerm's Inline Image Protocol)**
Set by default, change it with `{iipSupport: true}`.
The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions):
- Supported formats: PNG, JPEG and GIF
- No animation support.
- Image type hinting is not supported (always deducted from data header).
- File download is not supported.
- Filename gets parsed but not used.
- Strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding, no separator bytes allowed).
- Payload size may not exceed CEIL(sizeParameter * 4 / 3).
- Image scaling beyond terminal viewport size is allowed (e.g. `width=200%`).
- Image pixel size is restricted by `pixelLimit` (pre- and post resizing).
- Size parameter is restricted by `iipSizeLimit`.
- Cursor positioning behaves the same as for sixel (see above).
### Storage and Drawing Settings
The internal storage holds images up to `storageLimit` (in MB, calculated as 4-channel RBGA unpacked, default 100 MB). Once hit images get evicted by FIFO rules. Furthermore images on the alternate buffer will always be erased on buffer changes.
The addon exposes two properties to interact with the storage limits at runtime:
- `storageLimit`
Change the value to your needs at runtime. This is especially useful, if you have multiple terminal
instances running, that all add to one upper memory limit.
- `storageUsage`
Inspect the current memory usage of the image storage.
By default the addon will show a placeholder pattern for evicted images that are still part
of the terminal (e.g. in the scrollback). The pattern can be deactivated by toggling `showPlaceholder`.
### Image Data Retrieval
The addon provides the following API endpoints to retrieve raw image data as canvas:
- `getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined`
Returns the canvas containing the original image data (not resized) at the given buffer position.
The buffer position is the 0-based absolute index (including scrollback at top).
- `extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined`
Returns a canvas containing the actual single tile image data (maybe resized) at the given buffer position.
The buffer position is the 0-based absolute index (including scrollback at top).
Note that the canvas gets created and data copied over for every call, thus it is not suitable for performance critical actions.
### Memory Usage
The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps:
- Incomming image data chunk at `term.write` (terminal)
`term.write` might stock up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
- Sequence Parser (terminal)
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
- Sequence Handler - Chunk Decoding (addon)
Image data chunks are processed immediately by the SIXEL decoder (streamlined). The decoder allocates memory for image
pixels as needed. The allowed image size is restricted by `pixelLimit` (default 16M pixels), the decoder holds 2 pixel buffers at maximum during decoding (RGBA, ~128 MB for 16M pixels).
- Sequence Handler - Image Finalization (addon)
After decoding the final pixel buffer is grabbed by the sequence handler and written to a canvas of the same size (~64 MB for 16M pixels) and added to the storage.
- Image Storage (addon)
The image storage implements a FIFO cache, that will remove old images, if a new one arrives and `storageLimit` is hit (default 128 MB). The storage holds a canvas with the original image, and may additionally hold resized versions of images after a font rescaling. Both are accounted in `storageUsage` as a rough estimation of _pixels x 4 channels_.
Following the steps above, a rough estimation of maximum memory usage by the addon can be calculated with these formulas (in bytes):
```typescript
// storage alone
const storageBytes = storageUsage * storageLimit * 1024 * 1024;
// decoding alone
const decodingBytes = sixelSizeLimit + 2 * (pixelLimit * 4);
// totals
// inactive decoding
const totalInactive = storageBytes;
// active decoding
const totalActive = storageBytes + decodingBytes;
```
Note that browsers have offloading tricks for rarely touched memory segments, esp. `storageBytes` might not directly translate into real memory usage. Usage peaks will happen during active decoding of multiple big images due to the need of 2 full pixel buffers at the same time, which cannot be offloaded. Thus you may want to keep an eye on `pixelLimit` under limited memory conditions.
Further note that the formulas above do not respect the Javascript object's overhead. Compared to the raw buffer needs the book keeping by these objects is rather small (<<5%).
_Why should I care about memory usage at all?_
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the Javascript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
_How can I adjust the memory usage?_
- `pixelLimit`
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
- `storageLimit`
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
- `sixelSizeLimit`
A constructor setting. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken SIXEL sequences at an earlier phase, thus may stop the decoder from entering its memory intensive task for potentially invalid data.
### Terminal Interaction
- Images already on the terminal screen will reshape on font-rescaling to keep the terminal cell coverage intact.
This behavior might diverge from other terminals, but is in general the more desired one.
- On terminal resize images may expand to the right automatically, if they were right-truncated before.
They never expand to the bottom, if they were bottom-truncated before (e.g. from scrolling-off).
- Text autowrapping from terminal resize may break and wrap images into multiple parts. This is unfortunate,
but cannot be avoided, while sticking to the stronger terminal text-grid mechanics.
(Yes images are a second class citizen on a mainly text-driven interface.)
- Characters written over an image will erase the image information for affected cells.
- Images are painted above BG/FG data not erasing it. More advanced "composition tricks" like painting images
below FG/BG data are not possible. (We currently dont hook into BG/FG rendering itself.)
- Previous image data at a cell will be erased on new image data. (We currently have no transparency composition.)
### Performance & Latency
- Performance should be good enough for occasional SIXEL output from REPLs, up to downscaled movies
from `mpv` with its SIXEL renderer (tested in the demo). For 3rd party xterm.js integrations this
furthermore depends highly on the overall incoming data throughput.
- Image processing has a high latency. Most of the latency though is inherited from xterm.js' incoming data route
(PTY -> server process -> websocket -> xterm.js async parsing), where every step creates more waiting time.
Since we cannot do much about that "long line", keep that in mind when you try to run more demanding applications with realtime drawing and interactive response needs.
### Status
- Sixel support and image handling in xterm.js is considered beta quality.
- IIP support is in alpha stage. Please file a bug for any awkwardities.
### Changelog
- 0.5.0 integrate with xtermjs base repo (at v0.4.3)
- 0.4.3 defer canvas creation
- 0.4.2 fix image canvas resize
- 0.4.1 compat release for xterm.js 5.2.0
- 0.4.0 IIP support
- 0.3.1 compat release for xterm.js 5.1.0
- 0.3.0 important change: worker removed from addon
- 0.2.0 compat release for xterm.js 5.0.0
- 0.1.3 bugfix: avoid striping
- 0.1.2 bugfix: reset clear flag
- 0.1.1 bugfixes:
- clear sticky image tiles on render
- create xterm-addon-image folder from bootstrap.sh
- fix peer dependency in package.json
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# sixel endless mode
# Should print an endless sine curve, abort with Ctrl-C.
period=200
amplitude=50
sixels=(@\$ A\$ C\$ G\$ O\$ _\$-)
pi=$(echo "scale=10; 4*a(1)" | bc -l)
run=true
trap run=false INT
echo -ne "\x1bP0;0;0q\"1;1#1;2;100;0;0#1"
y=0
while $run
do
x=$(echo "s(2*${pi}*${y}/${period})*${amplitude}+2*${amplitude}+0.5" | bc -l)
echo -ne "!${x%%.*}?${sixels[$((y%6))]}"
(( y++ ))
done
echo -e "\x1b\\"
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# GLF should move the text cursor downwards,
# even if no pixels were modified,
# when sixel scrolling is on.
echo -n $'\e[?80l' # Ensure sixel scrolling is on (disable DECSDM)
clear
echo "A test of Sixel GLF (Graphics Line Feed) when sixel scrolling is on"
echo
# Move cursor down three using GLF ("-")
echo -e '\x1bPq$-$-$-$-\x1b\\'
# Show a single sixel line that says, "Your terminal ->"
cat <<'EOF'
P0;0;0q"1;1;244;21#0;2;0;0;0#1;2;80;80;80#0~~~NFFFN~~~n!4FN!68~NFN!64~bbb!42~rbbbBB!41~$#1???owwwo???O!4wo!68?owo!64?[[[!42?K[[[{{-#0!5~{woBFB`o{}!4~NFBpp!4xpbBF~~~x@@@~~~xxp@@!5~xxp@@b`pxwxpz!17~zpp???!5px!5~NFBpxxwxxpBBN!4~xxp@@b`pxwxpz~x@@@xxp@@pxx@@F!4~xppp@@!8~x@@@pxxwxp@B!5~zxXXXWXX@BF!10~??!24~!10^FNN^~~~$#1!5?BFN{w{]NB@!4?ow{MM!4EM[{w???E}}}???EEM}}!5?EEM}}[]MEFEMC!17?CMM~~~!5ME!5?ow{MEEFEEM{{o!4?EEM}}[]MEFEMC?E}}}EEM}}MEE}}w!4?EMMM}}!8?E}}}MEEFEM}{!5?CEeeefee}{w!10?~~!24?!10_woo_-#0!5~nFFF??FFN!5~wo_FF!4NFb_o!4~{o_!4Ffb??Fn~~~NFF??!4FN!23~_??!5NFFn~~~w__C!7KCC!4~NFF??!4FN!4~N???N~~??Fn~???N~~N!4F??!4Fn~~~F???Fn~~nF??Fn~~p??KMMMEE???Fn~~~N!4F??!4Fn!29~{{}!4~$#1!5?Owww~~wwo!5?FN^ww!4ow[^N!4?BN^!4wW[~~wO???oww~~!4wo!23?^~~!5owwO???F^^z!7rzz!4?oww~~!4wo!4?o~~~o??~~wO?~~~o??o!4w~~!4wO???w~~~wO??Ow~~wO??M~~rpppxx~~~wO???o!4w~~!4wO!29?BB@-#0!25FE!63FE!15FE!77FE!60F$#1!25?@!63?@!15?@!77?@-\
EOF
sleep 1
tput cup 2 30
# Show four lines saying, "<- A genuine VT340 would end here"
cat <<'EOF'
P0;0;0q"1;1;144;80#0;2;0;0;0#1;2;80;80;80#0~~rpp@@`@BN!25~!11^!5~^^^N^^^!6~!7^N^^^!5~!4^~~~!5^!6~^^^WWW!8~!7^N^^^!8~^^^N^^^!5~$#1??KMM}}]}{o!25?!11_!5?___o___!6?!7_o___!5?!4_???!5_!6?___fff!8?!7_o___!8?___o___-#0~~NB??EFE??@F^!19~B??{!4}{W??}}~~B@?K!5MK??B~~~}???{!4}{??!4~}???~~~}}{??!6~}{{{??!8~}???{!4}{??!4~B@?K!5MK??B~~$#1??o{~~xwx~~}w_!19?{~~B!4@Bf~~@@??{}~r!5pr~~{???@~~~B!4@B~~!4?@~~~???@@B~~!6?@BBB~~!8?@~~~B!4@B~~!4?{}~r!5pr~~{-#0rpoopr~~~zpooopr!17~}{ww!4pwW??!4~}wwprrb!4rpp~~~pooopz~~zpoopz!4~{w!4pxwoopz~~~r!4poo!4pz~~~pooopz~~zpoopz~~}wwprrb!4rpp~~$#1KMNNMK???CMNNNMK!17?@BFF!4MFf~~!4?@FFMKK[!4KMM???MNNNMC??CMNNMC!4?BF!4MEFNNMC???K!4MNN!4MC???MNNNMC??CMNNMC??@FFMKK[!4KMM-#0!32~^NNNKWwww[[MNNN^~!13N!5~NN!5FNN^!10~N!4F!8~NN!4FN^!36~$#1!32?_ooorfFFFbbpooo_?!13o!5?oo!5woo_!10?o!4w!8?oo!4wo_-#0!32~}{o?AM!4~E?_w}}~o_o}}???}}}__!5~}}~fFFFAOw!6~^FB_w{???!6~B??}!4~{??!35~$#1!32?@BN~|p!4?x~^F@@?N^N@@~~~@@@^^!5?@@?Wwww|nF!6?_w{^FB~~~!6?{~~@!4?B~~-#0!36~{_?FB?o}!7~^NN???NN^!6~!8NE?_x!4~poooP@@???X!5~{o?F!4NB_o!35~$#1!36?B^~w{~N@!7?_oo~~~oo_!6?!8ox~^E!4?MNNNm}}~~~e!5?BN~w!4o{^N-#0!38~}}}!11~!8}!8~!6}!13~!6}!9~!4}!38~$#1!38?@@@!11?!8@!8?!6@!13?!6@!9?!4@-#0!5^!5~!5^!5~!7^!6~!4^~~~!5^!6~{www??!11~^^^N^\W???!22~^^^N^^^!6~!7^N^^^!8~^^^N^\W???~~$#1!5_!5?!5_!5?!7_!6?!4_???!5_!6?BFFF~~!11?___o_af~~~!22?___o___!6?!7_o___!8?___o_af~~~-#0}{??CMB@@FM??o{~~B@?{{!4}{w?@~~~}???~~~}}{??!10~??!8~B??{!5}{???!19~B@?K!5MK??B~~~}???{!4}{??!4~B??{!5}{???~~$#1@B~~zp{}}wp~~NB??{}~BB!4@BF~}???@~~~???@@B~~!10?~~!8?{~~B!5@B~~~!19?{}~r!5pr~~{???@~~~B!4@B~~!4?{~~B!5@B~~~-#0~~~woow~}woo{!4~}{wpprbrrpww{!5~{w!4pxwoopz~~~r!4poo!4pz~~~}{ww!5pwooopz!17~}wwprrb!4rpp~~~pooopz~~zpoopz~~}{ww!5pwooopz$#1???FNNF?@FNNB!4?@BFMMK[KKMFFB!5?BF!4MEFNNMC???K!4MNN!4MC???@BFF!5MFNNNMC!17?@FFMKK[!4KMM???MNNNMC??CMNNMC??@BFF!5MFNNNMC-#0!40~n!4F!99~$#1!40?O!4w-#0!10~^^n!29~???b!4rbBF!4~^NFbrrprrbFF^!4~rrbBBFBbrrrbv~~^NFbrrprrbFF^!42~$#1!10?__O!29?~~~[!4K[{w!4?_ow[KKMKK[ww_!4?KK[{{w{[KKK[G??_ow[KKMKK[ww_-#0!9~}{wq!10}!18~N???N^~~^N??N^~~o??H!6XWGG!4~^NN??!4N^!5~o??H!6XWGG!42~$#1!9?@BFL!10@!18?o~~~o_??_o~~o_??N~~u!6efvv!4?_oo~~!4o_!5?N~~u!6efvv-#0!41B!6ABB!6A!5BAAA?!6A!4B!10A!8BAAA?!6A!42B$#1!41?!6@??!6@!5?@@@B!6@!4?!10@!8?@@@B!6@$-\This text should be indented.
EOF
# For more details, please see:
# https://github.com/jerch/xterm-addon-image/issues/37
# By default, sixel is in a 2:1 aspect ratio, which means
# every sixel graphics linefeed (GLF) adds another 12 pixels.
# With four GLFs, we have five sixel lines = 5 * 12 = 60 pixels.
# Which text line that ends up on depends upon the height of your
# font. On the VT340, the font is 20 pixels high.
# On a VT340 sixel scrolling is on by default. DEC refers to this by
# two different names "sixel scrolling" and its negation, "DECSDM"
# (Sixel Display Mode). They control the same thing, so when DECSDM is
# on, sixel scrolling is off, and vice versa.
# When DECSDM is on, the graphic line feeds do not affect the text cursor.
# How hackerb9 created the two sixel test images:
#
# convert -family "Courier" -style normal -density 72 -pointsize 26 -interline-spacing -12 -gravity center -fill gray80 -background none label:$'A genuine\nVT340\nwould end\n← here ' +trim +dither -colors 2 sixel:- > reference.six
#
# convert -family "Courier" -style normal -density 72 -pointsize 26 -interline-spacing -12 -gravity center -fill gray80 -background none label:$'Your terminal →' +trim -bordercolor none -border 2 +dither -colors 2 sixel:- > yourterminal.six
@@ -0,0 +1,44 @@
const sixelEncode = require('../node_modules/sixel/lib/SixelEncoder').image2sixel;
const toRGBA8888 = require('../node_modules/sixel/lib/Colors').toRGBA8888;
function createRect(size, color) {
const pixels = new Uint32Array(size * size);
pixels.fill(toRGBA8888(...color));
return sixelEncode(new Uint8ClampedArray(pixels.buffer), size, size);
}
function createRectMinusOne(size, color) {
const pixels = new Uint32Array(size * size);
if (size - 1) {
const sub = new Uint32Array(size - 1);
sub.fill(toRGBA8888(...color));
const last = size * (size - 1);
for (let y = 0; y < last; y += size) {
pixels.set(sub, y);
}
}
return sixelEncode(new Uint8ClampedArray(pixels.buffer), size, size);
}
async function main() {
// clear + cursor and sixelScrolling off
process.stdout.write('\x1b[2J\x1b[?25;80h');
for (let i = 1; i < 300; ++i) {
await new Promise(res => setTimeout(() => {
process.stdout.write(createRect(i, [0, 255, 0]));
res();
}, 5));
}
for (let i = 299; i >= 1; --i) {
await new Promise(res => setTimeout(() => {
process.stdout.write(createRectMinusOne(i, [0, 255, 0]));
res();
}, 5));
}
// re-enable cursor and sixel scrolling
process.stdout.write('\x1b[2J\x1b[?25;80l');
}
main();
@@ -0,0 +1 @@
]1337;File=inline=1;size=525;name=Li4vcGFsZXR0ZS5wbmc=:iVBORw0KGgoAAAANSUhEUgAAAoAAAABQCAYAAACJbMQlAAAABmJLR0QA/wD/AP+gvaeTAAABwklEQVR4nO3dwQnEMAwAQedIIem/yVwLBj0C3pm3uAO/Fj2ia631rh3P1lRu7vnof0+Z836zOe83m/N+sznvN5vzfrM57zeb+23+HAAAhxCAAAAxAhAAIEYAAgDECEAAgBgBCAAQIwABAGIEIABAjAAEAIi59z8tDQDACWwAAQBiBCAAQIwABACIEYAAADECEAAgRgACAMQIQACAGAEIABAjAAEAYlwCAQCIsQEEAIgRgAAAMQIQACBGAAIAxAhAAIAYAQgAECMAAQBiBCAAQIwABACIcQkEACDGBhAAIEYAAgDECEAAgBgBCAAQIwABAGIEIABAjAAEAIgRgAAAMQIQACDGJRAAgBgbQACAGAEIABAjAAEAYgQgAECMAAQAiBGAAAAxAhAAIEYAAgDECEAAgBiXQAAAYmwAAQBiBCAAQIwABACIEYAAADECEAAgRgACAMQIQACAGAEIABAjAAEAYlwCAQCIsQEEAIgRgAAAMQIQACBGAAIAxAhAAIAYAQgAECMAAQBiBCAAQIwABACIcQkEACDGBhAAIEYAAgDECEAAgBgBCAAQIwABAGIEIABAjAAEAIgRgAAAMQIQACDmDz4lC02AaqMRAAAAAElFTkSuQmCC
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
]1337;File=inline=1;size=1667;name=Li4vdGVzdGltYWdlcy93M2NfaG9tZV8yNTYuZ2lm:R0lGODlhSAAwAPcAAAQCBIyKjMzOzERGRHSOvAxCnLy6vNzm9DRmrCQiJGxubNze3HSWxJSq1JyenCRarPz+/FR+vBweHLzG5FxeXGSKxBROpOzu7ERytDw+PHx+fLTC5AwODIyOjExOTAxGnLy+vDxqtCwuLHR2dNTa7ISezKS23KyurMTO5GyKxOzy/ACCXwAAN7AAF+IAjIEAw8DPArtsAOSupIHW2HBvAsUBAHIAbwAAARwAiEdgA/caAr+CAgCIALAjAOMcJIGCAAB8EADGAAByiAAArcQcSrtHBOT3zYG/AaQA7sVgE3Ib9wCCv88AZ2wAAaIAANUAAG+M4wEjogAc9wCCv8SwGbvGBeRyAIEAAF/PrBZsxwCucgDWAIRvpMUBaHIAAQAAAFCMfEIjj/kcUb+CAAC4sLAZKOL3AIG/AECQoAfGowBy9wAAvwBQAABCAAD5UAC/ALgALBlguPcaUb+CAMRwpNUFaAAAAQAAAOAAAPgAAHIAAAAAAG24DBgZAPf3UL+/AM/QAGzWAKIAUNUAAADgfAD4jwByUQAAAKJtQNUYAH/3AFe/AGTPABNsAACuAADWAAAAMgAABUYAAAIAAACuGQDWBQh/AABXAAJH1AABx8QAcgsAAAMAUAAApTdG9wYCv3oAADMAAKUBULAAAEcCZwEApQCM9wDDv0ACAAcAAABfUAAWAKUAQ7AAAEelAAGwAPhHQ8UBAHIAAAAAABhwFIkFxX8AUIEAABClAtKwADxHAIIBABAUopPXKKZm99Iov/gf+MUXx3IAcgAAAAxwZsYFtHIA9wAAvxgAAIlgAH8aUIGCAHiM1QrDtD0C94IAvxAAAJMASqYChdIAgQxw78YFGXIA+AAAvw4AM6EAGvcA+L8AvyYAALMASveNhb+tgQBKLGAEuDzNUYIBAADRmwABbgAA9wAAv8+kFmzYxWYCUNYAAG+k4wHYKAACQgAAAAAAHmAAxTsoUIIAADwcvcZ6bnIC9wAAvxgAm4kAbn8A94EAv0QABAoAAD0MAIIXACH5BAAAAAAALAAAAABIADAABwj/AAEIHEiwoMGDCBMqXGgQgsOHECNKnEixosWLGDNq3Mixo8ePIEOKHCkxgMkRBkiq7BhAgEmXAlbKxBgAwoALAyDUnMlzYs0EA1LuVIGiKAoVEg8YRXFxQoOnJpZKLYpUY0wII2wKGLBg54EPYD+YkEgi7AemFS2AVRuW7Qe3aB8K8ACAA4UFDy+IcGhg54URFx4+CFthIlsLDCqiKGD2bWOzcSE4MOjAoQYAck0GCPyQweGJFcI+qMiA8eO2azFAFIDQwAUOAEBcRNGWhEQTZg9QRFDg8NrTkSUglKBAYOWLbhNHVGG2wcSvYVNE3GC28MPJA0VoUJCAIAfOFiOE/0UwEYPjCBMbmK0quC17CBkGHodwgcJADRlxO9YdMXTY9w6Jx1gIEZXg2AclQHSBfBLZ911GzIU1VkRufTCBRL8595AKbo0G0QkCUUCRdhuFEBZ6EJFQoXUPTWCWbQ/5B9aFEF0GAF4TCQAeRuqFVeBvH3gYo2gQ0eaYahHR5cFKZTm2AUQIPAajQ4OBpZxD5oXFH0TxzUdSlR9c+VWFFiToUJNgPemQfmCxyCUAO44ko5BsukWeQz1aYMGGg7kVwpQOZSDBTC46BqN4bxn4n0MmgoUiBD2iJhaXGfDUnENuHSCeWmNFCNaEEJRQoVmgalDpTIh+oBp1YJEXKXpsfv+wJQol1IqAbxZU5cCpMuHGlgoMrJUYdG9BEJpaBFKEQoVmLsDrSsSKBSaMYC4blpkUoVmsZT1FCVaj20Lg2bcvXnQsWDDKxtO4jbFoZGNCVsTqWxr2BEGTvtGIqaRuZmsWtvaO+sF7Moalr0VuAdxTwR88uuaBYAH43G8pqItRnBkVGla9DnmqlsOK/btkRhnguFGEbG35UJZg1TtBBHdGFCxbKHSA30UBAHCCRywnG1GksjrkrYXLgTnwAgB4KZEAsN3MUaRXQkSszzJaAGiwJzpUnNJywZa0R9BZEBlED6glpltlokDCuW+h9RoAO0EUgNd7fdQnaWBFpqjAWT9gBCIAxJ1wggLCCcSByR0F22+R4a7cWIUIAIjdQRxc9RFtoBq2uAopnPZBCBJL5jVBElgOUq4WVZB5kZt+u3pECygggkAZdICxvbjnrvvuvPfu++/ABy88Q8QXb/zxAgUEADs=
@@ -0,0 +1 @@
]1337;File=inline=1;size=2159;name=Li4vdGVzdGltYWdlcy93M2NfaG9tZV8yNTYuanBn:/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAwAEgDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD33/hXPgn/AKE7w3/4K4P/AImj/hXPgn/oTvDf/grg/wDia8y/aLuNX0PWtB1nS7+8ghcGJ4o5mWPejbhlQcHIYj6LXr07J4l8IO1jM8K6lZboZUbayb0yrAjoRkH8K6J4flpwqX0l+BhGrzSlC2qM7/hXPgn/AKE7w3/4K4P/AImj/hXPgn/oTvDf/grg/wDia81/Zt8S313da3ousXk9xcR7biLz5S7DB2OMknvsrov2ifEEujeB0trSd4bq/uFiDRsVYIvzMQR9FH/AquWDlHELD3+ZMcQnS9qdR/wrnwT/ANCd4b/8FcH/AMTR/wAK58E/9Cd4b/8ABXB/8TXhFr4F8dL4SXX5fFCWOntbfays1/OrqhGRkKpGSMcZzyB1q7+z9D4g8QeKzqF3q2pSaZpwzIsty7LJIwIVME4Pcn0wPUVvPL4xhKaqJqJnHFNyUXFq57V/wrnwT/0J3hv/AMFcH/xNH/CufBP/AEJ3hv8A8FcH/wATXgnxP8YePPiB8Sr/AMI/DKe8tbLSG8u4ntZ/ILSKcOzy5BVQ2VCg87ScHoF0/wAcfE3T9Z8O/D7xJZSNr0mq20635YYuLJG3upZeGHycsOdoYEZ6+Ydh6Pd2fgW38Qvpn/CA+G2EaNKzfYoPMEStIrPs8voDDJ37D+JlUld9ceFNNnvzcyISGfzGjKoQW+Yn5iu8Al3yoYA73yPmbJQBzvx30X+2fhtqRRN01jtvE9tmdx/74L1ynw18Z/Y/gVqN20n+laMktuhJ5y3MX4ZdV/CvZ7qCO6tpredQ8UqGN1PdSMEfrXxPqb6hoD674WDExveKkygHLmJnC4Hvuz74Fexl8FiaToy6NP5dTz8VJ0Zqouqa/wAjb+Ed/P4c8f8Ah++uEeO0v2MIc8K6Oxjzn0DgH/gNdt8bpH8V/FnQ/DFu5KQeXC+P4GlIZz+CbD+FT/Grwf8A2L8NvCk1uu2fSQtvMyccuNzN/wB/F/8AH6p/AeO48V/E/VfE2ohWkgRpiQOFkk+VQPYLvH4V3SqQmnjV0TXzvoc6jKP+zPq0/wDM0P2k/FRt47Twnp+Y4QizXOOAQPuR/QY3H/gNdx8BtQ8PS+CoLDQJibm3G+9jlULIZW6uR3BxgEE8ADqK7nUdG0zUlcajp1pdb12t50KuSPqRXzl4bgTwP+0C2m2EhSwMxgKs/HlSR7wpP+ySp5/u1wUnDE4Z0Y6OKv6nTNSpVlUeqenoej/s26IuneAZdSmX/iY6xf3N3cuw+YkSsgB+gXOOxY16bNY2s17b3k1vE91bh1hlZQWjDY3BT2zgZ+lQ6JpcGj2H2O0GIfOllA9PMkaQgewLEVeJx1ryDvFooooAK8K8X+C/tf7QWjTCPNneKt9KSPl3QjDD8dsf/fdel/8ACxvBP/Q4+G//AAaQf/FVG3j/AMCNcJO3i3wwZ0VkSQ6lBuVWILAHdkA7VyO+B6V0YevKg249U0ZVaSqJJ9Hcu/EDRf8AhIfBesaYF3yT27eUP+mi/Mn/AI8BXm3wa8Panb/CDUbjRZ/sms6mzy28xUEgJ8qDDDAztbntuzXoH/CxvBP/AEOPhv8A8GkH/wAVUdv4/wDAltCkNv4t8MRRIMKialAqj6ANTp4mUKTpLun939IUqKlPn8rHkXg340XnhuwuNJ8Y2N9eX9s77ZWbEuSSdkm70PQjtgY4ql8KdCu/iD4+1LxNrVsTppMpkyPkd3QoI1Pfarfhgetet6n4l+F+qzCbVNa8F3soG0PcXVrIwHpliavW3j7wHawJBa+LPC8MKDCRx6jAqqPQANgV1SxtNRl7KFpS3/4BjHDTuueV0tjg0+LX/CvL5/DfxPivRNCT9i1iKHzI7+AfddgORJjAYAEZ54yM8xd/ETV/jN4v0zw/4Btrux8PWV3DealqU67WZY3DKMAkAZXhc5YgdADXrereMPhxrNr9m1fxF4Qv7bOfKur22lTPrhmIpdK8ZfDnSLRbXSfEfhGxtV5ENtfW0SD/AICrAV5h2HbUVyv/AAsbwT/0OPhv/wAGkH/xVFAH/9k=
@@ -0,0 +1 @@
]1337;File=inline=1;size=1658;name=Li4vdGVzdGltYWdlcy93M2NfaG9tZV8yNTYucG5n:iVBORw0KGgoAAAANSUhEUgAAAEgAAAAwCAMAAACFQszZAAADAFBMVEUEAgSMiozMzsxERkR0jrwMQpy8urzc5vQ0ZqwkIiRsbmzc3tx0lsSUqtScnpwkWqz8/vxUfrwcHhy8xuRcXlxkisQUTqTs7uxEcrQ8Pjx8fny0wuQMDgyMjoxMTkwMRpy8vrw8arQsLix0dnTU2uyEnsykttysrqzEzuRsisTs8vwAgl8AADewABfiAACBAAlwzwPVbADkrqSB1thwbwLFAQByAG8AAAEcADBHYAf3GgK/ggIA/ACwaADjHByBggAAfA4AxgAAchQAACB0HEbVRwTk98WBvwGkAO7FYBNyG/cAgr/PAGdsAAGiAADVAABvAOMBaaIAHPcAgr90sBXVxgXkcgCBAABfz6wWbMcArnIA1gCEb1TFARNyAAAAAABQANRCaU35HFG/ggAAuLCwGSji9wCBvwD4kKAIxqMAcvcAAL8AUAAAQgAA+VAAvwC4AIQZYHb3GlG/ggDElFTVDxMAAAAAAADgAAD4AAByAAAAAABtuAwYGQD391C/vwDP0ABs1gCiAFDVAAAA4NQA+E0AclEAAACibUDVGAB/9wBXvwBkzwATbAAArgAA1gAAABgAAAVGAAACAAAArhUA1gUIfwAAVwADR9QAAcd0AHIlAAADAFAAAKU3RvcGAr96AAAzAAClAVCwAABHA2cBAKUAAPcACb/4AwAIAAAAX1AAFgClAEOwAABHpQABsAD4R0PFAQByAAAAAAAYlByJD8V/AFCBAAAQpQLSsAA8RwCCAQAQFKKT1yimZvfSKL/4H/jFF8dyAHIAAAAMlGbGD7RyAPcAAL8YAACJYAB/GlCBggB4ANUKCbQ9A/eCAL8QAACTAEqmA4XSAIEMlO/GDxlyAPgAAL8OADOhABr3APi/AL8mAACzAEr3GoW/IIEARoRgBHY8xVGCAQAAz5sAAW4AAPcAAL/PpB5s2MVmAlDWAABvpOMB2CgAAkIAAAAAABZgAMU7KFCCAAA8HL3Gem5yAvcAAL8YAJuJAG5/APeBAL9EAAQKAAA9DACCFwCk+0jYAAAACXBIWXMAAAAAAAAAAACdYiYyAAADIElEQVR4nO2WW3OqQAyAAUehArJWXcDSUiwg3f//A8/msheU0/bBhzNnutMOEZKPJJtkCYJHLfGg9V+CwjD82DwAFEah/oseABKrciXCR4CeVhsEqUkvRXdjkD2tQ57n4+SWuqFEQnyIVbRKARS3eo30oAfZI8m2lXBL8j89it6C5phqobwIsQn19aOE0AqtcfIsM8uZ1q3B0ELQjkp5J8QZzHWywxJ3LQNrNj1p7cKCsrVFAE1W6I9pik3ZBO9GFUATqPX0ewSb2DxN1hSb9B16NqDnLbrlQBBPy/Eo0M/5IWbvE8WX1sSPgV3O2ye4NuUMtNdKCd+o9Ov3LOdgTRsFeZQo1pQeUR61cBYz0AjOxy5JbKxfsG47lAaIbgCpDGw8R88hAimvADAdB5KBj2EqaffgGgRHY3xxDnGvdVqP4umlTYY4gNhbN4l+DoLUGEflLQiTwTFAEIWNEqUJblZk8Ba8iaVFoB40X0BKaJ/Rj8JsZuXVRO12fAFkbWLuiEFwuwB9dNECqPwKZKIYuSESildCwasCb3VUsvXzIseADmDfY0XJgQvA7EBuOgT3ta6/BAkuaHh1vNdWo6uJwfYs/Dp/A4LirrAREnRh77puGoYhwYaD0t59AxrBd5XhEIEWkzptkssa1oRu6T1IvwHRdCto5+GCloNTxD2E3J+XMN5xBAXUsS4MqM6WEy/wEG6830NmoIxTCvUycXILXxPyJ/M7wB2o5wmGHcUj+uRr9u081r+BBHukKIzWGwKewk9AZEwzYKROmR0+MTj5eZOhm3mEC4cGT1mFoe1nNpi44ab1aztSHEhhWrjHKwM97M0MzuDx9Drb/TC43oPQ2FRgzmWd2EypAoNN/SkSNW5oe6DcnSVYnwCFxNFJlXEGt46kOTfHkTWW9rQuJM0nLIRh6rEc4bE+FEOOS3MuYgGkO0NaOePTcPAPbMz+FU7G63UL52STLoIyrwInA63MNNJzgcphZz9km0gsgiZzJMGSDFWfxp/OlNWu4SPb48w//aRXgScLnWDQtZ33knR7CYL6dTa8/6Wv2l/Qj0GPWn8AP2jx/kucqREAAAAASUVORK5CYII=
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
function print_palette() {
L=$(( LOWER / 256))
U=$(( (UPPER-1) / 256))
for ((p = $L; p <= $U; p++))
do
echo "slot $((p*256))..$((p*256+255)):"
echo -ne "\x1bP;1q"
for i in {0..15}
do
a=$((i * 16 + p * 256))
for j in {0..15}
do
echo -ne "#$((a+j))!6~"
done
echo -ne "\$-"
done
echo -e "\x1b\\"
done
}
colors=undefined
max_colors=undefined
echo "Terminal Reports (XTSMGRAPHICS):"
IFS=";" read -a REPLY -s -t 1 -d "S" -p $'\e[?1;1;0S'
[[ ${REPLY[1]} == "0" ]] && colors=${REPLY[2]}
echo "active colors: ${colors}"
IFS=";" read -a REPLY -s -t 1 -d "S" -p $'\e[?1;4;0S'
[[ ${REPLY[1]} == "0" ]] && max_colors=${REPLY[2]}
echo "max colors : ${max_colors}"
echo
# query up to colors by default
# if colors is undefined (no XTSMGRAPHICS), assume 256
ARG1=${1:-${colors}}
if [[ $colors == "undefined" ]]
then
ARG1=${1:-256}
fi
LOWER=0
UPPER=$ARG1
ARG2=${2:-undefined}
if [[ $ARG2 != "undefined" ]]
then
LOWER=ARG1
UPPER=ARG2
fi
if [[ $colors != "undefined" ]]
then
if [[ $colors -lt $UPPER ]] || [[ $colors -lt 256 ]]
then
echo -e "\x1b[33mNote: Active colors is smaller than test range."
echo -e "A spec-conform terminal may repeat colors in 'slot mod ${colors}'.\x1b[m"
echo
fi
else
echo -e "\x1b[33mNote: Cannot query active colors."
echo -e "The terminal may repeat colors beyond it max slot (e.g. slot mod 16).\x1b[m"
echo
fi
print_palette
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
function smiling_smiley() {
echo -ne '\x1bP;2q"1;1;60;60
#6!60~$-
!60~$-
!60~$
!15?#1!4]!22?!4]$-
#6!60~$-
!60~$-
!60~$-
!60~$
!15?#1!4~!22?!4~$-
#6!60~$
!15?#1!30N$-
#6!60~$-
!60~$-
'
echo -ne '\x1b\\'
}
function indifferent_smiley() {
echo -ne '\x1bP;2q"1;1;60;60
#6!60~$-
!60~$-
!60~$
!15?#1!4]!22?!4]$-
#6!60~$-
!60~$-
!60~$-
!60~$-
!60~$
!15?#1!30N$-
#6!60~$-
!60~$-
'
echo -ne '\x1b\\'
}
function sad_smiley() {
echo -ne '\x1bP;2q"1;1;60;60
#6!60~$-
!60~$-
!60~$
!15?#1!4]!22?!4]$-
#6!60~$-
!60~$-
!60~$-
!60~$-
!60~$
!15?#1!30N$
!15?#1!4o!22?!4o$-
#6!60~$
!15?#1!4B!22?!4B$-
#6!60~$-
'
echo -ne '\x1b\\'
}
function smiling_smiley_slim() {
echo -ne '\x1bP;1q"1;1;60;60
$-
$-
$-
$-
$-
$-
!15?#1!4~!22?!4~$-
!15?#1!30N
'
echo -ne '\x1b\\'
}
function indifferent_smiley_slim() {
echo -ne '\x1bP;1q"1;1;60;60
$-
$-
$-
$-
$-
$-
!15?#6!4~!22?!4~$-
!15?!4o!22?!4o$ !15?#1!30N$-
!15?#6!4B!22?!4B
'
echo -ne '\x1b\\'
}
function sad_smiley_slim() {
echo -ne '\x1bP;1q"1;1;60;60
$-
$-
$-
$-
$-
$-
$-
!15?#1!30N$
!15?#1!4o!22?!4o$-
!15?#1!4B!22?!4B$-
'
echo -ne '\x1b\\'
}
function full() {
smiling_smiley
sleep .5
indifferent_smiley
sleep .5
sad_smiley
sleep .5
indifferent_smiley
sleep .5
smiling_smiley
}
function slim() {
smiling_smiley
sleep .5
indifferent_smiley_slim
sleep .5
sad_smiley_slim
sleep .5
indifferent_smiley_slim
sleep .5
smiling_smiley_slim
}
# clear screen and place cursor to 1;10
echo -ne '\x1b[2J\x1b[10;1H'
# switch sixel scrolling off
echo -ne '\x1b[?80h'
case "$1" in
full ) full ;;
slim ) slim ;;
esac
# re-enable sixel scrolling
echo -ne '\x1b[?80l'
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+288
View File
@@ -0,0 +1,288 @@
#!/bin/bash
# Test cursor row, column placement after sixel image is sent.
# After a sixel image is displayed, the text cursor is moved to the
# row of the last sixel cursor position, but the column stays the same
# as it was before the sixel image was sent.
#
# This can be thought of as sixel images always ending with an
# implicit Graphics Carriage Return (`$`).
# ADDENDUM: It is not as simple as I thought. When a row of sixels
# straddles two rows of text, the text cursor can be left on the upper row.
# It seems up to three lines of pixels may be beneath any words printed.
#
# The rule for when this happens is not obvious to me, but can be seen
# with images of height: 21, 22, 23, 24, 41, 42, 81, 82, 83, 84...
#
# My guess:
# for a sixel image of height h, let a=(h-1)%6 and b=(h-1)%20,
# then, the text will overlap the image when a>b.
#
# If that is the case, then the entire list of heights for which this
# will happen on the VT340's 480 pixel high screen is:
#
# 21 22 23 24 41 42 81 82 83 84
# 101 102 141 142 143 144 161 162
# 201 202 203 204 221 222 261 262 263 264 281 282
# 321 322 323 324 341 342 381 382 383 384
# 401 402 441 442 443 444 461 462
#
# Note that there are 48 entries, so that means there's a 10% chance
# if heights are chosen randomly from 1 to 480. However, if one were
# to always pick heights which are a multiple of the character cell
# height (20px), then the chances are 0% as there are no problematic
# heights divisible by 20.
# Sixel images often do *not* end with a `-` (Graphics New Line = GNL)
# which sends the sixel cursor down 6 pixels. Any text printed next
# will potentially overlap the last row of sixels!
# I am not yet positive, but I believe that, in general, applications
# should send sixel images without a GNL but then send `^J`, a text
# newline (NL), before displaying more text or graphics.
# IMPORTANT: sometimes neither a graphics nor a text newline is wanted.
# For example, if an image is full screen, either newline would cause
# the top line to scroll off the screen.
# | Text cursor column | Text cursor row
# --------|--------------------|-------------------------------------
# !GNL !NL| Unchanged | Overlapping last line of graphics
# !GNL NL| Column=1 | First line immediately after graphic (usually)
# GNL !NL| Unchanged | _Sometimes_ overlapping graphics
# GNL NL| Column=1 | First *or* second line after graphic
CSI=$'\e[' # Control Sequence Introducer
DCS=$'\eP' # Device Control String
ST=$'\e\\' # String Terminator
set_cursor_pos() {
# Home, top left is row 1, col 1.
local row=$1 col=$2
echo -n ${CSI}${row}';'${col}'H'
}
reset_palette() {
# Send DECRSTS to load colors from a Color Table Report
echo -n ${DCS}'2$p'
echo -n "0;2;0;0;0/" # VT color #0 is black and BG text color
echo -n "1;2;20;20;79/" # VT color #1 is blue
echo -n "2;2;79;13;13/" # VT color #2 is red
echo -n "3;2;20;79;20/" # VT color #3 is green
echo -n "4;2;79;20;79/" # VT color #4 is magenta
echo -n "5;2;20;79;79/" # VT color #5 is cyan
echo -n "6;2;79;79;20/" # VT color #6 is yellow
echo -n "7;2;46;46;46/" # VT color #7 is gray 50% and FG text color
echo -n "8;2;26;26;26/" # VT color #8 is gray 25%
echo -n "9;2;33;33;59/" # VT color #9 is pastel blue
echo -n "10;2;59;26;26/" # VT color #10 is pastel red
echo -n "11;2;33;59;33/" # VT color #11 is pastel green
echo -n "12;2;59;33;59/" # VT color #12 is pastel magenta
echo -n "13;2;33;59;59/" # VT color #13 is pastel cyan
echo -n "14;2;59;59;33/" # VT color #14 is pastel yellow
echo -n "15;2;79;79;79" # VT color #15 is gray 75% and BOLD text color
echo -n ${ST} # String Terminator
}
# Generate square of size w with final graphics new line removed
square() {
# Given a color index number and (optionally) a size, row, and column,
# draw a square with top left corner at (row, column) and of size×size px.
# Default size 100×100px (10cols, 5 rows)
local -i color=${1:-1} # Default is color index 1 (blue)
local -i size=${2:-100} # Size in pixels (defaults to 100)
local -i row=$3 column=$4 # If set to 0, cursor is not moved
if [[ row -ne 0 && column -ne 0 ]]; then
set_cursor_pos $row $column
fi
# Draw a square of the right color & size
squaresize $color $size
}
squaresize() {
# Helper for square() that uses convert to return a sixel square of
# the right color ($1) and size ($2).
# Similar to this but with variable size squares:
# echo -n ${DCS}'0;0;0q"1;1;100;100#'${color}'!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100N'${ST}
local color=${1:-1} # Default color index is 1 (blue)
local size=${2:-100} # Default size is 100x100
# Get a sixel string
local sq=$(convert -geometry ${size}x${size} xc:black sixel:-)
# Remove ImageMagick's extraneous Graphic New Line at end of image.
sq=${sq%-??}$'\e\\'
# VT340s always used the same color register for the first sixel
# color defined no matter what number it was assigned. That means,
# each time we send a new sixel image, the previous one's color
# palette gets changed. We don't want squares of all the same
# color, so remove the color definition and just use the defaults.
sq=${sq/\#0;2;0;0;0/}
# And finally, switch to the proper index for the color we want.
echo -n ${sq//\#0/#${color}}
}
squaregnl() {
# Same as square(), but sends a graphics newline at the end of the sixels.
# (Sticks a `-` before the String Terminator, "Esc \")
sq=$(square "${@}")
echo -n ${sq%??}$'-\e\\'
}
main() {
clear
reset_palette
show_labels
neither_graphic_nor_text 96 4 4 31 # size, color, row, column
text_newline_only 96 9 4 1
text_newline_only 84 9 4 14
graphics_new_line_only 100 1 4 51
graphics_new_line_only 96 1 4 64
set_cursor_pos 1000 1
}
neither_graphic_nor_text() {
# Typically sixel images should not end with a Graphics New Line (GNL)
# However, if a text newline isn't sent, there will be overlap.
local -i size color row column
read size color row column <<<"$@"
set_cursor_pos $((row++)) $column
echo -n "Height $size"
set_cursor_pos $((row++)) $column
# Three squares sent as separate sixel images, indented +1
for i in {1..3}; do
square $((color++)) $size
tput cuf 1
done
echo -n "overlap?"
}
text_newline_only() {
# USING A TEXT NEWLINE (NL) after an sixel image that does NOT
# have GNL is probably the best way to be on the text line
# immediately below the image. However, the text will still
# occasionally overlap the last four rows of pixels.
# Also, if multiple images are intended to be shown, there will
# usually be a gap between them when using a text newline.
# Overlap happens because the height of a text cell is 20 pixels
# and the height of a sixel is 6.
# for (h=0; h<480; h++) if ((h-1)%6 > (h-1)%20 ) { h }
# Let the pixel position of the top of the graphics cursor be 'Yg'
# and let the pixel position of the top of the corresponding cell
# of text which the text cursor will be placed on be 'Yt'. Note
# that Yg is evenly divisible by 6 and Yt, by 20. Taking the
# remainder, r, after dividing Yg by 20 tells us how many pixels
# down into a row of text the last line of sixels started. When
# r==0, the sixels started at the top of the text row.
# When r = 14, the sixels covered the bottom six pixels on the row
# of text. When 14 < r < 20, the sixel line impinged by r - 14
# pixels into the text row below and there is a chance the next
# text printed will overlap.
local -i size color row column
read size color row column <<<"$@"
local -i offset
offset=$((column-1))
set_cursor_pos $row 1
if ((offset)); then tput cuf $((offset)); fi
echo "Height $size"
# Three squares, separated by text new lines and indented +1
for i in {1..3}; do
if ((offset)); then tput cuf $offset; fi
square $((color++)) $size
offset=offset+1
echo
done
tput cuf $((offset))
echo -n "overlap?"
}
graphics_new_line_only() {
# However, some sixel images end with a `-`, a Graphics New Line.
# This can be useful for writing another image starting at the same
# column without having to reposition the cursor.
#
# However, this runs the risk of having occasional overlap.
local -i size color row column
read size color row column <<<"$@"
set_cursor_pos $((row++)) $column
echo -n "Height $size"
set_cursor_pos $((row++)) $column
# Three squares, separated by graphics new lines and indented +1
for i in {1..3}; do
squaregnl $((color++)) $size
tput cuf 1
done
echo -n "overlap?"
}
show_labels() {
set_cursor_pos 1 10
echo -n "Should sixel images include a GNL ('-') at the end?"
set_cursor_pos 3 29
echo -n "Neither NL nor GNL"
set_cursor_pos 3 1
echo -n "Text New Line only"
set_cursor_pos 3 51
echo -n "Graphics New Line only"
set_cursor_pos 22 29
echo -n "Always overlaps" # Neither NL nor GNL
set_cursor_pos 22 3
echo -n "Overlaps a little" # NL only
set_cursor_pos 23 3
echo -n " Gaps a little" # NL only
set_cursor_pos 22 54
echo -n "Overlaps badly" # GNL only
set_cursor_pos 23 54
echo -n "Never gaps" # GNL only
}
main
+31
View File
@@ -0,0 +1,31 @@
{
"name": "xterm-addon-image",
"version": "0.6.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-image.js",
"types": "typings/xterm-addon-image.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"image",
"sixel",
"xterm",
"xterm.js"
],
"scripts": {
"prepackage": "../../node_modules/.bin/tsc -p .",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^5.2.0"
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
}
}
+161
View File
@@ -0,0 +1,161 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// eslint-disable-next-line
declare const Buffer: any;
// limit hold memory in base64 decoder
const KEEP_DATA = 4194304;
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
name: 'Unnamed file',
size: 0,
width: 'auto',
height: 'auto',
preserveAspectRatio: 1,
inline: 0
};
export class IIPHandler implements IOscHandler, IResetHandler {
private _aborted = false;
private _hp = new HeaderParser();
private _header: IHeaderFields = DEFAULT_HEADER;
private _dec = new Base64Decoder(KEEP_DATA);
private _metrics = UNSUPPORTED_TYPE;
constructor(
private readonly _opts: IImageAddonOptions,
private readonly _renderer: ImageRenderer,
private readonly _storage: ImageStorage,
private readonly _coreTerminal: ITerminalExt
) {}
public reset(): void {}
public start(): void {
this._aborted = false;
this._header = DEFAULT_HEADER;
this._metrics = UNSUPPORTED_TYPE;
this._hp.reset();
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data, start, end)) {
this._dec.release();
this._aborted = true;
}
} else {
const dataPos = this._hp.parse(data, start, end);
if (dataPos === -1) {
this._aborted = true;
return;
}
if (dataPos > 0) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
this._aborted = true;
return;
}
this._dec.init(this._header.size);
if (this._dec.put(data, dataPos, end)) {
this._dec.release();
this._aborted = true;
}
}
}
}
public end(success: boolean): boolean | Promise<boolean> {
if (this._aborted) return true;
let w = 0;
let h = 0;
// early exit condition chain
let cond: number | boolean = true;
if (cond = success) {
if (cond = !this._dec.end()) {
this._metrics = imageType(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._metrics.height;
if (cond = w && h && w * h < this._opts.pixelLimit) {
[w, h] = this._resize(w, h).map(Math.floor);
cond = w && h && w * h < this._opts.pixelLimit;
}
}
}
}
if (!cond) {
this._dec.release();
return true;
}
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
this._dec.release();
if (!window.createImageBitmap) {
const url = URL.createObjectURL(blob);
const img = new Image();
return new Promise<boolean>(r => {
img.addEventListener('load', () => {
URL.revokeObjectURL(url);
const canvas = ImageRenderer.createCanvas(window.document, w, h);
canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
this._storage.addImage(canvas);
r(true);
});
img.src = url;
// sanity measure to avoid terminal blocking from dangling promise
// happens from corrupt data (onload never gets fired)
setTimeout(() => r(true), 1000);
});
}
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
.then(bm => {
this._storage.addImage(bm);
return true;
});
}
private _resize(w: number, h: number): [number, number] {
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
const rw = this._dim(this._header.width!, width, cw);
const rh = this._dim(this._header.height!, height, ch);
if (!rw && !rh) {
const wf = width / w; // TODO: should this respect initial cursor offset?
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
const f = Math.min(wf, hf);
return f < 1 ? [w * f, h * f] : [w, h];
}
return !rw
? [w * rh / h, rh]
: this._header.preserveAspectRatio || !rw || !rh
? [rw, h * rw / w] : [rw, rh];
}
private _dim(s: string, total: number, cdim: number): number {
if (s === 'auto') return 0;
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
return parseInt(s) * cdim;
}
}
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser';
const CASES: [string, IHeaderFields][] = [
['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}],
['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}],
// utf-8 encoding in name
['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}],
['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}],
// full header spec
[
'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
{
inline: 1,
width: '10px',
height: '20%',
preserveAspectRatio: 1,
size: 123456,
name: 'ümläutß'
}
],
[
'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
{
inline: 1,
width: 'auto',
height: '20',
preserveAspectRatio: 1,
size: 123456,
name: 'ümläutß'
}
]
];
function fromBs(bs: string): Uint32Array {
const r = new Uint32Array(bs.length);
for (let i = 0; i < r.length; ++i) r[i] = bs.charCodeAt(i);
return r;
}
describe('IIPHeaderParser', () => {
it('at once', () => {
const hp = new HeaderParser();
for (const example of CASES) {
hp.reset();
const inp = fromBs(example[0]);
const res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, example[1]);
}
});
it('bytewise', () => {
const hp = new HeaderParser();
for (const example of CASES) {
hp.reset();
const inp = fromBs(example[0]);
let pos = 0;
let res = -2;
while (res === -2 && pos < inp.length) {
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
}
assert.strictEqual(res, 1);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, example[1]);
}
});
it('no File= starter', () => {
const hp = new HeaderParser();
let inp = fromBs('size=123456;name=dGVzdA==:');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, -1);
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty key - error', () => {
const hp = new HeaderParser();
let inp = fromBs('File=size=123456;=dGVzdA==:');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, -1);
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty size value - set to 0', () => {
const hp = new HeaderParser();
let inp = fromBs('File=size=;name=dGVzdA==:');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, {name: 'test', size: 0});
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty name value - set to empty string', () => {
const hp = new HeaderParser();
let inp = fromBs('File=size=123456;name=:');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, {name: '', size: 123456});
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, CASES[0][1]);
});
it('empty size value - error', () => {
const hp = new HeaderParser();
let inp = fromBs('File=inline=1;width=;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:');
let res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, -1);
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
assert.strictEqual(res, inp.length);
assert.strictEqual(hp.state, HeaderState.END);
assert.deepEqual(hp.fields, CASES[0][1]);
});
});
@@ -0,0 +1,186 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
// eslint-disable-next-line
declare const Buffer: any;
export interface IHeaderFields {
// base-64 encoded filename. Defaults to "Unnamed file".
name: string;
// File size in bytes. The file transfer will be canceled if this size is exceeded.
size: number;
/**
* Optional width and height to render:
* - N: N character cells.
* - Npx: N pixels.
* - N%: N percent of the session's width or height.
* - auto: The image's inherent size will be used to determine an appropriate dimension.
*/
width?: string;
height?: string;
// Optional, defaults to 1 respecting aspect ratio (width takes precedence).
preserveAspectRatio?: number;
// Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
// (download not supported).
inline?: number;
}
export const enum HeaderState {
START = 0,
ABORT = 1,
KEY = 2,
VALUE = 3,
END = 4
}
// field value decoders
// ASCII bytes to string
function toStr(data: Uint32Array): string {
let s = '';
for (let i = 0; i < data.length; ++i) {
s += String.fromCharCode(data[i]);
}
return s;
}
// digits to integer
function toInt(data: Uint32Array): number {
let v = 0;
for (let i = 0; i < data.length; ++i) {
if (data[i] < 48 || data[i] > 57) {
throw new Error('illegal char');
}
v = v * 10 + data[i] - 48;
}
return v;
}
// check for correct size entry
function toSize(data: Uint32Array): string {
const v = toStr(data);
if (!v.match(/^((auto)|(\d+?((px)|(%)){0,1}))$/)) {
throw new Error('illegal size');
}
return v;
}
// name is base64 encoded utf-8
function toName(data: Uint32Array): string {
if (typeof Buffer !== 'undefined') {
return Buffer.from(toStr(data), 'base64').toString();
}
const bs = atob(toStr(data));
const b = new Uint8Array(bs.length);
for (let i = 0; i < b.length; ++i) {
b[i] = bs.charCodeAt(i);
}
return new TextDecoder().decode(b);
}
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
inline: toInt,
size: toInt,
name: toName,
width: toSize,
height: toSize,
preserveAspectRatio: toInt
};
const FILE_MARKER = [70, 105, 108, 101];
const MAX_FIELDCHARS = 1024;
export class HeaderParser {
public state: HeaderState = HeaderState.START;
private _buffer = new Uint32Array(MAX_FIELDCHARS);
private _position = 0;
private _key = '';
public fields: {[key: string]: any} = {};
public reset(): void {
this._buffer.fill(0);
this.state = HeaderState.START;
this._position = 0;
this.fields = {};
this._key = '';
}
public parse(data: Uint32Array, start: number, end: number): number {
let state = this.state;
let pos = this._position;
const buffer = this._buffer;
if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
if (state === HeaderState.START && pos > 6) return -1;
for (let i = start; i < end; ++i) {
const c = data[i];
switch (c) {
case 59: // ;
if (!this._storeValue(pos)) return this._a();
state = HeaderState.KEY;
pos = 0;
break;
case 61: // =
if (state === HeaderState.START) {
for (let k = 0; k < FILE_MARKER.length; ++k) {
if (buffer[k] !== FILE_MARKER[k]) return this._a();
}
state = HeaderState.KEY;
pos = 0;
} else if (state === HeaderState.KEY) {
if (!this._storeKey(pos)) return this._a();
state = HeaderState.VALUE;
pos = 0;
} else if (state === HeaderState.VALUE) {
if (pos >= MAX_FIELDCHARS) return this._a();
buffer[pos++] = c;
}
break;
case 58: // :
if (state === HeaderState.VALUE) {
if (!this._storeValue(pos)) return this._a();
}
this.state = HeaderState.END;
return i + 1;
default:
if (pos >= MAX_FIELDCHARS) return this._a();
buffer[pos++] = c;
}
}
this.state = state;
this._position = pos;
return -2;
}
private _a(): number {
this.state = HeaderState.ABORT;
return -1;
}
private _storeKey(pos: number): boolean {
const k = toStr(this._buffer.subarray(0, pos));
if (k) {
this._key = k;
this.fields[k] = null;
return true;
}
return false;
}
private _storeValue(pos: number): boolean {
if (this._key) {
try {
const v = this._buffer.slice(0, pos);
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
} catch (e) {
return false;
}
return true;
}
return false;
}
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { imageType, IMetrics } from './IIPMetrics';
// fix missing nodejs decl
declare const require: (s: string) => any;
const fs = require('fs');
const TEST_IMAGES: [string, IMetrics][] = [
['w3c_home_256.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_256.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home_256.png', { mime: 'image/png', width: 72, height: 48 }],
['w3c_home_2.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_2.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home_2.png', { mime: 'image/png', width: 72, height: 48 }],
['w3c_home_animation.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_gray.gif', { mime: 'image/gif', width: 72, height: 48 }],
['w3c_home_gray.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home_gray.png', { mime: 'image/png', width: 72, height: 48 }],
['w3c_home.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
['w3c_home.png', { mime: 'image/png', width: 72, height: 48 }],
['spinfox.png', { mime: 'image/png', width: 148, height: 148 }],
['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448 }],
['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600 }],
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }]
];
describe('IIPMetrics', () => {
it('bunch of testimages', () => {
for (let i = 0; i < TEST_IMAGES.length; ++i) {
const imageData = fs.readFileSync('./addons/xterm-addon-image/fixture/testimages/' + TEST_IMAGES[i][0]);
assert.deepStrictEqual(imageType(imageData), TEST_IMAGES[i][1]);
}
});
});
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
export interface IMetrics {
mime: ImageType;
width: number;
height: number;
}
export const UNSUPPORTED_TYPE: IMetrics = {
mime: 'unsupported',
width: 0,
height: 0
};
export function imageType(d: Uint8Array): IMetrics {
if (d.length < 24) {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
};
}
// JPEG: FF D8 FF E0 xx xx JFIF or FF D8 FF E1 xx xx Exif 00 00
if ((d32[0] === 0xE0FFD8FF || d32[0] === 0xE1FFD8FF)
&& (
(d[6] === 0x4a && d[7] === 0x46 && d[8] === 0x49 && d[9] === 0x46)
|| (d[6] === 0x45 && d[7] === 0x78 && d[8] === 0x69 && d[9] === 0x66)
)
) {
const [width, height] = jpgSize(d);
return { mime: 'image/jpeg', width, height };
}
// GIF: GIF87a or GIF89a
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
return {
mime: 'image/gif',
width: d[7] << 8 | d[6],
height: d[9] << 8 | d[8]
};
}
return UNSUPPORTED_TYPE;
}
function jpgSize(d: Uint8Array): [number, number] {
const len = d.length;
let i = 4;
let blockLength = d[i] << 8 | d[i + 1];
while (true) {
i += blockLength;
if (i >= len) {
// exhausted without size info
return [0, 0];
}
if (d[i] !== 0xFF) {
return [0, 0];
}
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
if (i + 8 < len) {
return [
d[i + 7] << 8 | d[i + 8],
d[i + 5] << 8 | d[i + 6]
];
}
return [0, 0];
}
i += 2;
blockLength = d[i] << 8 | d[i + 1];
}
}
+317
View File
@@ -0,0 +1,317 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IIPHandler } from './IIPHandler';
import { ITerminalAddon, IDisposable } from 'xterm';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { SixelHandler } from './SixelHandler';
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
// default values of addon ctor options
const DEFAULT_OPTIONS: IImageAddonOptions = {
enableSizeReports: true,
pixelLimit: 16777216, // limit to 4096 * 4096 pixels
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 256,
sixelSizeLimit: 25000000,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
};
// max palette size supported by the sixel lib (compile time setting)
const MAX_SIXEL_PALETTE_SIZE = 4096;
// definitions for _xtermGraphicsAttributes sequence
const enum GaItem {
COLORS = 1,
SIXEL_GEO = 2,
REGIS_GEO = 3
}
const enum GaAction {
READ = 1,
SET_DEFAULT = 2,
SET = 3,
READ_MAX = 4
}
const enum GaStatus {
SUCCESS = 0,
ITEM_ERROR = 1,
ACTION_ERROR = 2,
FAILURE = 3
}
export class ImageAddon implements ITerminalAddon {
private _opts: IImageAddonOptions;
private _defaultOpts: IImageAddonOptions;
private _storage: ImageStorage | undefined;
private _renderer: ImageRenderer | undefined;
private _disposables: IDisposable[] = [];
private _terminal: ITerminalExt | undefined;
private _handlers: Map<String, IResetHandler> = new Map();
constructor(opts?: Partial<IImageAddonOptions>) {
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
this._defaultOpts = Object.assign({}, DEFAULT_OPTIONS, opts);
}
public dispose(): void {
for (const obj of this._disposables) {
obj.dispose();
}
this._disposables.length = 0;
this._handlers.clear();
}
private _disposeLater(...args: IDisposable[]): void {
for (const obj of args) {
this._disposables.push(obj);
}
}
public activate(terminal: ITerminalExt): void {
this._terminal = terminal;
// internal data structures
this._renderer = new ImageRenderer(terminal);
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
// enable size reports
if (this._opts.enableSizeReports) {
// const windowOptions = terminal.getOption('windowOptions');
// windowOptions.getWinSizePixels = true;
// windowOptions.getCellSizePixels = true;
// windowOptions.getWinSizeChars = true;
// terminal.setOption('windowOptions', windowOptions);
const windowOps = terminal.options.windowOptions || {};
windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true;
windowOps.getWinSizeChars = true;
terminal.options.windowOptions = windowOps;
}
this._disposeLater(
this._renderer,
this._storage,
// DECSET/DECRST/DA1/XTSMGRAPHICS handlers
terminal.parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this._decset(params)),
terminal.parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this._decrst(params)),
terminal.parser.registerCsiHandler({ final: 'c' }, params => this._da1(params)),
terminal.parser.registerCsiHandler({ prefix: '?', final: 'S' }, params => this._xtermGraphicsAttributes(params)),
// render hook
terminal.onRender(range => this._storage?.render(range)),
/**
* reset handlers covered:
* - DECSTR
* - RIS
* - Terminal.reset()
*/
terminal.parser.registerCsiHandler({ intermediates: '!', final: 'p' }, () => this.reset()),
terminal.parser.registerEscHandler({ final: 'c' }, () => this.reset()),
terminal._core._inputHandler.onRequestReset(() => this.reset()),
// wipe canvas and delete alternate images on buffer switch
terminal.buffer.onBufferChange(() => this._storage?.wipeAlternate()),
// extend images to the right on resize
terminal.onResize(metrics => this._storage?.viewportResize(metrics))
);
// SIXEL handler
if (this._opts.sixelSupport) {
const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
this._handlers.set('sixel', sixelHandler);
this._disposeLater(
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
);
}
// iTerm IIP handler
if (this._opts.iipSupport) {
const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
this._handlers.set('iip', iipHandler);
this._disposeLater(
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
);
}
}
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
public reset(): boolean {
// reset options customizable by sequences to defaults
this._opts.sixelScrolling = this._defaultOpts.sixelScrolling;
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
// also clear image storage
this._storage?.reset();
// reset protocol handlers
for (const handler of this._handlers.values()) {
handler.reset();
}
return false;
}
public get storageLimit(): number {
return this._storage?.getLimit() || -1;
}
public set storageLimit(limit: number) {
this._storage?.setLimit(limit);
this._opts.storageLimit = limit;
}
public get storageUsage(): number {
if (this._storage) {
return this._storage.getUsage();
}
return -1;
}
public get showPlaceholder(): boolean {
return this._opts.showPlaceholder;
}
public set showPlaceholder(value: boolean) {
this._opts.showPlaceholder = value;
this._renderer?.showPlaceholder(value);
}
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
return this._storage?.getImageAtBufferCell(x, y);
}
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
return this._storage?.extractTileAtBufferCell(x, y);
}
private _report(s: string): void {
this._terminal?._core.coreService.triggerDataEvent(s);
}
private _decset(params: (number | number[])[]): boolean {
for (let i = 0; i < params.length; ++i) {
switch (params[i]) {
case 80:
this._opts.sixelScrolling = false;
break;
}
}
return false;
}
private _decrst(params: (number | number[])[]): boolean {
for (let i = 0; i < params.length; ++i) {
switch (params[i]) {
case 80:
this._opts.sixelScrolling = true;
break;
}
}
return false;
}
// overload DA to return something more appropriate
private _da1(params: (number | number[])[]): boolean {
if (params[0]) {
return true;
}
// reported features:
// 62 - VT220
// 4 - SIXEL support
// 9 - charsets
// 22 - ANSI colors
if (this._opts.sixelSupport) {
this._report(`\x1b[?62;4;9;22c`);
return true;
}
return false;
}
/**
* Implementation of xterm's graphics attribute sequence.
*
* Supported features:
* - read/change palette limits (max 4096 by sixel lib)
* - read SIXEL canvas geometry (reports current window canvas or
* squared pixelLimit if canvas > pixel limit)
*
* Everything else is deactivated.
*/
private _xtermGraphicsAttributes(params: (number | number[])[]): boolean {
if (params.length < 2) {
return true;
}
if (params[0] === GaItem.COLORS) {
switch (params[1]) {
case GaAction.READ:
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
return true;
case GaAction.SET_DEFAULT:
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
// also reset protocol handlers for now
for (const handler of this._handlers.values()) {
handler.reset();
}
return true;
case GaAction.SET:
if (params.length > 2 && !(params[2] instanceof Array) && params[2] <= MAX_SIXEL_PALETTE_SIZE) {
this._opts.sixelPaletteLimit = params[2];
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
} else {
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
}
return true;
case GaAction.READ_MAX:
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${MAX_SIXEL_PALETTE_SIZE}S`);
return true;
default:
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
return true;
}
}
if (params[0] === GaItem.SIXEL_GEO) {
switch (params[1]) {
// we only implement read and read_max here
case GaAction.READ:
let width = this._renderer?.dimensions?.css.canvas.width;
let height = this._renderer?.dimensions?.css.canvas.height;
if (!width || !height) {
// for some reason we have no working image renderer
// --> fallback to default cell size
const cellSize = CELL_SIZE_DEFAULT;
width = (this._terminal?.cols || 80) * cellSize.width;
height = (this._terminal?.rows || 24) * cellSize.height;
}
if (width * height < this._opts.pixelLimit) {
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${width.toFixed(0)};${height.toFixed(0)}S`);
} else {
// if we overflow pixelLimit report that squared instead
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${x};${x}S`);
}
return true;
case GaAction.READ_MAX:
// read_max returns pixelLimit as square area
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${x};${x}S`);
return true;
default:
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
return true;
}
}
// exit with error on ReGIS or any other requests
this._report(`\x1b[?${params[0]};${GaStatus.ITEM_ERROR}S`);
return true;
}
}
@@ -0,0 +1,379 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { toRGBA8888 } from 'sixel/lib/Colors';
import { IDisposable } from 'xterm';
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
const PLACEHOLDER_LENGTH = 4096;
const PLACEHOLDER_HEIGHT = 24;
/**
* ImageRenderer - terminal frontend extension:
* - provide primitives for canvas, ImageData, Bitmap (static)
* - add canvas layer to DOM (browser only for now)
* - draw image tiles onRender
*/
export class ImageRenderer extends Disposable implements IDisposable {
public canvas: HTMLCanvasElement | undefined;
private _ctx: CanvasRenderingContext2D | null | undefined;
private _placeholder: HTMLCanvasElement | undefined;
private _placeholderBitmap: ImageBitmap | undefined;
private _optionsRefresh = this.register(new MutableDisposable());
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
private _renderService: IRenderService | undefined;
private _oldSetRenderer: ((renderer: any) => void) | undefined;
// drawing primitive - canvas
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
/**
* NOTE: We normally dont care, from which document the canvas
* gets created, so we can fall back to global document,
* if the terminal has no document associated yet.
* This way early image loads before calling .open keep working
* (still discouraged though, as the metrics will be screwed up).
* Only the DOM output canvas should be on the terminal's document,
* which gets explicitly checked in `insertLayerToDom`.
*/
const canvas = (localDocument || document).createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
return canvas;
}
// drawing primitive - ImageData with optional buffer
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
if (typeof ImageData !== 'function') {
const imgData = ctx.createImageData(width, height);
if (buffer) {
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
}
return imgData;
}
return buffer
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
: new ImageData(width, height);
}
// drawing primitive - ImageBitmap
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
if (typeof createImageBitmap !== 'function') {
return Promise.resolve(undefined);
}
return createImageBitmap(img);
}
constructor(private _terminal: ITerminalExt) {
super();
this._oldOpen = this._terminal._core.open;
this._terminal._core.open = (parent: HTMLElement): void => {
this._oldOpen?.call(this._terminal._core, parent);
this._open();
};
if (this._terminal._core.screenElement) {
this._open();
}
// hack to spot fontSize changes
this._optionsRefresh.value = this._terminal._core.optionsService.onOptionChange(option => {
if (option === 'fontSize') {
this.rescaleCanvas();
this._renderService?.refreshRows(0, this._terminal.rows);
}
});
this.register(toDisposable(() => {
this.removeLayerFromDom();
if (this._terminal._core && this._oldOpen) {
this._terminal._core.open = this._oldOpen;
this._oldOpen = undefined;
}
if (this._renderService && this._oldSetRenderer) {
this._renderService.setRenderer = this._oldSetRenderer;
this._oldSetRenderer = undefined;
}
this._renderService = undefined;
this.canvas = undefined;
this._ctx = undefined;
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined;
}));
}
/**
* Enable the placeholder.
*/
public showPlaceholder(value: boolean): void {
if (value) {
if (!this._placeholder && this.cellSize.height !== -1) {
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
}
} else {
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined;
}
this._renderService?.refreshRows(0, this._terminal.rows);
}
/**
* Dimensions of the terminal.
* Forwarded from internal render service.
*/
public get dimensions(): IRenderDimensions | undefined {
return this._renderService?.dimensions;
}
/**
* Current cell size (float).
*/
public get cellSize(): ICellSize {
return {
width: this.dimensions?.css.cell.width || -1,
height: this.dimensions?.css.cell.height || -1
};
}
/**
* Clear a region of the image layer canvas.
*/
public clearLines(start: number, end: number): void {
this._ctx?.clearRect(
0,
start * (this.dimensions?.css.cell.height || 0),
this.dimensions?.css.canvas.width || 0,
(++end - start) * (this.dimensions?.css.cell.height || 0)
);
}
/**
* Clear whole image canvas.
*/
public clearAll(): void {
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
}
/**
* Draw neighboring tiles on the image layer canvas.
*/
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
if (!this._ctx) {
return;
}
const { width, height } = this.cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
this._rescaleImage(imgSpec, width, height);
const img = imgSpec.actual!;
const cols = Math.ceil(img.width / width);
const sx = (tileId % cols) * width;
const sy = Math.floor(tileId / cols) * height;
const dx = col * width;
const dy = row * height;
// safari bug: never access image source out of bounds
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
// Floor all pixel offsets to get stable tile mapping without any overflows.
// Note: For not pixel perfect aligned cells like in the DOM renderer
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
this._ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
);
}
/**
* Extract a single tile from an image.
*/
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
const { width, height } = this.cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
this._rescaleImage(imgSpec, width, height);
const img = imgSpec.actual!;
const cols = Math.ceil(img.width / width);
const sx = (tileId % cols) * width;
const sy = Math.floor(tileId / cols) * height;
const finalWidth = width + sx > img.width ? img.width - sx : width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
);
return canvas;
}
}
/**
* Draw a line with placeholder on the image layer canvas.
*/
public drawPlaceholder(col: number, row: number, count: number = 1): void {
if (this._ctx) {
const { width, height } = this.cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
if (!this._placeholder) {
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
} else if (height >= this._placeholder!.height) {
this._createPlaceHolder(height + 1);
}
if (!this._placeholder) return;
this._ctx.drawImage(
this._placeholderBitmap || this._placeholder!,
col * width,
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
width * count,
height,
col * width,
row * height,
width * count,
height
);
}
}
/**
* Rescale image layer canvas if needed.
* Checked once from `ImageStorage.render`.
*/
public rescaleCanvas(): void {
if (!this.canvas) {
return;
}
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
this.canvas.width = this.dimensions!.css.canvas.width || 0;
this.canvas.height = this.dimensions!.css.canvas.height || 0;
}
}
/**
* Rescale image in storage if needed.
*/
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
return;
}
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
if (currentWidth === originalWidth && currentHeight === originalHeight) {
spec.actual = spec.orig;
spec.actualCellSize.width = originalWidth;
spec.actualCellSize.height = originalHeight;
return;
}
const canvas = ImageRenderer.createCanvas(
this.document,
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
spec.actual = canvas;
spec.actualCellSize.width = currentWidth;
spec.actualCellSize.height = currentHeight;
}
}
/**
* Lazy init for the renderer.
*/
private _open(): void {
this._renderService = this._terminal._core._renderService;
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
this._renderService.setRenderer = (renderer: any) => {
this.removeLayerFromDom();
this._oldSetRenderer?.call(this._renderService, renderer);
};
}
public insertLayerToDom(): void {
// make sure that the terminal is attached to a document and to DOM
if (this.document && this._terminal._core.screenElement) {
if (!this.canvas) {
this.canvas = ImageRenderer.createCanvas(
this.document, this.dimensions?.css.canvas.width || 0,
this.dimensions?.css.canvas.height || 0
);
this.canvas.classList.add('xterm-image-layer');
this._terminal._core.screenElement.appendChild(this.canvas);
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
this.clearAll();
}
} else {
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
}
}
public removeLayerFromDom(): void {
if (this.canvas) {
this._ctx = undefined;
this.canvas.remove();
this.canvas = undefined;
}
}
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
// create blueprint to fill placeholder with
const bWidth = 32; // must be 2^n
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
const ctx = blueprint.getContext('2d', { alpha: false });
if (!ctx) return;
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
const d32 = new Uint32Array(imgData.data.buffer);
const black = toRGBA8888(0, 0, 0);
const white = toRGBA8888(255, 255, 255);
d32.fill(black);
for (let y = 0; y < height; ++y) {
const shift = y % 2;
const offset = y * bWidth;
for (let x = 0; x < bWidth; x += 2) {
d32[offset + x + shift] = white;
}
}
ctx.putImageData(imgData, 0, 0);
// create placeholder line, width aligned to blueprint width
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
if (!ctx2) {
this._placeholder = undefined;
return;
}
for (let i = 0; i < width; i += bWidth) {
ctx2.drawImage(blueprint, i, 0);
}
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
}
public get document(): Document | undefined {
return this._terminal._core._coreBrowserService?.window.document;
}
}
@@ -0,0 +1,592 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
import { ImageRenderer } from './ImageRenderer';
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
// fallback default cell size
export const CELL_SIZE_DEFAULT: ICellSize = {
width: 7,
height: 14
};
/**
* Extend extended attribute to also hold image tile information.
*
* Object definition is copied from base repo to fully mimick its behavior.
* Image data is added as additional public properties `imageId` and `tileId`.
*/
class ExtendedAttrsImage implements IExtendedAttrsImage {
private _ext: number = 0;
public get ext(): number {
if (this._urlId) {
return (
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
(this.underlineStyle << 26)
);
}
return this._ext;
}
public set ext(value: number) { this._ext = value; }
public get underlineStyle(): UnderlineStyle {
// Always return the URL style if it has one
if (this._urlId) {
return UnderlineStyle.DASHED;
}
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
}
public set underlineStyle(value: UnderlineStyle) {
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
}
public get underlineColor(): number {
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public set underlineColor(value: number) {
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
private _urlId: number = 0;
public get urlId(): number {
return this._urlId;
}
public set urlId(value: number) {
this._urlId = value;
}
constructor(
ext: number = 0,
urlId: number = 0,
public imageId = -1,
public tileId = -1
) {
this._ext = ext;
this._urlId = urlId;
}
public clone(): IExtendedAttrsImage {
/**
* Technically we dont need a clone variant of ExtendedAttrsImage,
* as we never clone a cell holding image data.
* Note: Clone is only meant to be used by the InputHandler for
* sticky attributes, which is never the case for image data.
* We still provide a proper clone method to reflect the full ext attr
* state in case there are future use cases for clone.
*/
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
}
public isEmpty(): boolean {
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
}
}
const EMPTY_ATTRS = new ExtendedAttrsImage();
/**
* ImageStorage - extension of CoreTerminal:
* - hold image data
* - write/read image data to/from buffer
*
* TODO: image composition for overwrites
*/
export class ImageStorage implements IDisposable {
// storage
private _images: Map<number, IImageSpec> = new Map();
// last used id
private _lastId = 0;
// last evicted id
private _lowestId = 0;
// whether a full clear happened before
private _fullyCleared = false;
// whether render should do a full clear
private _needsFullClear = false;
// hard limit of stored pixels (fallback limit of 10 MB)
private _pixelLimit: number = 2500000;
private _viewportMetrics: { cols: number, rows: number };
constructor(
private _terminal: ITerminalExt,
private _renderer: ImageRenderer,
private _opts: IImageAddonOptions
) {
try {
this.setLimit(this._opts.storageLimit);
} catch (e: any) {
console.error(e.message);
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
}
this._viewportMetrics = {
cols: this._terminal.cols,
rows: this._terminal.rows
};
}
public dispose(): void {
this.reset();
}
public reset(): void {
for (const spec of this._images.values()) {
spec.marker?.dispose();
}
// NOTE: marker.dispose above already calls ImageBitmap.close
// therefore we can just wipe the map here
this._images.clear();
this._renderer.clearAll();
}
public getLimit(): number {
return this._pixelLimit * 4 / 1000000;
}
public setLimit(value: number): void {
if (value < 0.5 || value > 1000) {
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
}
this._pixelLimit = (value / 4 * 1000000) >>> 0;
this._evictOldest(0);
}
public getUsage(): number {
return this._getStoredPixels() * 4 / 1000000;
}
private _getStoredPixels(): number {
let storedPixels = 0;
for (const spec of this._images.values()) {
if (spec.orig) {
storedPixels += spec.orig.width * spec.orig.height;
if (spec.actual && spec.actual !== spec.orig) {
storedPixels += spec.actual.width * spec.actual.height;
}
}
}
return storedPixels;
}
private _delImg(id: number): void {
const spec = this._images.get(id);
this._images.delete(id);
// FIXME: really ugly workaround to get bitmaps deallocated :(
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
spec.orig.close();
}
}
/**
* Wipe canvas and images on alternate buffer.
*/
public wipeAlternate(): void {
// remove all alternate tagged images
const zero = [];
for (const [id, spec] of this._images.entries()) {
if (spec.bufferType === 'alternate') {
spec.marker?.dispose();
zero.push(id);
}
}
for (const id of zero) {
this._delImg(id);
}
// mark canvas to be wiped on next render
this._needsFullClear = true;
this._fullyCleared = false;
}
/**
* Only advance text cursor.
* This is an edge case from empty sixels carrying only a height but no pixels.
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
*/
public advanceCursor(height: number): void {
if (this._opts.sixelScrolling) {
let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = CELL_SIZE_DEFAULT;
}
const rows = Math.ceil(height / cellSize.height);
for (let i = 1; i < rows; ++i) {
this._terminal._core._inputHandler.lineFeed();
}
}
}
/**
* Method to add an image to the storage.
*/
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);
// calc rows x cols needed to display the image
let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = CELL_SIZE_DEFAULT;
}
const cols = Math.ceil(img.width / cellSize.width);
const rows = Math.ceil(img.height / cellSize.height);
const imageId = ++this._lastId;
const buffer = this._terminal._core.buffer;
const termCols = this._terminal.cols;
const termRows = this._terminal.rows;
const originX = buffer.x;
const originY = buffer.y;
let offset = originX;
let tileCount = 0;
if (!this._opts.sixelScrolling) {
buffer.x = 0;
buffer.y = 0;
offset = 0;
}
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(buffer.y + buffer.ybase);
for (let col = 0; col < cols; ++col) {
if (offset + col >= termCols) break;
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
tileCount++;
}
if (this._opts.sixelScrolling) {
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
} else {
if (++buffer.y >= termRows) break;
}
buffer.x = offset;
}
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
// cursor positioning modes
if (this._opts.sixelScrolling) {
buffer.x = offset;
} else {
buffer.x = originX;
buffer.y = originY;
}
// deleted images with zero tile count
const zero = [];
for (const [id, spec] of this._images.entries()) {
if (spec.tileCount < 1) {
spec.marker?.dispose();
zero.push(id);
}
}
for (const id of zero) {
this._delImg(id);
}
// eviction marker:
// delete the image when the marker gets disposed
const endMarker = this._terminal.registerMarker(0);
endMarker?.onDispose(() => {
const spec = this._images.get(imageId);
if (spec) {
this._delImg(imageId);
}
});
// since markers do not work on alternate for some reason,
// we evict images here manually
if (this._terminal.buffer.active.type === 'alternate') {
this._evictOnAlternate();
}
// create storage entry
const imgSpec: IImageSpec = {
orig: img,
origCellSize: cellSize,
actual: img,
actualCellSize: { ...cellSize }, // clone needed, since later modified
marker: endMarker || undefined,
tileCount,
bufferType: this._terminal.buffer.active.type
};
// finally add the image
this._images.set(imageId, imgSpec);
}
/**
* Render method. Collects buffer information and triggers
* canvas updates.
*/
// TODO: Should we move this to the ImageRenderer?
public render(range: { start: number, end: number }): void {
// setup image canvas in case we have none yet, but have images in store
if (!this._renderer.canvas && this._images.size) {
this._renderer.insertLayerToDom();
// safety measure - in case we cannot spawn a canvas at all, just exit
if (!this._renderer.canvas) {
return;
}
}
// rescale if needed
this._renderer.rescaleCanvas();
// exit early if we dont have any images to test for
if (!this._images.size) {
if (!this._fullyCleared) {
this._renderer.clearAll();
this._fullyCleared = true;
this._needsFullClear = false;
}
if (this._renderer.canvas) {
this._renderer.removeLayerFromDom();
}
return;
}
// buffer switches force a full clear
if (this._needsFullClear) {
this._renderer.clearAll();
this._fullyCleared = true;
this._needsFullClear = false;
}
const { start, end } = range;
const buffer = this._terminal._core.buffer;
const cols = this._terminal._core.cols;
// clear drawing area
this._renderer.clearLines(start, end);
// walk all cells in viewport and draw tiles found
for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
if (!line) return;
for (let col = 0; col < cols; ++col) {
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
}
const imgSpec = this._images.get(imageId);
if (e.tileId !== -1) {
const startTile = e.tileId;
const startCol = col;
let count = 1;
/**
* merge tiles to the right into a single draw call, if:
* - not at end of line
* - cell has same image id
* - cell has consecutive tile id
*/
while (
++col < cols
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
&& (e.imageId === imageId)
&& (e.tileId === startTile + count)
) {
count++;
}
col--;
if (imgSpec) {
if (imgSpec.actual) {
this._renderer.draw(imgSpec, startTile, startCol, row, count);
}
} else if (this._opts.showPlaceholder) {
this._renderer.drawPlaceholder(startCol, row, count);
}
this._fullyCleared = false;
}
}
}
}
}
public viewportResize(metrics: { cols: number, rows: number }): void {
// exit early if we have nothing in storage
if (!this._images.size) {
this._viewportMetrics = metrics;
return;
}
// handle only viewport width enlargements, exit all other cases
// TODO: needs patch for tile counter
if (this._viewportMetrics.cols >= metrics.cols) {
this._viewportMetrics = metrics;
return;
}
// walk scrollbuffer at old col width to find all possible expansion matches
const buffer = this._terminal._core.buffer;
const rows = buffer.lines.length;
const oldCol = this._viewportMetrics.cols - 1;
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row) as IBufferLineExt;
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
}
const imgSpec = this._images.get(imageId);
if (!imgSpec) {
continue;
}
// found an image tile at oldCol, check if it qualifies for right exapansion
const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
continue;
}
// expand only if right side is empty (nothing got wrapped from below)
let hasData = false;
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
hasData = true;
break;
}
}
if (hasData) {
continue;
}
// do right expansion on terminal buffer
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
let lastTile = e.tileId;
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
imgSpec.tileCount++;
}
}
}
// store new viewport metrics
this._viewportMetrics = metrics;
}
/**
* Retrieve original canvas at buffer position.
*/
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
return canvas;
}
return orig as HTMLCanvasElement;
}
}
}
/**
* Extract active single tile at buffer position.
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId);
if (spec) {
return this._renderer.extractTile(spec, e.tileId);
}
}
}
}
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
private _evictOldest(room: number): number {
const used = this._getStoredPixels();
let current = used;
while (this._pixelLimit < current + room && this._images.size) {
const spec = this._images.get(++this._lowestId);
if (spec && spec.orig) {
current -= spec.orig.width * spec.orig.height;
if (spec.actual && spec.orig !== spec.actual) {
current -= spec.actual.width * spec.actual.height;
}
spec.marker?.dispose();
this._delImg(this._lowestId);
}
}
return used - current;
}
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
oldSpec.tileCount--;
}
old.imageId = imageId;
old.tileId = tileId;
return;
}
// found a plain ExtendedAttrs instance, clone it to new entry
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
return;
}
}
// fall-through: always create new ExtendedAttrsImage entry
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
}
private _evictOnAlternate(): void {
// nullify tile count of all images on alternate buffer
for (const spec of this._images.values()) {
if (spec.bufferType === 'alternate') {
spec.tileCount = 0;
}
}
// re-count tiles on whole buffer
const buffer = this._terminal._core.buffer;
for (let y = 0; y < this._terminal.rows; ++y) {
const line = buffer.lines.get(y) as IBufferLineExt;
if (!line) {
continue;
}
for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const imgId = line._extendedAttrs[x]?.imageId;
if (imgId) {
const spec = this._images.get(imgId);
if (spec) {
spec.tileCount++;
}
}
}
}
}
// deleted images with zero tile count
const zero = [];
for (const [id, spec] of this._images.entries()) {
if (spec.bufferType === 'alternate' && !spec.tileCount) {
spec.marker?.dispose();
zero.push(id);
}
}
for (const id of zero) {
this._delImg(id);
}
}
}
@@ -0,0 +1,151 @@
/**
* Copyright (c) 2020, 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ImageStorage } from './ImageStorage';
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
import { RGBA8888 } from 'sixel/lib/Types';
import { ImageRenderer } from './ImageRenderer';
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
// always free decoder ressources after decoding if it exceeds this limit
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
// custom default palette: VT340 (lower 16 colors) + ANSI256 (up to 256) + zeroed (up to 4096)
const DEFAULT_PALETTE = PALETTE_ANSI_256;
DEFAULT_PALETTE.set(PALETTE_VT340_COLOR);
export class SixelHandler implements IDcsHandler, IResetHandler {
private _size = 0;
private _aborted = false;
private _dec: Decoder | undefined;
constructor(
private readonly _opts: IImageAddonOptions,
private readonly _storage: ImageStorage,
private readonly _coreTerminal: ITerminalExt
) {
DecoderAsync({
memoryLimit: this._opts.pixelLimit * 4,
palette: DEFAULT_PALETTE,
paletteLimit: this._opts.sixelPaletteLimit
}).then(d => this._dec = d);
}
public reset(): void {
/**
* reset sixel decoder to defaults:
* - release all memory
* - nullify palette (4096)
* - apply default palette (256)
*/
if (this._dec) {
this._dec.release();
// FIXME: missing interface on decoder to nullify full palette
(this._dec as any)._palette.fill(0);
this._dec.init(0, DEFAULT_PALETTE, this._opts.sixelPaletteLimit);
}
}
public hook(params: IParams): void {
this._size = 0;
this._aborted = false;
if (this._dec) {
const fillColor = params.params[1] === 1 ? 0 : extractActiveBg(
this._coreTerminal._core._inputHandler._curAttrData,
this._coreTerminal._core._themeService?.colors);
this._dec.init(fillColor, null, this._opts.sixelPaletteLimit);
}
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._aborted || !this._dec) {
return;
}
this._size += end - start;
if (this._size > this._opts.sixelSizeLimit) {
console.warn(`SIXEL: too much data, aborting`);
this._aborted = true;
this._dec.release();
return;
}
try {
this._dec.decode(data, start, end);
} catch (e) {
console.warn(`SIXEL: error while decoding image - ${e}`);
this._aborted = true;
this._dec.release();
}
}
public unhook(success: boolean): boolean | Promise<boolean> {
if (this._aborted || !success || !this._dec) {
return true;
}
const width = this._dec.width;
const height = this._dec.height;
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
if (!width || ! height) {
if (height) {
this._storage.advanceCursor(height);
}
return true;
}
const canvas = ImageRenderer.createCanvas(undefined, width, height);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release();
}
this._storage.addImage(canvas);
return true;
}
}
/**
* Some helpers to extract current terminal colors.
*/
// get currently active background color from terminal
// also respect INVERSE setting
function extractActiveBg(attr: AttributeData, colors: ReadonlyColorSet | undefined): RGBA8888 {
let bg = 0;
if (!colors) {
// FIXME: theme service is prolly not available yet,
// happens if .open() was not called yet (bug in core?)
return bg;
}
if (attr.isInverse()) {
if (attr.isFgDefault()) {
bg = convertLe(colors.foreground.rgba);
} else if (attr.isFgRGB()) {
const t = (attr.constructor as typeof AttributeData).toColorRGB(attr.getFgColor());
bg = toRGBA8888(...t);
} else {
bg = convertLe(colors.ansi[attr.getFgColor()].rgba);
}
} else {
if (attr.isBgDefault()) {
bg = convertLe(colors.background.rgba);
} else if (attr.isBgRGB()) {
const t = (attr.constructor as typeof AttributeData).toColorRGB(attr.getBgColor());
bg = toRGBA8888(...t);
} else {
bg = convertLe(colors.ansi[attr.getBgColor()].rgba);
}
}
return bg;
}
// rgba values on the color managers are always in BE, thus convert to LE
function convertLe(color: number): RGBA8888 {
if (BIG_ENDIAN) return color;
return (color & 0xFF) << 24 | (color >>> 8 & 0xFF) << 16 | (color >>> 16 & 0xFF) << 8 | color >>> 24 & 0xFF;
}
+108
View File
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable, IMarker, Terminal } from 'xterm';
// private imports from base repo we build against
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
import type { AttributeData } from 'common/buffer/AttributeData';
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
export const enum Cell {
CONTENT = 0, // codepoint and wcwidth information (enum Content)
FG = 1, // foreground color in lower 3 bytes (rgb), attrs in 4th byte (enum FgFlags)
BG = 2, // background color in lower 3 bytes (rgb), attrs in 4th byte (enum BgFlags)
SIZE = 3 // size of single cell on buffer array
}
// export some privates for local usage
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
/**
* Plugin ctor options.
*/
export interface IImageAddonOptions {
enableSizeReports: boolean;
pixelLimit: number;
storageLimit: number;
showPlaceholder: boolean;
sixelSupport: boolean;
sixelScrolling: boolean;
sixelPaletteLimit: number;
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
}
export interface IResetHandler {
// attached to RIS and DECSTR
reset(): void;
}
/**
* Stub into private interfaces.
* This should be kept in line with common libs.
* Any change made here should be replayed in the accessors test case to
* have a somewhat reliable testing against code changes in the core repo.
*/
// overloaded IExtendedAttrs to hold image refs
export interface IExtendedAttrsImage extends IExtendedAttrs {
imageId: number;
tileId: number;
clone(): IExtendedAttrsImage;
}
/* eslint-disable */
export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
_data: Uint32Array;
}
interface IInputHandlerExt extends IInputHandler {
_parser: IEscapeSequenceParser;
_curAttrData: AttributeData;
_dirtyRowTracker: {
markRangeDirty(y1: number, y2: number): void;
markAllDirty(): void;
markDirty(y: number): void;
};
onRequestReset(handler: () => void): IDisposable;
}
export interface ICoreTerminalExt extends ITerminal {
_themeService: IThemeService | undefined;
_inputHandler: IInputHandlerExt;
_renderService: IRenderService;
_coreBrowserService: ICoreBrowserService | undefined;
}
export interface ITerminalExt extends Terminal {
_core: ICoreTerminalExt;
}
/* eslint-enable */
/**
* Some storage definitions.
*/
export interface ICellSize {
width: number;
height: number;
}
export interface IImageSpec {
orig: HTMLCanvasElement | ImageBitmap | undefined;
origCellSize: ICellSize;
actual: HTMLCanvasElement | ImageBitmap | undefined;
actualCellSize: ICellSize;
marker: IMarker | undefined;
tileCount: number;
bufferType: 'alternate' | 'normal';
}
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"sourceMap": true,
"outDir": "../out",
"rootDir": ".",
"strict": true,
"noUnusedLocals": true,
"preserveWatchOutput": true,
"types": [
"../../../node_modules/@types/mocha"
],
"baseUrl": ".",
"paths": {
"browser/*": [ "../../../src/browser/*" ],
"common/*": [ "../../../src/common/*" ]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{ "path": "../../../src/browser" },
{ "path": "../../../src/common" }
]
}
@@ -0,0 +1,321 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser, pollFor } from '../../../out-test/api/TestUtils';
import { Browser, Page } from '@playwright/test';
import { IImageAddonOptions } from '../src/Types';
import { FINALIZER, introducer, sixelEncode } from 'sixel';
import { readFileSync } from 'fs';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 800;
const height = 600;
// eslint-disable-next-line
declare const ImageAddon: {
new(options?: Partial<IImageAddonOptions>): any;
};
interface ITestData {
width: number;
height: number;
bytes: Uint8Array;
palette: number[];
sixel: string;
}
interface IDimensions {
cellWidth: number;
cellHeight: number;
width: number;
height: number;
}
// image: 640 x 80, 512 color
const TESTDATA: ITestData = (() => {
const data8 = readFileSync('./addons/xterm-addon-image/fixture/palette.blob');
const data32 = new Uint32Array(data8.buffer);
const palette = new Set<number>();
for (let i = 0; i < data32.length; ++i) palette.add(data32[i]);
const sixel = sixelEncode(data8, 640, 80, [...palette]);
return {
width: 640,
height: 80,
bytes: data8,
palette: [...palette],
sixel
};
})();
const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
// NOTE: the data is loaded as string for easier transport through playwright
const TESTDATA_IIP: [string, [number, number]][] = [
[readFileSync('./addons/xterm-addon-image/fixture/iip/palette.iip', { encoding: 'utf-8' }), [640, 80]],
[readFileSync('./addons/xterm-addon-image/fixture/iip/spinfox.iip', { encoding: 'utf-8' }), [148, 148]],
[readFileSync('./addons/xterm-addon-image/fixture/iip/w3c_gif.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/xterm-addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/xterm-addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
];
describe('ImageAddon', () => {
before(async () => {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async () => {
await page.goto(APP);
await openTerminal(page);
await page.evaluate(opts => {
(window as any).imageAddon = new ImageAddon(opts.opts);
(window as any).term.loadAddon((window as any).imageAddon);
}, { opts: { sixelPaletteLimit: 512 } });
});
it('test for private accessors', async () => {
// terminal privates
const accessors = [
'_core',
'_core._renderService',
'_core._inputHandler',
'_core._inputHandler._parser',
'_core._inputHandler._curAttrData',
'_core._inputHandler._dirtyRowTracker',
'_core._themeService.colors',
'_core._coreBrowserService'
];
for (const prop of accessors) {
assert.equal(
await page.evaluate('(() => { const v = window.term.' + prop + '; return v !== undefined && v !== null; })()'),
true, `problem at ${prop}`
);
}
// bufferline privates
assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
// inputhandler privates
assert.equal(await page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), 'AttributeData');
assert.equal(await page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
});
describe('ctor options', () => {
it('empty settings should load defaults', async () => {
const DEFAULT_OPTIONS: IImageAddonOptions = {
enableSizeReports: true,
pixelLimit: 16777216,
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 512, // set to 512 to get example image working
sixelSizeLimit: 25000000,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
};
assert.deepEqual(await page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
});
it('custom settings should overload defaults', async () => {
const customSettings: IImageAddonOptions = {
enableSizeReports: false,
pixelLimit: 5,
sixelSupport: false,
sixelScrolling: false,
sixelPaletteLimit: 1024,
sixelSizeLimit: 1000,
storageLimit: 10,
showPlaceholder: false,
iipSupport: false,
iipSizeLimit: 1000
};
await page.evaluate(opts => {
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
(window as any).term.loadAddon((window as any).imageAddonCustom);
}, { opts: customSettings });
assert.deepEqual(await page.evaluate(`window.imageAddonCustom._opts`), customSettings);
});
});
describe('scrolling & cursor modes', () => {
it('testdata default (scrolling with VT240 cursor pos)', async () => {
const dim = await getDimensions();
await writeToTerminal(SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
// moved to right by 10 cells
await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
});
it('write testdata noScrolling', async () => {
await writeToTerminal('\x1b[?80h' + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, 0]);
// second draw does not change anything
await writeToTerminal(SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, 0]);
});
it('testdata cursor always at VT240 pos', async () => {
const dim = await getDimensions();
// offset 0
await writeToTerminal(SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
// moved to right by 10 cells
await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
// moved by 30 cells (+10 prev)
await writeToTerminal('#'.repeat(30) + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]);
});
});
describe('image lifecycle & eviction', () => {
it('delete image once scrolled off', async () => {
await writeToTerminal(SIXEL_SEQ_0);
pollFor(page, 'window.imageAddon._storage._images.size', 1);
// scroll to scrollback + rows - 1
await page.evaluate(
scrollback => new Promise(res => (window as any).term.write('\n'.repeat(scrollback), res)),
(await getScrollbackPlusRows() - 1)
);
// wait here, as we have to make sure, that eviction did not yet occur
await new Promise(r => setTimeout(r, 100));
pollFor(page, 'window.imageAddon._storage._images.size', 1);
// scroll one further should delete the image
await page.evaluate(() => new Promise(res => (window as any).term.write('\n', res)));
pollFor(page, 'window.imageAddon._storage._images.size', 0);
});
it('get storageUsage', async () => {
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
await writeToTerminal(SIXEL_SEQ_0);
assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05);
});
it('get/set storageLimit', async () => {
assert.equal(await page.evaluate('imageAddon.storageLimit'), 128);
assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1);
assert.equal(await page.evaluate('imageAddon.storageLimit'), 1);
});
it('remove images by storage limit pressure', async () => {
assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1);
// never go beyond storage limit
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const usage = await page.evaluate('imageAddon.storageUsage');
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
assert.equal(await page.evaluate('imageAddon.storageUsage'), usage);
assert.equal(usage as number < 1, true);
});
it('set storageLimit removes images synchronously', async () => {
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
const usage: number = await page.evaluate('imageAddon.storageUsage');
const newUsage: number = await page.evaluate('imageAddon.storageLimit = 0.5; imageAddon.storageUsage');
assert.equal(newUsage < usage, true);
assert.equal(newUsage < 0.5, true);
});
it('clear alternate images on buffer change', async () => {
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
await writeToTerminal('\x1b[?1049h' + SIXEL_SEQ_0);
assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05);
await writeToTerminal('\x1b[?1049l');
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
});
it('evict tiles by in-place overwrites (only full overwrite tested)', async () => {
await new Promise(r => setTimeout(r, 50));
await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
let usage = await page.evaluate('imageAddon.storageUsage');
while (usage === 0) {
await new Promise(r => setTimeout(r, 50));
usage = await page.evaluate('imageAddon.storageUsage');
}
await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
await new Promise(r => setTimeout(r, 200)); // wait some time and re-check
assert.equal(await page.evaluate('imageAddon.storageUsage'), usage);
});
it('manual eviction on alternate buffer must not miss images', async () => {
await writeToTerminal('\x1b[?1049h');
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const usage: number = await page.evaluate('imageAddon.storageUsage');
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const newUsage: number = await page.evaluate('imageAddon.storageUsage');
assert.equal(newUsage, usage);
});
});
describe('IIP support - testimages', () => {
it('palette.png', async () => {
await writeToTerminal(TESTDATA_IIP[0][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
});
it('spinfox.png', async () => {
await writeToTerminal(TESTDATA_IIP[1][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
});
it('w3c gif', async () => {
await writeToTerminal(TESTDATA_IIP[2][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
});
it('w3c jpeg', async () => {
await writeToTerminal(TESTDATA_IIP[3][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
});
it('w3c png', async () => {
await writeToTerminal(TESTDATA_IIP[4][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
});
});
});
/**
* terminal access helpers.
*/
async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await page.evaluate(`term._core._renderService.dimensions`);
return {
cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.css.cell.height),
width: Math.round(dimensions.css.canvas.width),
height: Math.round(dimensions.css.canvas.height)
};
}
async function getCursor(): Promise<[number, number]> {
return page.evaluate('[window.term.buffer.active.cursorX, window.term.buffer.active.cursorY]');
}
async function getImageStorageLength(): Promise<number> {
return page.evaluate('window.imageAddon._storage._images.size');
}
async function getScrollbackPlusRows(): Promise<number> {
return page.evaluate('window.term.options.scrollback + window.term.rows');
}
async function writeToTerminal(d: string): Promise<any> {
return page.evaluate(data => new Promise(res => (window as any).term.write(data, res)), d);
}
async function getOrigSize(id: number): Promise<[number, number]> {
return page.evaluate<any>(`[
window.imageAddon._storage._images.get(${id}).orig.width,
window.imageAddon._storage._images.get(${id}).orig.height
]`);
}
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"browser/*": [ "../../../src/browser/*" ],
"common/*": [ "../../../src/common/*" ]
},
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{ "path": "../../../src/browser" },
{ "path": "../../../src/common" }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"files": [],
"include": [],
"references": [
{ "path": "./src" },
{ "path": "./test" }
]
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-image' {
export interface IImageAddonOptions {
/**
* Enable size reports in windowOptions:
* - getWinSizePixels (CSI 14 t)
* - getCellSizePixels (CSI 16 t)
* - getWinSizeChars (CSI 18 t)
*
* If `true` (default), the reports will be activated during addon loading.
* If `false`, no settings will be touched. Use `false`, if you have high
* security constraints and/or deal with windowOptions by other means.
* On addon disposal, the settings will not change.
*/
enableSizeReports?: boolean;
/**
* Maximum pixels a single image may hold. Images exceeding this number will
* be discarded during processing with no changes to the terminal buffer
* (no cursor advance, no placeholder).
* This setting is mainly used to restrict images sizes during initial decoding
* including the final canvas creation.
*
* Note: The image worker decoder may hold additional memory up to
* `pixelLimit` * 4 bytes permanently, plus the same amount on top temporarily
* for pixel transfers, which should be taken into account under memory pressure conditions.
*
* Note: Browsers restrict allowed canvas dimensions further. We dont reflect those
* limits here, thus the construction of an oddly shaped image having most pixels
* in one dimension still can fail.
*
* Note: `storageLimit` bytes are calculated from images by multiplying the pixels with 4
* (4 channels with one byte, images are stored as RGBA8888).
*
* Default is 2^16 (4096 x 4096 pixels).
*/
pixelLimit?: number;
/**
* Storage limit in MB.
* The storage implements a FIFO cache removing old images, when the limit gets hit.
* Also exposed as addon property for runtime adjustments.
* Default is 128 MB.
*/
storageLimit?: number;
/**
* Whether to show a placeholder for images removed from cache, default is true.
*/
showPlaceholder?: boolean;
/**
* SIXEL settings
*/
/** Whether SIXEL is enabled (default is true). */
sixelSupport?: boolean;
/** Whether SIXEL scrolling is enabled (default is true). Same as DECSET 80. */
sixelScrolling?: boolean;
/** Palette color limit (default 256). */
sixelPaletteLimit?: number;
/** SIXEL image size limit in bytes (default 25000000 bytes). */
sixelSizeLimit?: number;
/**
* IIP settings (iTerm image protocol)
*/
/** Whether iTerm image protocol style is enabled (default is true). */
iipSupport?: boolean;
/** IIP sequence size limit (default 20000000 bytes). */
iipSizeLimit?: number;
}
export class ImageAddon implements ITerminalAddon {
constructor(options?: IImageAddonOptions);
public activate(terminal: Terminal): void;
public dispose(): void;
/**
* Reset the image addon.
*
* This resets all runtime options to default values (as given to the ctor)
* and resets the image storage.
*/
public reset(): void;
/**
* Getter/Setter for the storageLimit in MB.
* Synchronously deletes images if the stored data exceeds the new value.
*/
public storageLimit: number;
/**
* Current memory usage of the stored images in MB.
*/
public readonly storageUsage: number;
/**
* Getter/Setter whether the placeholder should be shown.
*/
public showPlaceholder: boolean;
/**
* Get original image canvas at buffer position.
*/
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
/**
* Extract single tile canvas at buffer position.
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
}
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
const path = require('path');
const addonName = 'ImageAddon';
const mainFile = 'xterm-addon-image.js';
const addon = {
entry: `./out/${addonName}.js`,
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
exclude: /node_modules/
}
]
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd'
},
mode: 'production'
};
module.exports = [addon];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-ligatures",
"version": "0.6.0",
"version": "0.7.0",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
+1 -1
View File
@@ -65,7 +65,7 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
}
}
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
else if (typeof process !== 'object' && 'queryLocalFonts' in window) {
else if (typeof window !== 'undefined' && 'queryLocalFonts' in window) {
const fonts: Record<string, IFontMetadata[]> = {};
try {
const fontsIterator = await (window as any).queryLocalFonts();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-search",
"version": "0.12.0",
"version": "0.13.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+20 -28
View File
@@ -5,7 +5,7 @@
import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
import { Disposable, toDisposable, disposeArray, MutableDisposable } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -66,10 +66,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
private _cachedSearchTerm: string | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _selectedDecoration: IHighlight | undefined;
private _selectedDecoration: MutableDisposable<IHighlight> = this.register(new MutableDisposable());
private _highlightLimit: number;
private _onDataDisposable: IDisposable | undefined;
private _onResizeDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
private _highlightTimeout: number | undefined;
/**
@@ -93,13 +91,9 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches()));
this.register(toDisposable(() => {
this.clearDecorations();
this._onDataDisposable?.dispose();
this._onResizeDisposable?.dispose();
}));
this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
this.register(this._terminal.onResize(() => this._updateMatches()));
this.register(toDisposable(() => this.clearDecorations()));
}
private _updateMatches(): void {
@@ -116,7 +110,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this.clearActiveDecoration();
this._selectedDecoration.clear();
disposeArray(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
@@ -125,11 +119,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
}
public clearActiveDecoration(): void {
this._selectedDecoration?.dispose();
this._selectedDecoration = undefined;
}
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
@@ -326,8 +315,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
private _fireResults(searchOptions?: ISearchOptions): void {
if (searchOptions?.decorations) {
let resultIndex = -1;
if (this._selectedDecoration) {
const selectedMatch = this._selectedDecoration.match;
if (this._selectedDecoration.value) {
const selectedMatch = this._selectedDecoration.value.match;
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
@@ -440,7 +429,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
/**
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
* A found substring is a whole word if it doesn't have an alphanumeric character directly
* adjacent to it.
* @param searchIndex starting indext of the potential whole word substring
* @param line entire string in which the potential whole word was found
* @param term the substring that starts at searchIndex
@@ -451,14 +441,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* Searches a line for a search term. Takes the provided terminal line and searches the text line,
* which may contain subsequent terminal lines if the text is wrapped. If the provided line number
* is part of a wrapped text line that started on an earlier line then it is skipped since it will
* be properly searched when the terminal line that the text starts on is searched.
* @param term The search term.
* @param searchPosition The position to start the search.
* @param searchOptions Search options.
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
* @param isReverseSearch Whether the search should start from the right side of the terminal and
* search to the left.
* @returns The search result if it was found.
*/
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
@@ -526,7 +517,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
return;
}
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
// Adjust the row number and search index if needed since a "line" of text can span multiple
// rows
let startRowOffset = 0;
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
startRowOffset++;
@@ -645,7 +637,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
*/
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
const terminal = this._terminal!;
this.clearActiveDecoration();
this._selectedDecoration.clear();
if (!result) {
terminal.clearSelection();
return false;
@@ -669,7 +661,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
this._selectedDecoration = { decoration, match: result, dispose() { decoration.dispose(); } };
this._selectedDecoration.value = { decoration, match: result, dispose() { decoration.dispose(); } };
}
}
}
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { readFile } from 'fs';
import { resolve } from 'path';
import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-serialize",
"version": "0.10.0",
"version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -74,7 +74,7 @@ describe('xterm-addon-serialize', () => {
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
terminal.loadAddon(serializeAddon);
(terminal as any)._core._themeService = new ThemeService(new OptionsService({}));
(terminal as any)._core._themeService = new ThemeService((terminal as any)._core.optionsService);
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
});
@@ -205,5 +205,25 @@ describe('xterm-addon-serialize', () => {
});
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
});
it('cells with custom color styling', async () => {
terminal.options.theme.black = '#ffa500';
terminal.options.theme = { ... terminal.options.theme };
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) || []).length, 1, output);
});
it('cells with color styling - xterm headless', async () => {
// a headless terminal doesn't have a themeservice
(terminal as any)._core._themeService = undefined;
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
});
});
});
@@ -5,9 +5,9 @@
* (EXPERIMENTAL) This Addon is still under development
*/
import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IAttributeData } from 'common/Types';
import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
import { IAttributeData, IColor } from 'common/Types';
import { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from 'xterm';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -130,7 +130,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
protected _rowEnd(row: number, isLastRow: boolean): void {
// if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing
// if there is colorful empty cell at line end, whe must pad it back, or the the color block
// will missing
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
// use clear right to set background.
this._currentRow += `\u001b[${this._nullCellCount}X`;
@@ -291,7 +292,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
const sgrSeq = this._diffStyle(cell, this._cursorStyle);
// the empty cell style is only assumed to be changed when background changed, because foreground is always 0.
// the empty cell style is only assumed to be changed when background changed, because
// foreground is always 0.
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;
/**
@@ -412,8 +414,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
export class SerializeAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;
constructor() { }
public activate(terminal: Terminal): void {
this._terminal = terminal;
}
@@ -536,7 +536,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
private _htmlContent = '';
private _colors: IColorSet;
private _ansiColors: Readonly<IColor[]>;
constructor(
buffer: IBuffer,
@@ -545,8 +545,13 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
) {
super(buffer);
// https://github.com/xtermjs/xterm.js/issues/3601
this._colors = (_terminal as any)._core._themeService.colors;
// For xterm headless: fallback to ansi colors
if ((_terminal as any)._core._themeService) {
this._ansiColors = (_terminal as any)._core._themeService.colors.ansi;
}
else {
this._ansiColors = DEFAULT_ANSI_COLORS;
}
}
private _padStart(target: string, targetLength: number, padString: string): string {
@@ -602,7 +607,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
}
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
return this._colors.ansi[color].css;
return this._ansiColors[color].css;
}
return undefined;
}
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
@@ -204,7 +204,6 @@ describe('SerializeAddon', () => {
sgr(NO_INVISIBLE) + line,
sgr(NO_STRIKETHROUGH) + line
];
const rows = lines.length;
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
@@ -593,7 +592,6 @@ const FG_RGB_GREEN = '38;2;0;255;0';
const FG_RGB_YELLOW = '38;2;255;255;0';
const FG_RESET = '39';
const BG_P16_RED = '41';
const BG_P16_GREEN = '42';
const BG_P16_YELLOW = '43';
@@ -21,6 +21,14 @@ module.exports = {
}
]
},
resolve: {
modules: ['./node_modules'],
extensions: [ '.js' ],
alias: {
common: path.resolve('../../out/common'),
browser: path.resolve('../../out/browser')
}
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-unicode11",
"version": "0.5.0",
"version": "0.6.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-web-links",
"version": "0.8.0",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
@@ -35,64 +35,90 @@ describe('WebLinksAddon', () => {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page, { cols: 40 });
});
after(async () => await browser.close());
beforeEach(async () => await page.goto(APP));
it('.com', async function(): Promise<any> {
await testHostName('foo.com');
beforeEach(async () => {
await page.evaluate(`
window._linkaddon?.dispose();
window.term.reset();
window._linkaddon = new window.WebLinksAddon();
window.term.loadAddon(window._linkaddon);
`);
});
it('.com.au', async function(): Promise<any> {
await testHostName('foo.com.au');
});
it('.io', async function(): Promise<any> {
await testHostName('foo.io');
});
const countryTlds = [
'.ac', '.ad', '.ae', '.af', '.ag', '.ai', '.al', '.am', '.ao', '.aq', '.ar', '.as', '.at',
'.au', '.aw', '.ax', '.az', '.ba', '.bb', '.bd', '.be', '.bf', '.bg', '.bh', '.bi', '.bj',
'.bm', '.bn', '.bo', '.bq', '.br', '.bs', '.bt', '.bw', '.by', '.bz', '.ca', '.cc', '.cd',
'.cf', '.cg', '.ch', '.ci', '.ck', '.cl', '.cm', '.cn', '.co', '.cr', '.cu', '.cv', '.cw',
'.cx', '.cy', '.cz', '.de', '.dj', '.dk', '.dm', '.do', '.dz', '.ec', '.ee', '.eg', '.eh',
'.er', '.es', '.et', '.eu', '.fi', '.fj', '.fk', '.fm', '.fo', '.fr', '.ga', '.gd', '.ge',
'.gf', '.gg', '.gh', '.gi', '.gl', '.gm', '.gn', '.gp', '.gq', '.gr', '.gs', '.gt', '.gu',
'.gw', '.gy', '.hk', '.hm', '.hn', '.hr', '.ht', '.hu', '.id', '.ie', '.il', '.im', '.in',
'.io', '.iq', '.ir', '.is', '.it', '.je', '.jm', '.jo', '.jp', '.ke', '.kg', '.kh', '.ki',
'.km', '.kn', '.kp', '.kr', '.kw', '.ky', '.kz', '.la', '.lb', '.lc', '.li', '.lk', '.lr',
'.ls', '.lt', '.lu', '.lv', '.ly', '.ma', '.mc', '.md', '.me', '.mg', '.mh', '.mk', '.ml',
'.mm', '.mn', '.mo', '.mp', '.mq', '.mr', '.ms', '.mt', '.mu', '.mv', '.mw', '.mx', '.my',
'.mz', '.na', '.nc', '.ne', '.nf', '.ng', '.ni', '.nl', '.no', '.np', '.nr', '.nu', '.nz',
'.om', '.pa', '.pe', '.pf', '.pg', '.ph', '.pk', '.pl', '.pm', '.pn', '.pr', '.ps', '.pt',
'.pw', '.py', '.qa', '.re', '.ro', '.rs', '.ru', '.rw', '.sa', '.sb', '.sc', '.sd', '.se',
'.sg', '.sh', '.si', '.sk', '.sl', '.sm', '.sn', '.so', '.sr', '.ss', '.st', '.su', '.sv',
'.sx', '.sy', '.sz', '.tc', '.td', '.tf', '.tg', '.th', '.tj', '.tk', '.tl', '.tm', '.tn',
'.to', '.tr', '.tt', '.tv', '.tw', '.tz', '.ua', '.ug', '.uk', '.us', '.uy', '.uz', '.va',
'.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw'
];
for (const tld of countryTlds) {
it(tld, async () => await testHostName(`foo${tld}`));
}
it(`.com`, async () => await testHostName(`foo.com`));
for (const tld of countryTlds) {
it(`.com${tld}`, async () => await testHostName(`foo.com${tld}`));
}
describe('correct buffer offsets & uri', () => {
beforeEach(async () => {
await page.evaluate(`
window._linkStateData = {uri:''};
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
`);
});
it('all half width', async () => {
setupCustom();
await writeSync(page, 'aaa http://example.com aaa http://example.com aaa');
await resetAndHover(5, 1);
await resetAndHover(5, 0);
await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } });
await resetAndHover(1, 2);
await resetAndHover(1, 1);
await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } });
});
it('url after full width', async () => {
setupCustom();
await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
await resetAndHover(8, 1);
await resetAndHover(8, 0);
await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } });
await resetAndHover(1, 2);
await resetAndHover(1, 1);
await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } });
});
it('full width within url and before', async () => {
setupCustom();
await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
await resetAndHover(8, 1);
await resetAndHover(8, 0);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
await resetAndHover(1, 2);
await resetAndHover(1, 1);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
await resetAndHover(17, 2);
await resetAndHover(17, 1);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } });
});
it('name + password url after full width and combining', async () => {
setupCustom();
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path');
await resetAndHover(12, 1);
await resetAndHover(12, 0);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
await resetAndHover(13, 2);
await resetAndHover(5, 1);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
});
});
});
async function testHostName(hostname: string): Promise<void> {
await openTerminal(page, { cols: 40 });
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
const data = ` http://${hostname} \\r\\n` +
` http://${hostname}/a~b#c~d?e~f \\r\\n` +
` http://${hostname}/colon:test \\r\\n` +
@@ -101,35 +127,28 @@ async function testHostName(hostname: string): Promise<void> {
`\\'http://${hostname}/\\'\\r\\n` +
`http://${hostname}/subpath/+/id`;
await writeSync(page, data);
await pollForLinkAtCell(3, 1, `http://${hostname}`);
await pollForLinkAtCell(3, 2, `http://${hostname}/a~b#c~d?e~f`);
await pollForLinkAtCell(3, 0, `http://${hostname}`);
await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`);
await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`);
await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`);
await pollForLinkAtCell(3, 4, `http://${hostname}/colon:test`);
await pollForLinkAtCell(2, 4, `http://${hostname}/`);
await pollForLinkAtCell(2, 5, `http://${hostname}/`);
await pollForLinkAtCell(2, 6, `http://${hostname}/`);
await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`);
await pollForLinkAtCell(1, 6, `http://${hostname}/subpath/+/id`);
}
async function pollForLinkAtCell(col: number, row: number, value: string): Promise<void> {
const rowSelector = `.xterm-rows > :nth-child(${row})`;
// Ensure the hover element exists before trying to hover it
await pollFor(page, `!!document.querySelector('${rowSelector} > :nth-child(${col})')`, true);
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
}
async function setupCustom(): Promise<void> {
await openTerminal(page, { cols: 40 });
await page.evaluate(`window._linkStateData = {};
window._linkaddon = new window.WebLinksAddon();
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
window.term.loadAddon(window._linkaddon);`);
await page.mouse.move(...(await cellPos(col, row)));
await pollFor(page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true);
const text = await page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`);
assert.deepEqual(text, value);
}
async function resetAndHover(col: number, row: number): Promise<void> {
await page.evaluate(`window._linkStateData = {};`);
const rowSelector = `.xterm-rows > :nth-child(${row})`;
await page.hover(`${rowSelector} > :nth-child(${col})`);
await page.mouse.move(0, 0);
await page.evaluate(`window._linkStateData = {uri:''};`);
await new Promise(r => setTimeout(r, 200));
await page.mouse.move(...(await cellPos(col, row)));
await pollFor(page, `!!window._linkStateData.uri.length`, true);
}
async function evalLinkStateData(uri: string, range: any): Promise<void> {
@@ -137,3 +156,14 @@ async function evalLinkStateData(uri: string, range: any): Promise<void> {
assert.equal(data.uri, uri);
assert.deepEqual(data.range, range);
}
async function cellPos(col: number, row: number): Promise<[number, number]> {
const coords: any = await page.evaluate(`
(function() {
const rect = window.term.element.getBoundingClientRect();
const dim = term._core._renderService.dimensions;
return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.css.cell.width, height: dim.css.cell.height};
})();
`);
return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2];
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
"version": "0.15.0",
"version": "0.16.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+11 -8
View File
@@ -3,14 +3,15 @@
* @license MIT
*/
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { traceCall } from 'common/services/LogService';
import { Terminal } from 'xterm';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
interface IVertices {
attributes: Float32Array;
@@ -212,6 +213,7 @@ export class GlyphRenderer extends Disposable {
return this._atlas ? this._atlas.beginFrame() : true;
}
@traceCall
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
@@ -236,9 +238,9 @@ export class GlyphRenderer extends Disposable {
// Get the glyph
if (chars && chars.length > 1) {
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
} else {
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
}
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
@@ -308,6 +310,7 @@ export class GlyphRenderer extends Disposable {
public handleResize(): void {
const gl = this._gl;
gl.useProgram(this._program);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
this.clear();
+100 -26
View File
@@ -3,18 +3,17 @@
* @license MIT
*/
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColor } from 'common/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { DIM_OPACITY } from 'browser/renderer/shared/Constants';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types';
import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IColor } from 'common/Types';
import { Terminal } from 'xterm';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
const enum VertexAttribLocations {
POSITION = 0,
@@ -50,19 +49,23 @@ void main() {
outColor = v_color;
}`;
interface IVertices {
attributes: Float32Array;
count: number;
}
const INDICES_PER_RECTANGLE = 8;
const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEMENT;
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
class Vertices {
public attributes: Float32Array;
public count: number;
constructor() {
this.attributes = new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY);
this.count = 0;
}
}
// Work variables to avoid garbage collection
let $rgba = 0;
let $isDefault = false;
let $x1 = 0;
let $y1 = 0;
let $r = 0;
@@ -77,11 +80,10 @@ export class RectangleRenderer extends Disposable {
private _attributesBuffer: WebGLBuffer;
private _projectionLocation: WebGLUniformLocation;
private _bgFloat!: Float32Array;
private _cursorFloat!: Float32Array;
private _vertices: IVertices = {
count: 0,
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY)
};
private _vertices: Vertices = new Vertices();
private _verticesCursor: Vertices = new Vertices();
constructor(
private _terminal: Terminal,
@@ -142,7 +144,15 @@ export class RectangleRenderer extends Disposable {
}));
}
public render(): void {
public renderBackgrounds(): void {
this._renderVertices(this._vertices);
}
public renderCursor(): void {
this._renderVertices(this._verticesCursor);
}
private _renderVertices(vertices: Vertices): void {
const gl = this._gl;
gl.useProgram(this._program);
@@ -153,8 +163,8 @@ export class RectangleRenderer extends Disposable {
// Bind attributes buffer and draw
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count);
gl.bufferData(gl.ARRAY_BUFFER, vertices.attributes, gl.DYNAMIC_DRAW);
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, vertices.count);
}
public handleResize(): void {
@@ -167,6 +177,7 @@ export class RectangleRenderer extends Disposable {
private _updateCachedColors(colors: ReadonlyColorSet): void {
this._bgFloat = this._colorToFloat32Array(colors.background);
this._cursorFloat = this._colorToFloat32Array(colors.cursor);
}
private _updateViewportRectangle(): void {
@@ -231,8 +242,72 @@ export class RectangleRenderer extends Disposable {
vertices.count = rectangleCount;
}
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
$isDefault = false;
public updateCursor(model: IRenderModel): void {
const vertices = this._verticesCursor;
const cursor = model.cursor;
if (!cursor || cursor.style === 'block') {
vertices.count = 0;
return;
}
let offset: number;
let rectangleCount = 0;
if (cursor.style === 'bar' || cursor.style === 'outline') {
// Left edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
cursor.x * this._dimensions.device.cell.width,
cursor.y * this._dimensions.device.cell.height,
cursor.style === 'bar' ? cursor.dpr * cursor.cursorWidth : cursor.dpr,
this._dimensions.device.cell.height,
this._cursorFloat
);
}
if (cursor.style === 'underline' || cursor.style === 'outline') {
// Bottom edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
cursor.x * this._dimensions.device.cell.width,
(cursor.y + 1) * this._dimensions.device.cell.height - cursor.dpr,
cursor.width * this._dimensions.device.cell.width,
cursor.dpr,
this._cursorFloat
);
}
if (cursor.style === 'outline') {
// Top edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
cursor.x * this._dimensions.device.cell.width,
cursor.y * this._dimensions.device.cell.height,
cursor.width * this._dimensions.device.cell.width,
cursor.dpr,
this._cursorFloat
);
// Right edge
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._addRectangleFloat(
vertices.attributes,
offset,
(cursor.x + cursor.width) * this._dimensions.device.cell.width - cursor.dpr,
cursor.y * this._dimensions.device.cell.height,
cursor.dpr,
this._dimensions.device.cell.height,
this._cursorFloat
);
}
vertices.count = rectangleCount;
}
private _updateRectangle(vertices: Vertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
if (fg & FgFlags.INVERSE) {
switch (fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
@@ -258,7 +333,6 @@ export class RectangleRenderer extends Disposable {
case Attributes.CM_DEFAULT:
default:
$rgba = this._themeService.colors.background.rgba;
$isDefault = true;
}
}
+2 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderModel } from './Types';
import { ICursorRenderModel, IRenderModel } from './Types';
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
@@ -18,6 +18,7 @@ export class RenderModel implements IRenderModel {
public cells: Uint32Array;
public lineLengths: Uint32Array;
public selection: ISelectionRenderModel;
public cursor?: ICursorRenderModel;
constructor() {
this.cells = new Uint32Array(0);
+11
View File
@@ -4,11 +4,22 @@
*/
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { CursorInactiveStyle, CursorStyle } from 'common/Types';
export interface IRenderModel {
cells: Uint32Array;
lineLengths: Uint32Array;
selection: ISelectionRenderModel;
cursor?: ICursorRenderModel;
}
export interface ICursorRenderModel {
x: number;
y: number;
width: number;
style: CursorStyle | CursorInactiveStyle;
cursorWidth: number;
dpr: number;
}
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
+7 -1
View File
@@ -8,9 +8,10 @@ import { ITerminal } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { getSafariVersion, isSafari } from 'common/Platform';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
import { WebglRenderer } from './WebglRenderer';
import { setTraceLogger } from 'common/services/LogService';
export class WebglAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
@@ -51,8 +52,13 @@ export class WebglAddon extends Disposable implements ITerminalAddon {
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
const decorationService: IDecorationService = unsafeCore._decorationService;
const logService: ILogService = unsafeCore._logService;
const themeService: IThemeService = unsafeCore._themeService;
// Set trace logger just in case it hasn't been yet which could happen when the addon is
// bundled separately to the core module
setTraceLogger(logService);
this._renderer = this.register(new WebglRenderer(
terminal,
characterJoinerService,
+109 -41
View File
@@ -4,33 +4,34 @@
*/
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ITerminal } from 'browser/Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, MutableDisposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData';
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { traceCall } from 'common/services/LogService';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { IDisposable, Terminal } from 'xterm';
import { Terminal } from 'xterm';
import { GlyphRenderer } from './GlyphRenderer';
import { RectangleRenderer } from './RectangleRenderer';
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel';
import { IWebGL2RenderingContext } from './Types';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { IRenderLayer } from './renderLayer/Types';
import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { IWebGL2RenderingContext } from './Types';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _charAtlasDisposable: IDisposable | undefined;
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
private _charAtlasDisposable = this.register(new MutableDisposable());
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
@@ -40,8 +41,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer?: RectangleRenderer;
private _glyphRenderer?: GlyphRenderer;
private _rectangleRenderer: MutableDisposable<RectangleRenderer> = this.register(new MutableDisposable());
private _glyphRenderer: MutableDisposable<GlyphRenderer> = this.register(new MutableDisposable());
public readonly dimensions: IRenderDimensions;
@@ -65,7 +66,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private readonly _characterJoinerService: ICharacterJoinerService,
private readonly _charSizeService: ICharSizeService,
private readonly _coreBrowserService: ICoreBrowserService,
coreService: ICoreService,
private readonly _coreService: ICoreService,
private readonly _decorationService: IDecorationService,
private readonly _optionsService: IOptionsService,
private readonly _themeService: IThemeService,
@@ -80,12 +81,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core = (this._terminal as any)._core;
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService),
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, _optionsService, this._themeService)
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService)
];
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
this._updateCursorBlink();
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._canvas = document.createElement('canvas');
@@ -127,7 +128,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core.screenElement!.appendChild(this._canvas);
[this._rectangleRenderer, this._glyphRenderer] = this._initializeWebGLState();
[this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
@@ -181,10 +182,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
this._rectangleRenderer?.setDimensions(this.dimensions);
this._rectangleRenderer?.handleResize();
this._glyphRenderer?.setDimensions(this.dimensions);
this._glyphRenderer?.handleResize();
this._rectangleRenderer.value?.setDimensions(this.dimensions);
this._rectangleRenderer.value?.handleResize();
this._glyphRenderer.value?.setDimensions(this.dimensions);
this._glyphRenderer.value?.handleResize();
this._refreshCharAtlas();
@@ -201,6 +202,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.handleBlur(this._terminal);
}
this._cursorBlinkStateManager.value?.pause();
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
@@ -209,6 +211,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.handleFocus(this._terminal);
}
this._cursorBlinkStateManager.value?.resume();
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
@@ -225,28 +228,26 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.handleCursorMove(this._terminal);
}
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
}
private _handleOptionsChanged(): void {
this._updateDimensions();
this._refreshCharAtlas();
this._updateCursorBlink();
}
/**
* Initializes members dependent on WebGL context state.
*/
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
// Dispose any previous rectangle and glyph renderers before creating new ones.
this._rectangleRenderer?.dispose();
this._glyphRenderer?.dispose();
this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService));
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions));
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService);
this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions);
// Update dimensions and acquire char atlas
this.handleCharSizeChanged();
return [this._rectangleRenderer, this._glyphRenderer];
return [this._rectangleRenderer.value, this._glyphRenderer.value];
}
/**
@@ -270,16 +271,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._coreBrowserService.dpr
);
if (this._charAtlas !== atlas) {
this._charAtlasDisposable?.dispose();
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
this._charAtlasDisposable = getDisposeArrayDisposable([
this._charAtlasDisposable.value = getDisposeArrayDisposable([
forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
]);
}
this._charAtlas = atlas;
this._charAtlas.warmUp();
this._glyphRenderer?.setAtlas(this._charAtlas);
this._glyphRenderer.value?.setAtlas(this._charAtlas);
}
/**
@@ -290,7 +290,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _clearModel(clearGlyphRenderer: boolean): void {
this._model.clear();
if (clearGlyphRenderer) {
this._glyphRenderer?.clear();
this._glyphRenderer.value?.clear();
}
}
@@ -305,6 +305,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (const l of this._renderLayers) {
l.reset(this._terminal);
}
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
this._updateCursorBlink();
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
@@ -315,6 +318,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
return false;
}
@traceCall
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
@@ -331,14 +335,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
l.handleGridChanged(this._terminal, start, end);
}
if (!this._glyphRenderer || !this._rectangleRenderer) {
if (!this._glyphRenderer.value || !this._rectangleRenderer.value) {
return;
}
// Tell renderer the frame is beginning
// upon a model clear also refresh the full viewport model
// (also triggered by an atlas page merge, part of #4480)
if (this._glyphRenderer.beginFrame()) {
if (this._glyphRenderer.value.beginFrame()) {
this._clearModel(true);
this._updateModel(0, this._terminal.rows - 1);
} else {
@@ -347,8 +351,24 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Render
this._rectangleRenderer?.render();
this._glyphRenderer?.render(this._model);
this._rectangleRenderer.value.renderBackgrounds();
this._glyphRenderer.value.render(this._model);
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible) {
this._rectangleRenderer.value.renderCursor();
}
}
private _updateCursorBlink(): void {
if (this._terminal.options.cursorBlink) {
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => {
this._requestRedrawCursor();
}, this._coreBrowserService);
} else {
this._cursorBlinkStateManager.clear();
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._requestRedrawCursor();
}
private _updateModel(start: number, end: number): void {
@@ -371,6 +391,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
let j: number;
start = clamp(start, terminal.rows - 1, 0);
end = clamp(end, terminal.rows - 1, 0);
const cursorY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
// in case cursor.x == cols adjust visual cursor to cols - 1
const cursorX = Math.min(this._terminal.buffer.active.cursorX, terminal.cols - 1);
let lastCursorX = -1;
const isCursorVisible =
this._coreService.isCursorInitialized &&
!this._coreService.isCursorHidden &&
(!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isCursorVisible);
this._model.cursor = undefined;
let modelUpdated = false;
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
@@ -414,6 +446,32 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Load colors/resolve overrides into work colors
this._cellColorResolver.resolve(cell, x, row);
// Override colors for cursor cell
if (isCursorVisible && row === cursorY) {
if (x === cursorX) {
this._model.cursor = {
x: cursorX,
y: this._terminal.buffer.active.cursorY,
width: cell.getWidth(),
style: this._coreBrowserService.isFocused ?
(terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle,
cursorWidth: terminal.options.cursorWidth,
dpr: this._devicePixelRatio
};
lastCursorX = cursorX + cell.getWidth() - 1;
}
if (x >= cursorX && x <= lastCursorX &&
((this._coreBrowserService.isFocused &&
(terminal.options.cursorStyle || 'block') === 'block') ||
(this._coreBrowserService.isFocused === false &&
terminal.options.cursorInactiveStyle === 'block'))) {
this._cellColorResolver.result.fg =
Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK);
this._cellColorResolver.result.bg =
Attributes.CM_RGB | (this._themeService.colors.cursor.rgba >> 8 & Attributes.RGB_MASK);
}
}
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}
@@ -426,6 +484,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
continue;
}
modelUpdated = true;
// Flag combined chars with a bit mask so they're easily identifiable
if (chars.length > 1) {
code |= COMBINED_CHAR_BIT_MASK;
@@ -437,7 +497,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
if (isJoined) {
// Restore work cell
@@ -446,7 +506,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x < lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
@@ -455,7 +515,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
}
}
this._rectangleRenderer!.updateBackgrounds(this._model);
if (modelUpdated) {
this._rectangleRenderer.value!.updateBackgrounds(this._model);
}
this._rectangleRenderer.value!.updateCursor(this._model);
}
/**
@@ -527,6 +590,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
}
private _requestRedrawCursor(): void {
const cursorY = this._terminal.buffer.active.cursorY;
this._onRequestRedraw.fire({ start: cursorY, end: cursorY });
}
}
// TODO: Share impl with core
@@ -3,17 +3,17 @@
* @license MIT
*/
import { IRenderLayer } from './Types';
import { ReadonlyColorSet } from 'browser/Types';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { Terminal } from 'xterm';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { CellData } from 'common/buffer/CellData';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { IRenderLayer } from './Types';
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -49,7 +49,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
}));
this.register(toDisposable(() => {
this._canvas.remove();
this._charAtlas?.dispose();
}));
}
@@ -121,21 +120,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
public abstract reset(terminal: Terminal): void;
/**
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
* @param x The column to start at.
* @param y The row to start at
* @param width The number of columns to fill.
* @param height The number of rows to fill.
*/
protected _fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
* existing fillStyle on the context.
@@ -150,35 +134,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this._coreBrowserService.dpr);
}
/**
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
this._coreBrowserService.dpr * width,
this._deviceCellHeight);
}
/**
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
* strokeStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
this._ctx.lineWidth = this._coreBrowserService.dpr;
this._ctx.strokeRect(
x * this._deviceCellWidth + this._coreBrowserService.dpr / 2,
y * this._deviceCellHeight + (this._coreBrowserService.dpr / 2),
width * this._deviceCellWidth - this._coreBrowserService.dpr,
(height * this._deviceCellHeight) - this._coreBrowserService.dpr);
}
/**
* Clears the entire canvas.
*/
@@ -1,375 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ICoreService, IOptionsService } from 'common/services/Services';
import { toDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
interface ICursorState {
x: number;
y: number;
isFocused: boolean;
style: string;
width: number;
}
/**
* The time between cursor blinks.
*/
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (terminal: Terminal, x: number, y: number, cell: ICellData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>,
coreBrowserService: ICoreBrowserService,
private readonly _coreService: ICoreService,
optionsService: IOptionsService,
themeService: IThemeService
) {
super(terminal, container, 'cursor', zIndex, true, coreBrowserService, optionsService, themeService);
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
this._handleOptionsChanged(terminal);
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal)));
this.register(toDisposable(() => {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}));
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
}
public reset(terminal: Terminal): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
this._handleOptionsChanged(terminal);
}
public handleBlur(terminal: Terminal): void {
this._cursorBlinkStateManager?.pause();
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public handleFocus(terminal: Terminal): void {
this._cursorBlinkStateManager?.resume(terminal);
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
private _handleOptionsChanged(terminal: Terminal): void {
if (terminal.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(() => {
this._render(terminal, true);
}, this._coreBrowserService);
}
} else {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public handleCursorMove(terminal: Terminal): void {
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
}
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
}
}
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
this._clearCursor();
return;
}
const cursorY = terminal.buffer.active.baseY + terminal.buffer.active.cursorY;
const viewportRelativeCursorY = cursorY - terminal.buffer.active.viewportY;
// in case cursor.x == cols adjust visual cursor to cols - 1
const cursorX = Math.min(terminal.buffer.active.cursorX, terminal.cols - 1);
// Don't draw the cursor if it's off-screen
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) {
this._clearCursor();
return;
}
// TODO: Need fast buffer API for loading cell
(terminal as any)._core.buffer.lines.get(cursorY).loadCell(cursorX, this._cell);
if (this._cell.content === undefined) {
return;
}
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = terminal.options.cursorStyle;
this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = cursorStyle!;
this._state.width = this._cell.getWidth();
return;
}
// Don't draw the cursor if it's blinking
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
this._clearCursor();
return;
}
if (this._state) {
// The cursor is already in the correct spot, don't redraw
if (this._state.x === cursorX &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === terminal.options.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
}
this._clearCursor();
}
this._ctx.save();
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = terminal.options.cursorStyle!;
this._state.width = this._cell.getWidth();
}
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
// less than 1
if (isFirefox || this._coreBrowserService.dpr < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
}
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
}
}
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
this._fillCharTrueColor(terminal, cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
}
class CursorBlinkStateManager {
public isCursorVisible: boolean;
private _animationFrame: number | undefined;
private _blinkStartTimeout: number | undefined;
private _blinkInterval: number | undefined;
/**
* The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers
* multiple times over a short period.
*/
private _animationTimeRestarted: number | undefined;
constructor(
private _renderCallback: () => void,
private _coreBrowserService: ICoreBrowserService
) {
this.isCursorVisible = true;
if (this._coreBrowserService.isFocused) {
this._restartInterval();
}
}
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
public dispose(): void {
if (this._blinkInterval) {
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public restartBlinkAnimation(terminal: Terminal): void {
if (this.isPaused) {
return;
}
// Save a timestamp so that the restart can be done on the next interval
this._animationTimeRestarted = Date.now();
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}
}
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
if (time > 0) {
this._restartInterval(time);
return;
}
}
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
// Setup the blink interval
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
this._restartInterval(time);
return;
}
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}, BLINK_INTERVAL);
}, timeToStart);
}
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public resume(terminal: Terminal): void {
// Clear out any existing timers just in case
this.pause();
this._animationTimeRestarted = undefined;
this._restartInterval();
this.restartBlinkAnimation(terminal);
}
}
@@ -21,6 +21,7 @@
},
"strict": true,
"downlevelIteration": true,
"experimentalDecorators": true,
"types": [
"../../../node_modules/@types/mocha"
]
@@ -4,9 +4,9 @@
*/
import { assert } from 'chai';
import { Browser, Page } from 'playwright';
import { Browser, Page } from '@playwright/test';
import { ITheme } from 'xterm';
import { getBrowserType, launchBrowser, openTerminal, pollFor, timeout, writeSync } from '../../../out-test/api/TestUtils';
import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils';
import { ITerminalOptions } from '../../../src/common/Types';
const APP = 'http://127.0.0.1:3001/test';
@@ -25,7 +25,7 @@ describe('WebGL Renderer Integration Tests', async () => {
itWebgl('dispose removes renderer canvases', async function(): Promise<void> {
await setupBrowser();
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3);
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2);
await page.evaluate(`addon.dispose()`);
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0);
await browser.close();
@@ -789,7 +789,7 @@ describe('WebGL Renderer Integration Tests', async () => {
await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
// Setting and check for minimum contrast values, note that these are note
// Setting and check for minimum contrast values, note that these are not
// exact to the contrast ratio, if the increase luminance algorithm
// changes then these will probably fail
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
@@ -858,7 +858,7 @@ describe('WebGL Renderer Integration Tests', async () => {
await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
// Setting and check for minimum contrast values, note that these are note
// Setting and check for minimum contrast values, note that these are not
// exact to the contrast ratio, if the increase luminance algorithm
// changes then these will probably fail
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
@@ -879,6 +879,75 @@ describe('WebGL Renderer Integration Tests', async () => {
await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]);
await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]);
});
itWebgl('should enforce half the contrast for dim cells', async () => {
const theme: ITheme = {
background: '#ffffff',
black: '#2e3436',
red: '#cc0000',
green: '#4e9a06',
yellow: '#c4a000',
blue: '#3465a4',
magenta: '#75507b',
cyan: '#06989a',
white: '#d3d7cf',
brightBlack: '#555753',
brightRed: '#ef2929',
brightGreen: '#8ae234',
brightYellow: '#fce94f',
brightBlue: '#729fcf',
brightMagenta: '#ad7fa8',
brightCyan: '#34e2e2',
brightWhite: '#eeeeec'
};
await page.evaluate(`
window.term.options.theme = ${JSON.stringify(theme)};
window.term.options.minimumContrastRatio = 1;
`);
// Block characters ignore block elements so a different char is used here
await writeSync(page,
'\\x1b[2m' +
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
);
// Validate before minimumContrastRatio is applied
await pollFor(page, () => getCellColor(1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]);
await pollFor(page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]);
await pollFor(page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]);
await pollFor(page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]);
await pollFor(page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]);
await pollFor(page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]);
await pollFor(page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]);
await pollFor(page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]);
await pollFor(page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]);
await pollFor(page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]);
await pollFor(page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]);
await pollFor(page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]);
await pollFor(page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]);
await pollFor(page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]);
await pollFor(page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]);
await pollFor(page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]);
// Setting and check for minimum contrast values, note that these are not
// exact to the contrast ratio, if the increase luminance algorithm
// changes then these will probably fail
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
await pollFor(page, () => getCellColor(1, 1), [150, 153, 154, 255]);
await pollFor(page, () => getCellColor(2, 1), [229, 127, 127, 255]);
await pollFor(page, () => getCellColor(3, 1), [63, 124, 4, 255]);
await pollFor(page, () => getCellColor(4, 1), [127, 104, 0, 255]);
await pollFor(page, () => getCellColor(5, 1), [153, 178, 209, 255]);
await pollFor(page, () => getCellColor(6, 1), [186, 167, 189, 255]);
await pollFor(page, () => getCellColor(7, 1), [4, 122, 124, 255]);
await pollFor(page, () => getCellColor(8, 1), [110, 112, 108, 255]);
await pollFor(page, () => getCellColor(1, 2), [170, 171, 169, 255]);
await pollFor(page, () => getCellColor(2, 2), [215, 36, 36, 255]);
await pollFor(page, () => getCellColor(3, 2), [72, 117, 25, 255]);
await pollFor(page, () => getCellColor(4, 2), [117, 109, 36, 255]);
await pollFor(page, () => getCellColor(5, 2), [72, 103, 135, 255]);
await pollFor(page, () => getCellColor(6, 2), [125, 91, 121, 255]);
await pollFor(page, () => getCellColor(7, 2), [25, 117, 117, 255]);
await pollFor(page, () => getCellColor(8, 2), [111, 111, 110, 255]);
});
});
describe('selectionBackground', async () => {
@@ -1037,7 +1106,7 @@ describe('WebGL Renderer Integration Tests', async () => {
async function getCellColor(col: number, row: number): Promise<number[]> {
await page.evaluate(`
window.gl = window.term._core._renderService._renderer._gl;
window.gl = window.term._core._renderService._renderer.value._gl;
window.result = new Uint8Array(4);
window.d = window.term._core._renderService.dimensions;
window.gl.readPixels(
@@ -1051,7 +1120,7 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
async function getCellPixels(col: number, row: number): Promise<number[]> {
await page.evaluate(`
window.gl = window.term._core._renderService._renderer._gl;
window.gl = window.term._core._renderService._renderer.value._gl;
window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4);
window.d = window.term._core._renderService.dimensions;
window.gl.readPixels(

Some files were not shown because too many files have changed in this diff Show More