Compare commits

..
895 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
Daniel Imms ac923e55d2 Merge pull request #4553 from Tyriar/v5_2_1
v5.2.1
2023-06-10 14:01:33 -07:00
Daniel Imms e08e5607ed v5.2.1 2023-06-10 13:59:51 -07:00
Daniel Imms 2ac2eeadfc Merge pull request #4551 from Tyriar/npes
Fix NPEs on buffer line and windowsPty option
2023-06-09 08:59:24 -07:00
Daniel Imms ffadc0b31d Fix NPEs on buffer line and windowsPty option 2023-06-09 08:47:17 -07:00
Daniel Imms cc1089e943 Merge pull request #4549 from Tyriar/r5_2_0
v5.2.0
2023-06-08 14:43:25 -07:00
Daniel Imms 47c21df83d v5.2.0 2023-06-08 14:32:54 -07:00
Daniel Imms 380e6805e8 Merge pull request #4539 from Tyriar/tyriar/windowsPty
Add more granular windowsPty option
2023-06-07 11:41:13 -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
Daniel Imms 9f424833d6 Merge pull request #4546 from jerch/fix_4545
DOM renderer link underline
2023-06-05 05:20:54 -07:00
Jörg Breitbart 90ccc815cb more verbose comments to get the idea of the code 2023-06-05 12:23:56 +02:00
Jörg Breitbart 8de7bd5850 rename lneX to maxY 2023-06-04 21:11:55 +02:00
Jörg Breitbart 9fed8e2a3a defensive underline event coords handling in DOM renderer 2023-06-04 21:06:26 +02:00
Daniel Imms 118915c1f7 Merge pull request #4542 from silamon/node18
Upgrade to Node18
2023-05-31 10:50:12 -07:00
Simon Lamon fca593d93b Fix unit testing (headless) 2023-05-31 17:37:41 +00:00
Simon Lamon 3376ee2f85 Node 18 2023-05-31 17:14:49 +00:00
Daniel Imms f6fbf63673 Add windowsPty unit test 2023-05-29 09:50:52 -07:00
Daniel Imms 3e5c6a156b Correct Windows wrapping heuristics condition 2023-05-29 09:49:33 -07:00
Daniel Imms a91665cb18 Listen for windowsPty changes 2023-05-29 09:43:49 -07:00
Daniel Imms 22140b5e94 Add more granular windowsPty option
See microsoft/vscode#134448
2023-05-29 09:40:00 -07:00
Daniel Imms 22a4dc4b6b Merge pull request #4478 from jerch/fix_4469
fix DECRQM reports
2023-05-24 06:47:52 -07:00
Megan Rogge 0ff06db417 bring back accessibility tree (#4536) 2023-05-22 21:17:25 +00:00
Daniel Imms 83a77b6024 Merge pull request #4533 from Tyriar/4480
Workaround drawing off invalid bitmap in canvas
2023-05-21 09:13:18 -07:00
Daniel Imms 8b422442b7 Close generator before deleting it 2023-05-21 09:05:21 -07:00
Daniel Imms 02ce3eff87 Workaround drawing off invalid bitmap in canvas
Fixes #4480
2023-05-21 08:51:31 -07:00
Daniel Imms 518b87f01d Merge pull request #4532 from Tyriar/4529
Finish overline support
2023-05-21 08:25:20 -07:00
Daniel Imms 6188c07bf7 Serialize overline
Fixes #4529
2023-05-21 08:17:06 -07:00
Daniel Imms 325d133a75 Add overline to vt comments
Part of #4529
2023-05-21 08:16:57 -07:00
Daniel Imms b662824b81 Add isOverline to public API
Part of #4529
2023-05-21 08:16:38 -07:00
Daniel Imms ada5bfe048 Merge pull request #4530 from jerch/fix_atlas_merge_webgl
fix blackout lines in webgl
2023-05-21 07:52:38 -07:00
Jörg Breitbart f270127ce5 fix webgl part of #4480 2023-05-21 16:32:57 +02:00
Daniel Imms 7460782494 Merge pull request #4527 from jerch/fix_4485
fix buffer corruption for utf8 transport in demo
2023-05-21 07:03:21 -07:00
Jörg Breitbart 048010c219 fix buffer corruption for utf8 transport 2023-05-21 12:28:15 +02:00
Daniel Imms cdec1e5b03 Merge pull request #4526 from Tyriar/4499_overline
Add support for SGR 53 overline and SGR 55 not overline
2023-05-20 14:21:22 -07:00
Daniel Imms eb56356133 Don't dodge overline, simplify drawing 2023-05-20 10:04:14 -07:00
Daniel Imms 2bfd95fe54 Add support for SGR 53, 55 overline
Specced in ECMA-48
2023-05-20 09:56:56 -07:00
Daniel Imms e9b0593298 Merge pull request #4525 from Tyriar/4161_dim
Don't apply dim to background color
2023-05-20 09:43:16 -07:00
Daniel Imms d5c47ec531 it -> itWebgl 2023-05-20 09:35:37 -07:00
Daniel Imms 5a145fcffe Add webgl api tests for dim 2023-05-20 08:41:11 -07:00
Daniel Imms 539fed4daa Don't apply dim to background color
Fixes #4161
2023-05-20 08:13:50 -07:00
Daniel Imms a0493a604c Merge pull request #4521 from jeanp413/jp/fix-forced-reflow
Avoid triggering a reflow while rendering search decorations
2023-05-17 16:04:16 -07:00
Jean Pierre 07e8f86bd1 Avoid triggering a reflow 2023-05-17 17:03:25 -05:00
Daniel Imms 1dbc1bf639 Merge pull request #4516 from jeanp413/jp/fix-decoration-render-layer
Ensure decorations in the top layer render on top of decorations in the bottom layer
2023-05-17 07:08:47 -07:00
Daniel Imms 6e8c59f0db Merge pull request #4504 from jeanp413/jp/fix-4444
Fixes improve search addon behavior when there are > 1000 results
2023-05-17 07:02:29 -07:00
Daniel Imms ec227c757b Prefix class with xterm-decoration- 2023-05-17 06:57:13 -07:00
Jean Pierre 4fbeb58203 Update search demo 2023-05-15 22:18:36 -05:00
Jean Pierre 60f549028f Ensure decorations in the top layer render on top of decorations in the bottom layer 2023-05-12 16:19:03 -05:00
Jean Pierre cc5136aeb3 Address feedback 2023-05-12 16:08:11 -05:00
Daniel Imms a1e9105ce7 Merge branch 'master' into jp/fix-4444 2023-05-12 06:24:34 -07:00
Daniel Imms 7a7f4f0add Merge pull request #4512 from xtermjs/Tyriar-patch-1
Add FAQ to new issue report
2023-05-10 06:40:53 -07:00
Daniel Imms 96ea9fe188 Add FAQ to new issue report
Hopefully this will reduce issue count a little
2023-05-10 06:31:54 -07:00
Jean Pierre b4e2293dbc Fix incremental 2023-05-09 16:14:28 -05:00
Daniel Imms 7194223696 Merge pull request #4508 from Tyriar/4507_zalgo
Disallow glyphs to exceed atlas texture size
2023-05-09 07:44:34 -07:00
Daniel Imms 8c9e44c219 Disallow glyphs to exceed atlas texture size
Fixes #4507
2023-05-09 07:35:41 -07:00
Daniel Imms 075f33031d Merge pull request #4475 from kelvinhammond/patch-1
Re-render browser DOM style onOptionsChanged, fixes xtermjs#4474
2023-05-09 07:12:58 -07:00
Jean Pierre e4b670231d Add tests 2023-05-08 09:08:35 -05:00
Jean Pierre 1c19f1d282 Remove incremental? 2023-05-07 15:19:23 -05:00
Jean Pierre 508589aa80 Improve search addon behavior when there are > 1000 results 2023-05-07 15:19:23 -05:00
Daniel Imms 854e2736f6 Move inject into _handleOptionsChanged 2023-05-07 06:27:56 -07:00
Kelvin 58f650e6f0 Merge branch 'master' into patch-1 2023-05-04 15:32:29 -04:00
Daniel Imms 913cb25863 Merge pull request #4491 from jerch/fix_4477
fix urls with screenreader
2023-05-03 08:19:23 -07:00
Daniel Imms 16f82cb3b5 Merge pull request #4500 from Tyriar/4487
Clear whole cursor layer on Firefox
2023-05-02 15:54:23 -07:00
Daniel Imms 0a101a01f5 Clear whole cursor layer on Firefox
Fixes #4487
2023-05-02 15:44:36 -07:00
Jörg Breitbart 9638063359 fix #4477 2023-04-24 12:30:41 +02:00
tisilent b7d73f63cd Add smoothScroll to scrollLines 2023-04-19 15:48:12 +08:00
Jörg Breitbart ee26780e03 fix test cases 2023-04-16 21:17:12 +02:00
Jörg Breitbart b35415ed16 flip SET 2 & 12 perma flags, add DECSET 67 (DECBKM) as perma reset 2023-04-16 21:08:42 +02:00
Kelvin 2a9ec8fd1d Re-render browser DOM style onOptionsChanged, fixes xtermjs#4474
Fixes https://github.com/xtermjs/xterm.js/issues/4474
2023-04-11 13:43:32 -04:00
Daniel Imms 2fdb46919c Merge pull request #4465 from Tyriar/4410
Ensure Decoration.isDisposed can be true
2023-03-31 12:21:52 -07:00
Daniel Imms c8e633ecb5 Merge pull request #4464 from Tyriar/4409
Correct registerMarker docs
2023-03-31 12:12:19 -07:00
Daniel Imms c546f7cc8a Ensure Decoration.isDisposed can be true
Fixes #4410
2023-03-31 12:11:50 -07:00
Daniel Imms 6997f1a940 Correct registerMarker docs
Fixes #4409
2023-03-31 11:55:30 -07:00
Daniel Imms 14c6df3187 Merge pull request #4458 from Tyriar/tyriar/178014
Clear line wrapped state on explicit line feed
2023-03-28 11:46:45 -07:00
Daniel Imms 18be346df9 Clear line wrapped state on explicit line feed
Fixes #4457
See microsoft/vscode#178014
2023-03-28 11:35:21 -07:00
Daniel Imms 37297e09ed Merge pull request #4456 from xtermjs/Tyriar-patch-1
Add DOM term to real world use cases
2023-03-28 09:04:25 -07:00
Daniel Imms b2f0cc8dc1 Add DOM term to real world use cases
Fixes #4455
2023-03-28 08:51:24 -07:00
Daniel Imms 6cbee3349e Merge pull request #4451 from Starwort/master
Fix #4450
2023-03-27 06:36:53 -07:00
Starwort f0edc1b613 Fix #4450 2023-03-27 09:56:17 +01:00
Daniel Imms cd9867e222 Merge pull request #4443 from tisilent/#4441
Uniform cursor unfocused rendering. #4441
2023-03-23 07:58:55 -07:00
tisilent e1d852c86a Adjust DomRenderer cursor style 2023-03-22 21:09:28 +08:00
tisilent 455c744ab1 Uniform cursor unfocused rendering. #4441 2023-03-22 20:54:58 +08:00
Daniel Imms 21f48ba803 Merge pull request #4438 from xtermjs/jerch-patch-1
fix BCE render bug in canvas renderer
2023-03-15 07:51:45 -07:00
jerch 49fdacba6a fix BCE render bug in canvas renderer 2023-03-15 15:41:44 +01:00
Daniel Imms a359c7ca10 Merge pull request #4437 from xtermjs/dependabot/npm_and_yarn/webpack-5.76.0
Bump webpack from 5.61.0 to 5.76.0
2023-03-15 06:31:53 -07:00
dependabot[bot] 242eade757 Bump webpack from 5.61.0 to 5.76.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.61.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-14 15:58:20 +00:00
Megan Rogge 4037b943b9 remove AccessibleBuffer (#4427)
* remove AccessibleBuffer

* rm more

* Update src/browser/Terminal.ts

* rm css
2023-03-02 14:51:32 -06:00
Labhansh Agrawal a8cec5f02f Enable ligatures addon on electron 2023-02-23 23:49:40 +05:30
Megan Rogge 9154831afa allow html elements (#4413) 2023-02-15 14:34:00 -06:00
Megan Rogge d6599bc048 accessibilityBuffer -> accessibleBuffer (#4406) 2023-02-08 21:48:20 +00:00
Megan Rogge 8a1faed5cb ensure line is defined via clamp (#4403) 2023-02-07 21:03:06 +00:00
Megan Rogge 72a157df5e add registerBufferElementProvider (#4402) 2023-02-07 09:56:23 -08:00
Megan Rogge 6daadc4c55 pull AccessibleBuffer out of AccessibilityManager (#4400) 2023-02-03 18:59:30 +00:00
Daniel Imms c05d0f650c Merge pull request #4398 from Tyriar/tyriar/a11y_remove
Remove accessibility tree code
2023-02-02 11:47:17 -08:00
Daniel Imms d5b5f1a9a4 Clean up 2023-02-02 06:24:18 -08:00
Daniel Imms dfcd674f8c Fix const -> let in LocalizableStrings
This needs to be settable outside, so const gives the wrong impression
2023-02-02 06:18:09 -08:00
Daniel Imms a7e0a9c534 Remove old accessibility tree code 2023-02-02 06:17:32 -08:00
Daniel Imms 5839accbf1 Use is prefix for boolean prop/getter 2023-02-02 06:12:04 -08:00
Megan Rogge 01c9a98e8e update accessibilityBufferActive on focusOut (#4395) 2023-02-01 20:57:52 +00:00
Daniel Imms bcfed71e1d Merge pull request #4393 from Tyriar/createinstance
Improve createInstance types
2023-02-01 12:38:30 -08:00
Daniel Imms 046d4b8314 Improve createInstance types 2023-02-01 08:42:51 -08:00
Megan Rogge a9da61e605 accessibilityBuffer: textarea -> div & prevent mouse click from exiting (#4389) 2023-01-31 18:25:25 -06:00
Daniel Imms 24c7be210c Merge pull request #4390 from Tyriar/webgl_exception
Handle glyph/rect renderer safer
2023-01-31 11:25:41 -08:00
Daniel Imms 9c815250b6 Handle glyph/rect renderer safer
See microsoft/vscode#172782
2023-01-31 11:14:33 -08:00
Megan Rogge 377f88796f append accessibility tree as child, not as adjacent elt to terminal (#4383) 2023-01-24 19:40:46 +00:00
Megan Rogge ed6c849501 attach and detach accessibility tree root element (#4382) 2023-01-24 09:53:10 -06:00
Megan Rogge 5a21948b4a make accessibility buffer readonly (#4381) 2023-01-23 19:03:19 +00:00
Megan Rogge b24bda92ad use textbox instead of div for accessibility buffer (#4377) 2023-01-20 17:36:25 +00:00
Megan Rogge 9895691e0e use content editable (#4376) 2023-01-19 21:30:58 +00:00
Megan Rogge 77870b06b9 fix #4374 (#4375) 2023-01-19 16:49:53 +00:00
Daniel Imms 41dc93a008 Prototype improved a11y buffer view (#4340) 2023-01-18 23:00:21 +00:00
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
Daniel Imms ba9f2e0f11 Merge pull request #4364 from Tyriar/dom_remove
Replace dom helper with HTMLElement.remove()
2023-01-05 09:22:55 -08:00
Daniel Imms b3fb8db6b3 Replace dom helper with HTMLElement.remove() 2023-01-05 09:14:16 -08:00
Daniel Imms 91946dcd9b Merge pull request #4363 from xtermjs/dependabot/npm_and_yarn/json5-2.2.3
Bump json5 from 2.1.2 to 2.2.3
2023-01-05 08:23:59 -08:00
dependabot[bot] 41c0e26292 Bump json5 from 2.1.2 to 2.2.3
Bumps [json5](https://github.com/json5/json5) from 2.1.2 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.1.2...v2.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 16:15:34 +00:00
Daniel Imms fe89817667 Merge pull request #4361 from xtermjs/circularlist-cleanup
Removed unused imports in CircularList
2022-12-28 07:19:26 -08:00
Eugene 7ee7aa2719 removed unused imports in CircularList 2022-12-28 15:41:58 +01:00
Daniel Imms b05c26c6fd Merge pull request #4358 from Tyriar/4357
Request links after viewport resize
2022-12-27 06:50:53 -08:00
Daniel Imms 0adbedf98d Request links after viewport resize
This clears the underline when hovering the link but this is an edge
case that isn't so important. As it's difficult to do outside of the
demo.

Fixes #4357
2022-12-27 06:41:58 -08:00
Daniel Imms 89ead0b4b7 Merge pull request #4356 from Coding-Kiwi/Coding-Kiwi-patch-1
Add graSSHopper to Real-world uses
2022-12-27 03:56:47 -08:00
Erik 349510065b Add graSSHopper to Real-world uses 2022-12-26 20:48:44 +01:00
Daniel Imms fa8a0e7f24 Merge pull request #4355 from Tyriar/4354
Clear canvas selection state after a resize
2022-12-24 08:10:44 -08:00
Daniel Imms 0c4e9c6b27 Merge pull request #4347 from silamon/deprecatedubuntu
Switch to Ubuntu 20.04
2022-12-24 08:06:14 -08:00
Daniel Imms de2b62eeac Merge pull request #4352 from Tyriar/4096
Include right part of wide char in underline gap
2022-12-24 08:05:41 -08:00
Daniel Imms 65b2af10f8 Clear canvas selection state after a resize
Fixes #4354
2022-12-24 08:02:41 -08:00
Daniel Imms 1a20fbb25f Merge pull request #4353 from Tyriar/4057
Force canvas renderer selection redraw on theme change
2022-12-24 08:02:06 -08:00
Daniel Imms d51ba2578b Force canvas renderer selection redraw on theme change
Fixes #4057
2022-12-24 07:46:51 -08:00
Daniel Imms 56734874ac Include right part of wide char in underline gap
Fixes #4096
2022-12-24 07:30:26 -08:00
Daniel Imms 72f1d1048a Merge pull request #4350 from Tyriar/dotted
Improve rendering of dotted underlines
2022-12-23 14:16:15 -08:00
Daniel Imms d17df65fad Improve rendering of dotted underlines
Fixes #4060
2022-12-23 14:04:12 -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
Simon Lamon c2504fa669 Ubuntu 18.04 deprecation 2022-12-23 17:37:26 +01:00
Daniel Imms 5a2bf6d039 Merge pull request #4346 from Tyriar/4314_2
Clamp start and end in render service to ensure they're valid
2022-12-21 17:03:15 -08:00
Daniel Imms 568e4c6935 Clamp start and end in render service to ensure they're valid
Fixes #4314
2022-12-21 16:55:03 -08:00
Daniel Imms 8b1c5c4ffb Revert "Fix NPE in webgl renderer"
This reverts commit ea7571c2b9.
2022-12-21 16:51:45 -08:00
Daniel Imms d34bb5ed79 Merge pull request #4343 from Tyriar/bump_other_addons
Bump other addons for v5.1 release
2022-12-21 08:28:46 -08:00
Daniel Imms ace4c3663f Bump other addons for v5.1 release
Fixes #4336
2022-12-21 08:27:01 -08:00
Daniel Imms 75075ebb70 Merge pull request #4342 from Tyriar/4341
Fix link event listener leak
2022-12-20 15:15:22 -08:00
Daniel Imms 7b8ad8c0b4 Improve comment 2022-12-20 15:01:30 -08:00
Daniel Imms f28c21eb77 Fix link event listener leak
The bug here is that _clearCurrentLink was being called only on the
line(s) that changed, but _askForLink was then being triggered
regardless. This caused more onRenderedViewportChange listeners to be
registered and for the thread blocking to get exponentially worse.

Fixes #4341
2022-12-20 14:58:35 -08:00
Daniel Imms 3142460db5 Merge pull request #4335 from jerch/cleanup_buffer
Cleanup Buffer.ts
2022-12-19 17:01:26 -08:00
Daniel Imms b97b1b6093 Merge pull request #4337 from Tyriar/whitespace_bump
Make package.json changes to trigger release
2022-12-19 12:55:19 -08:00
Daniel Imms 303db85763 Make package.json changes to trigger release
Fixes #4336
2022-12-19 12:55:01 -08:00
Jörg Breitbart 49ec7199e9 remover iterator and stringIndexToBufferIndex from buffer 2022-12-19 19:50:18 +01:00
Daniel Imms 19c760a204 Merge pull request #4333 from xtermjs/revert-4196-ansiheadless
Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal"
2022-12-19 10:20:24 -08:00
Daniel Imms 3a81ef54ca Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal" 2022-12-19 10:10:59 -08:00
Daniel Imms 6cfdcb1b0d Merge pull request #4332 from Tyriar/r5_1_0
v5.1.0
2022-12-19 09:56:50 -08:00
Daniel Imms 30ef1fcaa5 v5.1.0 2022-12-19 09:56:20 -08:00
Daniel Imms 84b8121105 Merge pull request #4288 from jerch/fix_weblinks
Fix weblinks
2022-12-19 08:57:15 -08:00
Jörg Breitbart 218cfd256c Merge branch 'master' into fix_weblinks 2022-12-19 15:59:11 +01:00
Jörg Breitbart 1757f2733c explicit type for link test data 2022-12-19 15:49:10 +01:00
Jörg Breitbart 9411acbc73 move link tests to demo 2022-12-19 15:33:10 +01:00
Daniel Imms cb4102cdcb Merge pull request #4330 from Tyriar/2533
Add VT buttons through CSI P
2022-12-19 06:30:11 -08:00
Daniel Imms 1926c9bbbc Add sequence buttons through CSI P 2022-12-19 06:20:01 -08:00
Daniel Imms 06145c02b1 Add IL vt button
Part of #2533
2022-12-19 06:11:34 -08:00
Jörg Breitbart d65898ffed Merge branch 'master' into fix_weblinks 2022-12-19 14:19:43 +01:00
Daniel Imms b5e6066f8c Merge pull request #4327 from Tyriar/2603
Improve IAttributeData jsdoc
2022-12-18 08:51:41 -08:00
Daniel Imms 0981ca8796 Merge pull request #4328 from Tyriar/vscode166909
Remove ! to improve safety
2022-12-16 11:16:17 -08:00
Daniel Imms ffeaa2b976 Remove ! to improve safety
See microsoft/vscode#166909

This doesn't necessarily fix that, but it does remove the hacky ! which could
potentially let an error slip past TS.
2022-12-16 11:05:03 -08:00
Daniel Imms 167765f932 Improve IAttributeData jsdoc
Fixes #2603
2022-12-16 10:53:21 -08:00
Daniel Imms bcdbb61ed4 Merge pull request #4326 from Tyriar/166878
Fix NPE in webgl renderer
2022-12-16 10:51:52 -08:00
Daniel Imms ea7571c2b9 Fix NPE in webgl renderer
Theory is this can happen when a resize down happens as end will be too big

See microsoft/vscode#166878
2022-12-16 10:42:05 -08:00
Daniel Imms 880b726bcb Merge pull request #4325 from Tyriar/4056
Fix canvas renderer selection not re-rendering sometimes
2022-12-16 09:39:28 -08:00
Daniel Imms 902da2800b Merge pull request #4324 from Ry0taK/feature/secure-links-by-default
Add allowNonHttpProtocols option to ILinkHandler
2022-12-16 09:32:54 -08:00
Daniel Imms 2bd9c4e839 Fix canvas renderer selection not re-rendering sometimes
Fixes #4056
2022-12-16 09:28:02 -08:00
RyotaK b3d36478ca Properly handle errors from URL constructor 2022-12-16 17:01:00 +00:00
RyotaK b07c016fea Adjust the comment 2022-12-16 16:58:55 +00:00
Daniel Imms 0b5625581e Merge pull request #4284 from JasonXJ/a11y-2
Fix a11y issues #4269
2022-12-16 08:57:55 -08:00
RyotaK e4b473785d Make comment more clear 2022-12-16 16:54:27 +00:00
RyotaK cb8d93a1ef Implement protocol validation 2022-12-16 16:50:57 +00:00
RyotaK 8636d0f820 Add allowNonHttpProtocols to typings 2022-12-17 01:31:11 +09:00
Daniel Imms f79644bd4c Merge pull request #4298 from jerch/fix_4295
re-eval previous active link
2022-12-16 08:12:19 -08:00
Daniel Imms 917e31ee17 Merge pull request #4322 from Tyriar/4124
Explain object option edge case
2022-12-15 17:06:24 -08:00
Daniel Imms aba59e9386 Explain object option edge case
Fixes #4124
2022-12-15 16:41:31 -08:00
Daniel Imms 1c7c4eb62f Merge pull request #4315 from Tyriar/2757
Allow scroll bar interaction in demo in screenReaderMode
2022-12-15 07:58:45 -08:00
Daniel Imms fc41968131 Allow scroll bar interaction in demo in screenReaderMode
Fixes #2757
2022-12-15 07:50:45 -08:00
Daniel Imms 04177de95b Merge pull request #4313 from Tyriar/4258
Support slash and triangle custom glyphs
2022-12-14 13:36:35 -08:00
Daniel Imms c9c07aeabd Support slash and triangle custom glyphs
This also fixes an issue with padding making glyphs regress

Fixes #4258
2022-12-14 13:15:05 -08:00
Daniel Imms 0cc759e0b1 Merge pull request #4312 from xtermjs/Tyriar-patch-1
Point new issue questions at GH discussions
2022-12-14 12:30:33 -08:00
Daniel Imms 852255ea03 Point new issue questions at GH discussions 2022-12-14 12:10:48 -08:00
Daniel Imms 88f459583e Merge pull request #4311 from Tyriar/vtbuttons
Add VT arrow buttons
2022-12-14 09:37:38 -08:00
Daniel Imms fd8a0690c9 Add VT arrow buttons
Part of #2064
2022-12-14 09:28:25 -08:00
Daniel Imms 6e2744a299 Merge pull request #4310 from Tyriar/4309
Improve explanation of danger with the linkHandler API
2022-12-14 08:21:55 -08:00
Daniel Imms 15e3b1ac12 Improve explanation of danger with the linkHandler API
Fixes #4309
2022-12-14 08:10:52 -08:00
Daniel Imms 5a5ed6d767 Merge pull request #4306 from Tyriar/4161
Add combo entries to sgr test for testing multiple sgr
2022-12-11 13:42:47 -08:00
Daniel Imms 74ea0c23ca Add combo entries to sgr test for testing multiple sgr 2022-12-11 13:11:16 -08:00
Daniel Imms 6d0a287ed9 Merge pull request #4305 from Tyriar/4221
Fix line height and letter spacing on canvas renderer
2022-12-11 11:54:57 -08:00
Daniel Imms 83ff4e97c3 Merge pull request #4304 from Tyriar/4259
Simplify options API
2022-12-11 11:52:08 -08:00
Daniel Imms 7b28dc4d59 Fix line height and letter spacing on canvas renderer
Fixes #4221
2022-12-11 11:47:09 -08:00
Daniel Imms 02d3ef599a Fix internal usages that needed Required<ITerminalOptions> 2022-12-11 11:42:14 -08:00
Daniel Imms c118f04187 Sync xterm-headless.d.ts 2022-12-11 11:36:32 -08:00
Daniel Imms 2bd3869547 Use a monospace font in example 2022-12-11 11:33:23 -08:00
Daniel Imms 69379ba59b Simplify options API
Fixes #4259
2022-12-11 11:33:03 -08:00
Daniel Imms 5ed05295ba Merge pull request #4301 from Tyriar/cleanup_coreservice
Avoid passing circular callback into CoreService ctor
2022-12-11 07:44:46 -08:00
Daniel Imms 4ee4a0f988 Merge branch 'master' into cleanup_coreservice 2022-12-11 07:36:01 -08:00
Daniel Imms d6b8a21219 Avoid passing circular callback into CoreService ctor 2022-12-11 07:35:11 -08:00
Daniel Imms 2e2d275d6c Merge pull request #4289 from JasonXJ/scroll-options
Add option scrollOnUserInput
2022-12-11 07:31:31 -08:00
Daniel Imms 33bba35be7 Merge pull request #4291 from jerch/fix_4223
fix demo in epiphany
2022-12-11 07:17:08 -08:00
jerch c7660fb49c Merge pull request #4299 from xtermjs/dependabot/npm_and_yarn/express-4.17.3
Bump express from 4.17.1 to 4.17.3
2022-12-10 20:50:46 +01:00
dependabot[bot] 6d527c42b0 Bump express from 4.17.1 to 4.17.3
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.17.3.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.17.3)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-10 17:06:32 +00:00
Jörg Breitbart 440da4a5ff reeval previous active link, fixes #4295 2022-12-09 16:37:56 +01:00
Jörg Breitbart e66fd50383 cleanup & more comments 2022-12-09 14:37:31 +01:00
Jörg Breitbart f69f9decca allow ~ within urls 2022-12-08 17:50:55 +01:00
Jörg Breitbart e70138b63c revert temp changes in linkifier2 2022-12-08 17:46:45 +01:00
Jörg Breitbart becec91e98 better regexp, fix offset issue in #4294 2022-12-08 17:35:45 +01:00
Jörg Breitbart 03dc1908a5 Merge branch 'master' into fix_weblinks 2022-12-08 15:19:08 +01:00
Jörg Breitbart d2298d84a0 move safari check to WebglAddon ctor 2022-12-07 23:56:43 +01:00
Jörg Breitbart 0b56b56c06 skipping renderer on NUL and SP 2022-12-07 22:55:45 +01:00
Daniel Imms 6d46b7b86f Merge pull request #4115 from jerch/buffer_optimizations
some buffer handling optimizations
2022-12-07 13:43:40 -08:00
Jörg Breitbart b829ad8386 Merge branch 'master' into fix_4223 2022-12-07 22:28:00 +01:00
Jörg Breitbart 9bc65c36a6 Merge branch 'master' into buffer_optimizations 2022-12-07 19:45:14 +01:00
Jörg Breitbart a562f33d53 remove nonsense exp runtime of rescans 2022-12-07 19:43:56 +01:00
Daniel Imms fe30cd6ed8 Merge pull request #4290 from JasonXJ/alt-space
Send corresponding escape code for alt+space and ctrl+alt+space
2022-12-07 06:34:23 -08:00
Jason Lin 9e84895141 polish 2022-12-07 10:12:43 +11:00
Daniel Imms 497df06f3f Merge pull request #4293 from Tyriar/4292
Log a warning if task queues exceed deadline by 20ms
2022-12-06 14:20:37 -08:00
Daniel Imms 650cda31fd Tweak logging 2022-12-06 14:10:40 -08:00
Daniel Imms dec3791f4c Log a warning if task queues exceed deadline by 20ms
Fixes #4292
2022-12-06 14:04:10 -08:00
Jörg Breitbart e46a9e12d0 fix demo in epiphany 2022-12-06 13:40:25 +01:00
Jason Lin 7f64fa9ddd Send corresponding escape code for alt+space and ctrl+alt+space
The previous behavior is that this combinations do nothing.
2022-12-06 15:57:19 +11:00
Jason Lin 73d20c1d3b Add option scrollOnKeypress
Note that we also change the existing behavior a bit:
CoreService.triggerDataEvent() will not scroll to the bottom unless
`wasUserInput` is also true. This actually seems to be the intended
behavior according to the doc just above
ICoreService.triggerDataEvent().
2022-12-06 14:29:18 +11:00
Jörg Breitbart fd44d76262 api tests for uri and ranges 2022-12-05 22:49:25 +01:00
Jörg Breitbart 309ba47483 change batch size to 500 2022-12-05 20:03:25 +01:00
Jörg Breitbart 4084dc3949 remove _forceMemoryCleanup 2022-12-05 19:10:55 +01:00
Jörg Breitbart 42bacc3283 disabled faulty code in linkifier2 2022-12-05 18:30:25 +01:00
Jörg Breitbart 46bd97247d disabled faulty code in linkifier2 2022-12-05 18:28:42 +01:00
Jörg Breitbart 3bf72cdefc DOM renderer fix with much better performance 2022-12-05 15:50:29 +01:00
Jörg Breitbart de23616589 better line expansion 2022-12-05 13:45:47 +01:00
Jörg Breitbart 39c2e57ad3 hack around DOM renderer issue, fix api tests 2022-12-04 22:26:29 +01:00
Jörg Breitbart d19361ac85 initial rewrite of addon mechs 2022-12-04 19:53:36 +01:00
Jörg Breitbart 1f5159febe use IdleTaskQueue & Date.now; some testcases 2022-12-03 16:49:26 +01:00
Jörg Breitbart 3a2ae96960 Merge branch 'master' into buffer_optimizations 2022-12-03 11:24:51 +01:00
Daniel Imms ad1f2ac2a8 Merge pull request #4287 from xtermjs/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2022-12-02 09:54:13 -08:00
dependabot[bot] b972e7f5ad Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 17:45:59 +00:00
Daniel Imms 503fe99b0a Merge pull request #4286 from Tyriar/tyriar/mocha
Update mocha
2022-12-02 09:45:34 -08:00
Daniel Imms 953cf005c0 Update mocha
This resolves a security alert
2022-12-02 09:35:36 -08:00
Jason Lin d177acccee Fix a11y issues #4269 2022-12-02 18:35:09 +11:00
Daniel Imms 4451369c66 Merge pull request #4280 from Tyriar/4278
Add custom keymap example to attachCustomKeyEventHandler docs
2022-11-30 07:15:12 -08:00
Daniel Imms 6b09c1b576 Add custom keymap example to attachCustomKeyEventHandler docs
Fixes #4278
2022-11-30 07:06:28 -08:00
Daniel Imms 71eb514bd0 Merge pull request #4276 from bugwheels94/bugwheels94-add-dependent-to-readme
Added application that uses xterm.js
2022-11-28 13:04:48 -08:00
Ankit cd552500fc Update README.md 2022-11-29 02:23:56 +05:30
Daniel Imms ed1773c103 Merge pull request #4274 from JasonXJ/cap-atlas-pages
Cap webgl maxAtlasPages to 32
2022-11-26 07:24:47 -08:00
Jason Lin b7c119a071 Cap webgl maxAtlasPages to 32 2022-11-24 17:51:58 +11:00
Daniel Imms 452241ca92 Merge pull request #4271 from jeanp413/fix-vscode-166158
Fixes disappearance of characters in webgl renderer
2022-11-23 09:36:15 -08:00
Daniel Imms 1880399849 dirtyId -> version 2022-11-23 08:51:06 -08:00
Jean Pierre d4ee0309d9 Fixes https://github.com/microsoft/vscode/issues/166158 2022-11-22 00:07:23 -05:00
Daniel Imms a351f5758a Merge pull request #4255 from Tyriar/safari16
Enable the webgl renderer on safari 16 and above
2022-11-08 12:30:23 -08:00
Daniel Imms 3597d43788 Enable the webgl renderer on safari 16 and above
This does not fix the issue where webkit browsers cannot use it, I looked
at webkit versions in safari and was a bit confused. The wikipedia page
for safari shows v15 having webkit v612+ but I have safari v16 and it's
webkit v605, so trying to detect it based on webkit version is a risky
change.

Note that webgl2 shipped in safari 15, but it didn't work in webgl until
safari 16.

Fixes #3357
Related #3575
Related microsoft/vscode#165416
2022-11-08 12:23:20 -08:00
Daniel Imms f381783bd5 Merge pull request #4196 from silamon/ansiheadless
SerializeAsHTML(): Fallback to default ansi colors when running headless terminal
2022-11-04 08:15:59 -07:00
Daniel Imms 6969a94fe4 Merge branch 'master' into ansiheadless 2022-11-04 08:08:43 -07:00
Daniel Imms 2377472eca Merge pull request #4216 from JasonXJ/fix-csi-0-m
'CSI 0 m' should clear the extended attribute
2022-11-03 19:58:18 -07:00
Daniel Imms 0edabed595 Merge pull request #4240 from Tyriar/4222_shims
Remove promise and fetch shims and use await for fetch
2022-11-03 19:56:25 -07:00
Daniel Imms 7d98375ce4 Merge pull request #4254 from Tyriar/clear_green
Clear custom glyph test button sgr
2022-11-03 06:56:23 -07:00
Daniel Imms 862e83d7be Merge pull request #4252 from Tyriar/4243_merge
Continually merge texture atlas pages
2022-11-03 06:48:55 -07:00
Daniel Imms 7cf9b68e2c Clear custom glyph test button sgr 2022-11-03 06:48:33 -07:00
Daniel Imms 97a0df260b Remove unused event 2022-11-03 05:44:50 -07:00
Daniel Imms c66dc82286 Merge pull request #4251 from Tyriar/4250
Add 1024 character limit to web links addon
2022-11-02 06:56:22 -07:00
Daniel Imms 6c29864847 Add 1024 character limit to web links addon
Fixes #4250
2022-11-02 06:45:25 -07:00
Daniel Imms d82da195f3 Remove atlas pages from demo when they're merged 2022-11-02 06:02:53 -07:00
Daniel Imms abfe391d91 Add CJK with random SGR test button
This is good as a stress test for the renderer
2022-11-02 05:46:40 -07:00
Daniel Imms c22e684904 Fix exception when last 4 smallest pages are merged 2022-11-02 05:40:13 -07:00
Daniel Imms 6406fd79c1 Enforce a maximum page merge amount 2022-11-02 05:28:47 -07:00
Daniel Imms 0271fdf49d Merge 4 largest pages 2022-11-02 04:49:02 -07:00
Daniel Imms 805b41b80f Keep merging default texture size pages 2022-11-01 10:26:48 -07:00
Daniel Imms c9bacc80d7 Merge when max atlas pages is reached 2022-11-01 10:08:00 -07:00
Daniel Imms 54ee698ba6 Basic merging working
Part of #4243
2022-11-01 09:07:38 -07:00
Daniel Imms 295e3c7f0c Merge pull request #4249 from Tyriar/4241_fill
Remove fill polyfill
2022-10-31 12:34:17 -07:00
Daniel Imms d219da480c Remove fill polyfill
Fixes #4241
2022-10-31 12:24:59 -07:00
Daniel Imms 02fa38c334 Merge pull request #4244 from Tyriar/multi_page_atlas
Support multiple texture atlas pages
2022-10-30 18:30:11 -07:00
Daniel Imms 4d05905736 Polish 2022-10-30 18:11:24 -07:00
Jason Lin 81a3c80bc4 polish 2022-10-31 11:07:42 +11:00
Jason Lin f6fabebf8d polish 2022-10-31 11:07:42 +11:00
Jason Lin b735e7f1c4 polish 2022-10-31 11:07:42 +11:00
Jason Lin 839271096e Don't reset the url 2022-10-31 11:07:42 +11:00
Jason Lin 6a087e9896 'CSI 0 m' should clear the extended attribute
This change also removes `_currentLinkId` from `InputHandler` and uses
`_curAttrData.extended.urlId` directly so that we don't need to worry
about keep them in sync any more.
2022-10-31 11:07:42 +11:00
Daniel Imms dda3f5f545 Properly dispose of atlas event 2022-10-30 15:00:27 -07:00
Daniel Imms 04041d030f Generate bitmaps for all atlas pages 2022-10-30 14:56:27 -07:00
Daniel Imms 095ceda639 Show canvas atlas pages in demo 2022-10-30 14:53:01 -07:00
Daniel Imms 0aa9fc5bb8 Add border around atlas pages 2022-10-30 12:00:55 -07:00
Daniel Imms e092ca70e1 Simplify page reuse/creation 2022-10-30 10:33:38 -07:00
Daniel Imms 0592837620 Fill pages to capacity 2022-10-30 10:21:17 -07:00
Daniel Imms fc03a1698b Share page creation code 2022-10-30 10:16:48 -07:00
Daniel Imms c9ef4a8eb9 Increase texture size as it nears the maximum texture unit
#4243 would be a better, but more complicated, solution to this
2022-10-30 09:20:17 -07:00
Daniel Imms 846573bb01 Support up to maximum texture units (eg. 16 on my machine) 2022-10-30 03:16:29 -07:00
Daniel Imms 0f70055d76 Add CJK test button 2022-10-30 03:04:58 -07:00
Daniel Imms 3f2ed42e73 Fix glyphs getting clipped on bottom row 2022-10-29 20:57:42 -07:00
Daniel Imms 2a3c2d9520 Make better use of atlas space 2022-10-29 20:33:27 -07:00
Daniel Imms 5e95475d6b Use current row when filling in old atlas pages 2022-10-29 19:50:44 -07:00
Daniel Imms 57a65e0858 Continute filling atlas page after new pages are added 2022-10-29 19:44:12 -07:00
Daniel Imms bfa9dd5060 Clean up 2022-10-29 18:09:18 -07:00
Daniel Imms 04f79aa7b4 Share page texture binding code 2022-10-29 14:33:50 -07:00
Daniel Imms 86ccd709bf Avoid setting uniforms every frame 2022-10-29 14:26:51 -07:00
Daniel Imms c257a7247e Only upload textures that change 2022-10-29 13:27:33 -07:00
Daniel Imms 0a49a5dde1 Make texture atlas presentation nicer for multiple pages 2022-10-29 13:01:41 -07:00
Daniel Imms c6e3cc007c Stop atlas flickering on demo 2022-10-29 12:49:11 -07:00
Daniel Imms 713baeb64d Base atlas page size on device pixel ratio 2022-10-29 12:45:20 -07:00
Daniel Imms efba2c9040 Get pages working, add to demo 2022-10-28 19:30:42 -07:00
Daniel Imms 67ef455515 Get a second page kind of working 2022-10-28 19:20:26 -07:00
Daniel Imms 975d52fab9 Remove deprecated prop 2022-10-28 19:12:01 -07:00
Daniel Imms 6b37187b4d Create AtlasPage 2022-10-28 19:08:33 -07:00
Daniel Imms 84ab6a49c4 Bring texturePage partially to atlas 2022-10-28 18:56:49 -07:00
Daniel Imms 4ebc75aae0 Merge remote-tracking branch 'upstream/master' into multi_page_atlas 2022-10-28 18:42:46 -07:00
Daniel Imms 54335ef54a Create texture page attribute 2022-10-28 18:16:55 -07:00
Daniel Imms 43d8db77f9 Merge pull request #4239 from Tyriar/4224_mcr_canvas_ydisp
Correctly offset mcr check on canvas renderer
2022-10-28 14:10:25 -07:00
Daniel Imms 5a4a46f130 Remove promise and fetch shims and use await for fetch
Fixes #4222
2022-10-28 14:09:39 -07:00
Daniel Imms b0672aac09 Correctly offset mcr check on canvas renderer
Fixes #4224
2022-10-28 14:02:58 -07:00
Daniel Imms fcf378b172 Merge pull request #4237 from Tyriar/italic_bounds
Fix clipping of italic emoji
2022-10-28 09:12:07 -07:00
Daniel Imms 25551a4b98 Merge pull request #4236 from Tyriar/console_image
Add console.image helper
2022-10-28 09:04:37 -07:00
Daniel Imms 67b3faef01 Use bbox padding and width consistently 2022-10-28 09:01:22 -07:00
Daniel Imms 8b545a3fe4 Fix bbox right scan start offset
This causes particularly wide glyphs like italic emoji to get clipped
even if they're within the allowed padding.

Fixes #4229
2022-10-28 08:57:41 -07:00
Daniel Imms 4f4ce0146d Add console.image helper
This is useful for printing canvases that aren't attached to the DOM
2022-10-28 08:54:57 -07:00
Daniel Imms e7b347a2ef Merge pull request #4235 from Tyriar/opt_server_buffer
Avoid GC pressure from server data buffer
2022-10-27 22:41:07 -07:00
Daniel Imms 7908b7cf62 Avoid GC pressure from server data buffer 2022-10-27 22:30:46 -07:00
Daniel Imms 392f8e9c98 Merge pull request #4234 from Tyriar/slow_isFocused
Cache ICoreBrowserService.isFocused per task
2022-10-27 17:00:18 -07:00
Daniel Imms b5cd4f1492 Cache ICoreBrowserService.isFocused per task
This is called for every selected cell when rendering which ends up
consuming a bunch of CPU due to the DOM calls.
2022-10-27 16:50:45 -07:00
Daniel Imms 4edfc12cbf Merge pull request #4233 from Tyriar/4113_resize
Fix resize in demo
2022-10-27 14:03:17 -07:00
Daniel Imms 19c1ee56d3 Fix resize in demo
A double resize could happen due to the mismatch in canvas vs device
pixel widths

Fixes #4113
2022-10-27 13:55:24 -07:00
Daniel Imms a4f8b07a7f Merge pull request #4226 from Tyriar/dropped_frames
Fix dropped frame issue
2022-10-21 13:10:30 -07:00
Daniel Imms 8d4d5bd54b Fix dropped frame issue
This ensures decorations are refreshed in the same frame when called from
an animation frame callback

Fixes #4225
2022-10-21 13:02:24 -07:00
Daniel Imms d5710afcb3 Merge pull request #4219 from Tyriar/4218_safari_bitmap
Disable canvas ImageBitmap optimization on Safari
2022-10-19 10:58:43 -07:00
Daniel Imms 8bd8195e30 Disable canvas ImageBitmap optimization on Safari
Fixes #4218
2022-10-19 10:03:01 -07:00
Daniel Imms ffe066dfef Merge pull request #4217 from Nokel81/fix-dom-renderering-jsdom-v2
Change clearing innerText to using replaceChildren to fix testing using jsdom
2022-10-19 07:21:30 -07:00
Sebastian Malton 11e90591f9 Change clearing innerText to using replaceChildren to fix testing using jsdom
Signed-off-by: Sebastian Malton <sebastian@malton.name>
2022-10-19 10:09:12 -04:00
Megan Rogge 0b84d17b18 revert decoration dispose changes (#4215) 2022-10-18 21:32:22 +00:00
Daniel Imms 376f0d9972 Merge pull request #4211 from hackermondev/patch-1
replit branding change
2022-10-17 07:45:06 -07:00
hackermon 478535d236 replit branding change 2022-10-17 09:49:54 -04:00
Daniel Imms 3f12d2925a Merge pull request #4208 from Tyriar/todo_attach_socket
Warn/throw on unexpected attach addon socket state
2022-10-17 03:48:31 -07:00
Daniel Imms 8e65ee8832 Merge pull request #4210 from Tyriar/todo_type_tabs
Fix a bunch of TODOs
2022-10-16 07:15:19 -07:00
Daniel Imms a7f0c3868c Merge pull request #4209 from Tyriar/todo_onData_event_test
Re-enable onData event test
2022-10-16 07:08:59 -07:00
Daniel Imms 96486c414b Acquire char size service better in webgl renderer 2022-10-16 07:04:34 -07:00
Daniel Imms b560d85553 Remove unneeded params as link provider is stable 2022-10-16 07:04:24 -07:00
Daniel Imms 7796b4101a Type Buffer.tabs 2022-10-16 06:58:20 -07:00
Daniel Imms a982484392 Re-enable onData event test 2022-10-16 06:55:04 -07:00
Daniel Imms e15c6b9f3e Warn/throw on unexpected attach addon socket state 2022-10-16 06:49:27 -07:00
Daniel Imms 807183bba6 Merge pull request #4207 from Tyriar/3925_dimensions
Improve names of IRenderDimensions interface members
2022-10-16 06:41:37 -07:00
Daniel Imms dc19628014 Fix width -> height 2022-10-16 06:34:12 -07:00
Daniel Imms ca68a5c59a Fix typo in test 2022-10-15 20:13:24 -07:00
Daniel Imms 22e8a2f8e7 Remove old dimension properties 2022-10-15 18:07:05 -07:00
Daniel Imms 171ba446bd Update other files 2022-10-15 15:32:02 -07:00
Daniel Imms 89e0b87eac Move to device/css in domrenderer 2022-10-15 14:22:04 -07:00
Daniel Imms 2c9e711cae Move all other refs in webgl over 2022-10-15 13:55:36 -07:00
Daniel Imms b0f1c38fd2 Move all other refs in canvas over 2022-10-15 13:44:52 -07:00
Daniel Imms b393008b54 Use new render dimensions in canvasrenderer 2022-10-15 13:19:04 -07:00
Daniel Imms 65f41a151e Use new render dimensions in webglrenderer 2022-10-15 13:15:12 -07:00
Daniel Imms ddc4e75583 Add new dimensions properties, helper creation function
Part of #3925
2022-10-15 10:31:43 -07:00
Daniel Imms 869ce3aa35 Merge pull request #4194 from silamon/fixmissingline
Capture data between terminal creation and connected to terminal in demo
2022-10-15 10:17:51 -07:00
Daniel Imms 16ea621a5f Merge pull request #4206 from Tyriar/eslint-jsdoc
Add eslint-plugin-jsdoc with minimal comment rules
2022-10-15 08:55:07 -07:00
Daniel Imms 5204374494 Add no-multi-asterisks rule 2022-10-15 08:43:06 -07:00
Daniel Imms 9f1a71485c Fix invalid jsdoc tags 2022-10-15 08:41:49 -07:00
Daniel Imms 49e0480f78 Add rule jsdoc/check-alignment 2022-10-15 08:39:32 -07:00
Daniel Imms 114924f3fa Add eslint-plugin-jsdoc and enable check-param-names 2022-10-15 08:38:14 -07:00
Daniel Imms aeba140c75 Merge pull request #4204 from silamon/proposedApiCheck
Remove check proposed api checks
2022-10-15 08:04:12 -07:00
Daniel Imms aea7680f76 Merge pull request #4205 from Tyriar/triangle_strip
Draw quads with triangle strip
2022-10-15 08:02:07 -07:00
Daniel Imms fb4a4e6127 Draw quads with triangle strip
This reduces the number of needed vertices from 6 to 4, it may not have a
noticeable performance impact but it's less data and simpler
2022-10-15 07:54:31 -07:00
Simon Lamon cc82f605fe delete clean up 2022-10-15 10:47:59 +00:00
Simon Lamon a055948937 unsentOutput instead of logs and dispose temporary listener 2022-10-15 10:45:23 +00:00
Simon Lamon 8246440795 Merge branch 'master' into fixmissingline 2022-10-15 10:11:40 +00:00
Simon Lamon 073aa6d439 Fix test "Proposed API check" 2022-10-15 10:25:39 +02:00
Simon Lamon 6bbe5a82a8 Remove check proposed api checks 2022-10-15 07:28:21 +00:00
Daniel Imms 1e6dfec4ae Merge pull request #4201 from Tyriar/4197_2
Remove clipImageData completely
2022-10-13 06:34:37 -07:00
Daniel Imms 5285b9ac45 Merge branch 'master' into 4197_2 2022-10-13 06:25:59 -07:00
Daniel Imms 50d1834871 Remove clipImageData completely
putImageData works a little differently to drawImage which confused me
for a bit, you need to offset the destination as putImageData draws the
'dirty' parts of the texture using the same source image dimensions

Fixes #4197
2022-10-13 06:25:13 -07:00
Daniel Imms b1f785879b Merge pull request #4200 from Tyriar/4197
Optimize _clipImageData
2022-10-12 10:24:28 -07:00
Daniel Imms 28fcffe46c Optimize _clipImageData
This makes clip image data around twice as fast, it's a little tricky to
measure it though. Changes:

- Work on Uint32Array instead of Uint8 so it's 1 assignment per pixel
  instead of 1 per channel.
- Perform the clipping in-place using the original image data to avoid
  allocation a new buffer.

Fixes #4197
2022-10-12 10:16:26 -07:00
Jörg Breitbart 14b99dca76 Merge branch 'master' into buffer_optimizations 2022-10-09 21:41:06 +02:00
Daniel Imms 1b13127f0b Merge pull request #4195 from Tyriar/4190
Create onSpecificOptionChange and onMultipleOptionChange helpers
2022-10-09 11:04:59 -07:00
Daniel Imms a37ea14d10 Use options service in renderers 2022-10-09 10:57:49 -07:00
Simon Lamon f8a5d3a91e Fallback to default_ansi_colors when running headless terminal 2022-10-09 17:16:06 +00:00
Daniel Imms 8d7137417b Split renderer safe vs unsafe api on activate 2022-10-09 10:12:19 -07:00
Daniel Imms e47296c1b9 Use options service directly in renderers 2022-10-09 09:28:18 -07:00
Daniel Imms 96b26b2e4a Merge pull request #4193 from silamon/fixdemoagain
Fix demo again
2022-10-09 09:23:07 -07:00
Daniel Imms 8ac88bc6bf Move Terminal option updates into owning components 2022-10-09 09:07:01 -07:00
Daniel Imms 7eead44048 Add onMultipleOptionChange 2022-10-09 08:14:56 -07:00
Daniel Imms 49a0904857 Move onSpecificOptionChange adoption 2022-10-09 08:07:43 -07:00
Simon Lamon a4c62792df revert formatting changes 2022-10-09 15:03:54 +00:00
Simon Lamon 1c345bef48 Handle onWillFire if it fails, so it loads dom renderer 2022-10-09 15:01:15 +00:00
Daniel Imms 415100e8b8 Adopt onSpecificOptionChange 2022-10-09 07:46:10 -07:00
Simon Lamon 627410a537 Add broken demo to investigate 2022-10-09 14:26:08 +00:00
Daniel Imms cad9c4164c onOptionChange/onSpecificOptionChange tests 2022-10-09 07:16:56 -07:00
Daniel Imms a87b6c3f2e Implement IOptionsService.onSpecificOptionChange
Part of #4190
2022-10-09 07:09:35 -07:00
Daniel Imms a2a855d747 Merge pull request #4192 from silamon/ubuntudeprecation
Switch to Ubuntu 20.04
2022-10-09 06:25:11 -07:00
Simon Lamon fb91b01217 Adds logs again in server.js 2022-10-09 12:16:26 +00:00
Simon Lamon bd661fdb07 Fix demo again 2022-10-09 11:29:33 +00:00
Simon Lamon f983b3cac1 Ubuntu 18.04 deprecation 2022-10-09 10:15:13 +00:00
Daniel Imms 0ba12301a6 Merge pull request #4188 from Tyriar/4184
Create theme service
2022-10-08 15:51:44 -07:00
Daniel Imms 97e084aa50 Remove document from test 2022-10-08 15:44:21 -07:00
Daniel Imms e0fb3b4d19 Remove ColorManager 2022-10-08 15:23:29 -07:00
Daniel Imms a1050d1a11 Merge ColorManager into ThemeService 2022-10-08 15:20:15 -07:00
Daniel Imms 7f9e3b1225 Remove resolved todo 2022-10-08 14:09:00 -07:00
Daniel Imms 48f15cb543 Remove remaining setColors methods 2022-10-08 14:07:28 -07:00
Daniel Imms 6e35bcd767 Use theme service in more places 2022-10-08 13:55:23 -07:00
Daniel Imms 80e474c6ad Viewport listen to theme service 2022-10-08 13:31:31 -07:00
Daniel Imms 4dee0c5d4b Build out theme service more 2022-10-08 12:15:21 -07:00
Daniel Imms a22ad76bd8 Merge pull request #4189 from Tyriar/ensure_canvas_changed
Ensure stale bitmap is not used when drawing new characters
2022-10-08 12:04:27 -07:00
Daniel Imms 4f5154fd21 Ensure stale bitmap is not used when drawing new characters
This would cause a problem where the first time a glyph is used it would
not be visible because it was using the old invalidated bitmap.
2022-10-08 11:56:29 -07:00
Daniel Imms cc6def57e4 Use readonly color set in canvas addon 2022-10-08 11:46:41 -07:00
Daniel Imms c4750eaf77 Create basic theme service
Part of #4184
2022-10-08 11:43:19 -07:00
Daniel Imms 8db2244f5d Merge pull request #4187 from Tyriar/4183_on_naming
Lint rule for on=event emitter and rename all methods with on prefix to handle
2022-10-08 11:14:15 -07:00
Daniel Imms 5d8e4a0af4 Add lint rule to ensure _on prefix is only for event emitters 2022-10-08 11:05:52 -07:00
Daniel Imms eecf52e949 Add lint rule to ensure on prefix is only for event emitters
Fixes #4183
2022-10-08 10:57:31 -07:00
Daniel Imms d8c31d7ad0 Merge pull request #4185 from Tyriar/4167_register_emitter
Fix a bunch of memory retention problems
2022-10-08 10:35:04 -07:00
Daniel Imms d8cf3af555 Fix Marker.dispose 2022-10-08 10:23:19 -07:00
Daniel Imms d3976c05f1 Fix marker dispose unit tests 2022-10-08 09:27:58 -07:00
Daniel Imms 94c3d11239 Fix compile 2022-10-08 09:11:28 -07:00
Daniel Imms 00f6d3a88c Register all event emitters, make some services disposable 2022-10-08 09:00:14 -07:00
Daniel Imms f61d8c91a3 Properly register disposables in SearchAddon 2022-10-08 08:30:39 -07:00
Daniel Imms ac659cb412 Prevent overriding of Disposable.dispose
A common mistake made was overriding dispose and not calling super.dispose,
so a TS trick is now used to move it to a readonly property instead of a
member method which prevents it being overridden.
2022-10-08 08:27:10 -07:00
Daniel Imms 34475f24c3 Release canvas texture atlas 2022-10-08 08:11:28 -07:00
Daniel Imms 8116e035b4 Register webgl renderers 2022-10-08 08:06:43 -07:00
Daniel Imms 56b37a73ac Free WebglAddon 2022-10-08 07:37:55 -07:00
Daniel Imms fc8043a25a Ensure webgl texture atlas is released on dispose 2022-10-08 07:32:51 -07:00
Daniel Imms f88c0e7480 Polish emitter registering in canvas renderer
Part of #4167
2022-10-08 07:24:35 -07:00
Daniel Imms d948b31304 Merge pull request #4181 from Tyriar/clear_race
Fix race condition that could when clearing atlas on 2+ terminals
2022-10-07 16:34:27 -07:00
Daniel Imms 9a4d5dbe26 Merge pull request #4182 from Tyriar/delete_canvas_atlas
Remove the canvas char atlas
2022-10-07 16:32:51 -07:00
Daniel Imms 8e7093f03b Remove the canvas char atlas
This was unused and removing from cache was broken apparently
2022-10-07 16:20:01 -07:00
Daniel ImmsandMegan Rogge c4eef2f470 Fix race condition that could when clearing atlas on 2+ terminals
This is the proper fix for microsoft/vscode#162996, after a bunch of
investigation we found that this is causing the problem:

VS Code clears the texture atlas on OS resume to fix an issue where the texture
could be corrupted. This calls clearTextureAtlas twice which should be fine, but
it's not because inside WebglRenderer.clear we're calling updateModel which is
meant to only ever be called in an animation frame. This calls potentially both
renderers to have stale views of the texture atlas for those rows and our perf
caching of cells (the main purpose of the model) doesn't know it's stale.

Co-Authored-By: Megan Rogge <merogge@microsoft.com>
2022-10-07 16:17:11 -07:00
Daniel Imms 5fcb084089 Merge pull request #4180 from Tyriar/clear_atlas
Fix clearTextureAtlas call as implemented on IRenderer
2022-10-07 14:31:28 -07:00
Daniel Imms e89c507d06 Fix clearTextureAtlas call as implemented on IRenderer
This is the root cause of microsoft/vscode#162996
2022-10-07 14:22:51 -07:00
Daniel Imms eb3de3ebd5 Merge pull request #4151 from Tyriar/renderer_before_open
Allow renderer to be set before Terminal.open is called
2022-10-07 10:17:54 -07:00
Daniel Imms ffa4d921bb Merge remote-tracking branch 'upstream/master' into renderer_before_open 2022-10-07 10:10:24 -07:00
Daniel Imms a060c64a08 Merge pull request #4179 from Tyriar/4178
Split Utf8ToUtf32 decoder unit tests up
2022-10-06 10:38:03 -07:00
Daniel Imms 0d5a526e86 Split Utf8ToUtf32 decoder unit tests up
Fixes #4178
2022-10-06 10:28:54 -07:00
Daniel Imms 3ec08948ec Merge pull request #4177 from Tyriar/tyriar/revert_events
Revert "Merge pull request #4166 from Tyriar/event_with_emitter"
2022-10-06 09:59:50 -07:00
Daniel Imms 4d97729010 Revert "Merge pull request #4166 from Tyriar/event_with_emitter"
This reverts commit d22f7c9edd, reversing
changes made to 1f8e6f0952.
2022-10-06 09:48:54 -07:00
Daniel Imms 5f1651ffbc Merge pull request #4176 from xtermjs/Tyriar-patch-1
Bump copyright year
2022-10-06 09:15:02 -07:00
Daniel Imms ab0082b73a Bump copyright year 2022-10-06 09:14:50 -07:00
Daniel Imms aa55d1bde6 Merge pull request #4170 from Tyriar/shared_cache
Share texture atlas cache code between webgl and canvas renderers
2022-10-05 10:19:17 -07:00
Daniel Imms e0324e32a8 Bring back ImageBitmap optimization in canvas renderer
It's more simple than it used to be, it will clear and debounce all
glyphs when any change is made.
2022-10-04 15:24:10 -07:00
Daniel Imms 3b260a2193 Fix resolved todo 2022-10-04 14:46:00 -07:00
Daniel Imms 622acaaad7 Remove old selection start/end values 2022-10-04 14:45:19 -07:00
Daniel Imms 0d7e18d236 Add CellColorResolver to share load color code 2022-10-04 14:41:27 -07:00
Daniel Imms 83c09c34db Share selection render model code 2022-10-04 14:23:09 -07:00
Daniel Imms 03e3105c55 Fix fg/bg overrides in canvas 2022-10-04 14:10:27 -07:00
Daniel Imms c5d015c908 Merge branch 'master' into shared_cache 2022-10-04 13:54:14 -07:00
Daniel Imms e1e8f28d97 Merge pull request #4118 from coderaiser/feature/lint-by-putout
Lint using 🐊Putout: part 7
2022-10-04 10:45:24 -07:00
Daniel Imms f6a61acc86 Merge branch 'master' into feature/lint-by-putout 2022-10-04 09:52:10 -07:00
Daniel Imms 6316eee672 Comment out debug log 2022-10-02 21:32:24 -07:00
Daniel Imms 9259ec258f Fix combined chars in canvas 2022-10-02 14:27:43 -07:00
Daniel Imms c741c13409 Add ps to output string 2022-10-02 14:25:08 -07:00
Daniel Imms f2ff3a6a7a Add sgr attr test 2022-10-02 14:21:06 -07:00
Daniel Imms dbef850039 Hook up attributes to new texture atlas 2022-10-02 13:57:35 -07:00
Daniel Imms 590a8219e3 Merge remote-tracking branch 'upstream/master' into shared_cache 2022-10-02 13:42:44 -07:00
Daniel Imms d22f7c9edd Merge pull request #4166 from Tyriar/event_with_emitter
Create new event with emitter object to simplify code
2022-10-02 13:41:37 -07:00
Daniel Imms c2bf87ee34 Initial working shared atlas 2022-10-02 11:38:46 -07:00
Daniel Imms 0bc5ca8f80 Fix imports 2022-10-02 11:08:06 -07:00
Daniel Imms 5b785d29a1 Expose _listeners for debug/test 2022-10-02 09:09:05 -07:00
Daniel Imms f38a49b4d5 Move remaining webgl atlas to shared 2022-10-02 08:48:55 -07:00
Daniel Imms 26c974f9b6 Shuffle some utils 2022-10-02 08:37:13 -07:00
Daniel Imms 333b0e28ac Add ITextureAtlas and use that instead of WebglCharAtlas 2022-10-02 07:48:43 -07:00
Daniel Imms 0bc71156bf Merge remote-tracking branch 'upstream/master' into shared_cache 2022-10-02 07:26:23 -07:00
Daniel Imms dece3cc41c DynamicCharAtlas -> CanvasCharAtlas 2022-10-02 07:20:55 -07:00
Daniel Imms 8752c4fa8e Remove canvas char atlas base class 2022-10-02 07:19:57 -07:00
Daniel Imms 53d0297898 Lint client.ts 2022-10-02 07:15:12 -07:00
Daniel Imms af6f03600f Show canvas texture atlas on demo 2022-10-02 07:10:37 -07:00
Daniel Imms 1f8e6f0952 Merge pull request #4169 from Tyriar/canvas_freq
Add willReadFrequently to canvas renderer too
2022-10-01 21:46:13 -07:00
Daniel Imms f087bd121e Add willReadFrequently to canvas renderer too 2022-10-01 21:38:01 -07:00
Daniel Imms d669c6e9d6 Fix remaining references 2022-10-01 18:03:43 -07:00
Daniel Imms c5f9eacbb1 Move other render parts to shared folder 2022-10-01 17:41:16 -07:00
Daniel Imms 6fcc496547 Move cache utils to core shared/ folder
Part of #4065
2022-10-01 17:36:16 -07:00
Daniel Imms 5dee66353a Merge pull request #4168 from Tyriar/4128
Ensure texture atlas comparison uses rgba not object
2022-10-01 10:30:17 -07:00
Daniel Imms 2a602ef231 Ensure texture atlas comparison uses rgba not object
Fixes #4128
2022-10-01 10:20:37 -07:00
Daniel Imms e3e3c347ea Merge pull request #4160 from jerch/tweak_serverside_buffering
tweak buffering in server.js
2022-10-01 09:59:01 -07:00
Daniel Imms 2e8b793ba0 Format files 2022-10-01 08:56:56 -07:00
Daniel Imms 03fcf60aaf Docs 2022-10-01 08:55:01 -07:00
Daniel Imms ed15a8e170 Create new event with emitter object to simplify code 2022-10-01 08:45:40 -07:00
Daniel Imms 969db2bcf9 Merge pull request #4163 from Tyriar/4147
Inline dirty row service into input handler
2022-10-01 08:28:29 -07:00
Daniel Imms 8e378c07e9 Merge pull request #4165 from Tyriar/4164
Define all events and emitters consistently
2022-10-01 08:28:18 -07:00
Daniel Imms 1a22c44f2b Make all emitters readonly 2022-10-01 08:02:32 -07:00
Daniel Imms cf9949840b Use readonly over getter for exposing events
Fixes #4164
2022-10-01 07:52:26 -07:00
Daniel Imms 3a90f7e7c0 Inline dirty row service into input handler
Fixes #4147
2022-10-01 07:39:02 -07:00
Daniel Imms 035025dba3 Merge pull request #4162 from Tyriar/w_refactor
Move w objects to $ prefix variables
2022-10-01 07:33:45 -07:00
Daniel Imms 52ed65d7bd Move w objects to $ prefix variables
Fixes #4138
2022-10-01 07:26:07 -07:00
Jörg Breitbart 150499b7e5 comment fix 2022-09-29 10:46:25 +02:00
Jörg Breitbart 193d305dcd revert timeout to 5ms, remove log in server.js 2022-09-29 10:36:08 +02:00
Jörg Breitbart 2236e230fc Merge branch 'master' into tweak_serverside_buffering 2022-09-28 20:06:01 +02:00
Jörg Breitbart cd6f370c28 unbuffered delivery for userInput response only 2022-09-28 20:03:55 +02:00
Daniel Imms 2659de2291 Merge pull request #4159 from Tyriar/4158
Use microtask to flush the write buffer after input
2022-09-28 10:46:15 -07:00
Daniel Imms 34df17e36d Call innerWrite immediately over a microtask 2022-09-28 09:51:28 -07:00
Daniel Imms 44388e5e6c Use microtask to flush the write buffer after input
Fixes #4158
2022-09-28 09:06:07 -07:00
Jörg Breitbart 785b932d75 lazy cleanup on buffer 2022-09-28 15:57:58 +02:00
Jörg Breitbart e58f73dc82 Merge branch 'master' into buffer_optimizations 2022-09-28 13:22:16 +02:00
Jörg Breitbart 7bba07e24e rename fourByteCells to uint32Cells 2022-09-28 13:18:31 +02:00
Jörg Breitbart 39eea8ed88 remove useless condition 2022-09-28 13:15:34 +02:00
Jörg Breitbart 20c1f1a5f7 fix resize edge cases: always report cleanup state 2022-09-28 12:14:09 +02:00
Jörg Breitbart e87f70d6d6 removed EMPTY_DATA branch 2022-09-28 11:55:26 +02:00
Daniel Imms 51c580e7a4 Merge pull request #4157 from Tyriar/4156
Fix trusted types in dom renderer
2022-09-27 13:15:25 -07:00
Daniel Imms 23328a394b Fix trusted types in dom renderer
Fixes #4156
2022-09-27 13:01:36 -07:00
Daniel Imms 399621eeb5 Merge pull request #4154 from Tyriar/qmt
Remove queuemicrotask polyfill
2022-09-27 07:41:57 -07:00
Daniel Imms 66675ef649 Merge pull request #4153 from Tyriar/microtask_fix
Fix repetitive microtask problem
2022-09-27 06:57:00 -07:00
Daniel Imms c6df208a3e Merge pull request #4155 from Tyriar/fix_priority_queue
Fix priority task queue and idle callback fallback
2022-09-27 06:56:52 -07:00
Daniel Imms 8201925fe8 Fix priority task queue and idle callback fallback
Mistake of not hooking up the abstract methods, this should throw on
Safari for example.
2022-09-27 06:46:19 -07:00
Daniel Imms d74e43a42a Remove queuemicrotask polyfill
Support seems good
2022-09-27 06:44:18 -07:00
Daniel Imms 25b69e5ed0 Fix repetitive microtask problem
See #4145
2022-09-27 06:43:07 -07:00
Daniel Imms 37c722d556 Merge pull request #4152 from dstein64/master
Move real-world uses section down
2022-09-26 08:27:21 -07:00
Daniel Steinberg 2b597a82a1 Move real-world uses section down 2022-09-26 11:19:44 -04:00
Daniel Imms 318abe641b Merge pull request #4145 from Tyriar/microtask_server
Optimize critical I/O path for input latency
2022-09-25 09:39:02 -07:00
Daniel Imms eca9c721d4 Merge pull request #4144 from Tyriar/priority_queue
Add PriorityTaskQueue
2022-09-25 09:38:44 -07:00
Daniel Imms 0b8120808c Prefer microtask to timeout in write queue
This seems to make little difference but it's more correct as we want
to write as fast as possible to minimize input latency
2022-09-24 16:27:56 -07:00
Daniel Imms c6e4788344 Use microtasks on server
This reduces demo input latency on my mac approx 10ms -> 4ms
2022-09-24 16:23:14 -07:00
Daniel Imms c548977a3e Fix imports 2022-09-24 16:16:14 -07:00
Daniel Imms 04d196a32d Use task queue interface 2022-09-24 16:14:38 -07:00
Daniel Imms 494fe3a95d Fill in ITaskQueue 2022-09-24 16:13:53 -07:00
Daniel Imms 8f8813a19a Add PriorityTaskQueue 2022-09-24 15:16:02 -07:00
Daniel Imms df8a57d14b Merge pull request #4143 from Tyriar/idle_deadline
Use the browser's IdleDeadline to determine max task time
2022-09-24 15:00:02 -07:00
Daniel Imms 868fc7ec29 Allow loading canvas before open 2022-09-24 14:44:13 -07:00
Daniel Imms 2febe19222 Set renderer after screen element is attached to dom 2022-09-24 14:42:57 -07:00
Daniel Imms 693d09cbad Allow setting renderer addon before open is called 2022-09-24 14:41:48 -07:00
Daniel Imms ab90dcd0dc Clarify comment 2022-09-24 14:13:04 -07:00
Daniel Imms 1f45d5344c Use the browser's IdleDeadline to determine max task time 2022-09-24 14:10:29 -07:00
Daniel Imms a9eea3fd46 Merge pull request #4142 from Tyriar/defer_paused_resize
Defer paused renderer resize to idle callback
2022-09-24 12:35:40 -07:00
Daniel Imms aec1f2b2be Fix import 2022-09-24 11:58:17 -07:00
Daniel Imms 66efb0388a Move prop to top 2022-09-24 11:57:12 -07:00
Daniel Imms 4ac5a91c95 Make DebouncedIdleTask helper 2022-09-24 11:56:46 -07:00
Daniel Imms 4e8e6b9788 Defer paused renderer resize to idle callback 2022-09-24 11:45:48 -07:00
Daniel Imms 275a8a91ee Merge pull request #4141 from Tyriar/idle_task_queue
Do char atlas warmup via new IdleTaskQueue
2022-09-24 11:44:10 -07:00
Daniel Imms 270ac1c7d9 Docs 2022-09-24 11:17:39 -07:00
Daniel Imms c42f290eb8 Do char atlas warmup via new IdleTaskQueue 2022-09-24 11:10:54 -07:00
Daniel Imms 8b323bbf69 Merge pull request #4140 from Tyriar/fast_setTheme
Share rgba vars throughout Color.ts, fast setTheme parseColor
2022-09-24 11:09:51 -07:00
Daniel Imms e68bf3ca5b Merge remote-tracking branch 'ups/master' into fast_setTheme 2022-09-24 11:02:15 -07:00
Daniel Imms b742da4bfa Merge pull request #4137 from Tyriar/willReadFrequently
Optimize contexts for reading frequently
2022-09-24 10:40:39 -07:00
Daniel Imms 6368cb4cff Move canvas-based parsing to Color.ts lib
This removed the allowTransparency code, it didn't actually do much
before, just whether the 'advanced parsing' would allow transparency.
Since we already do the common formats, I'm not even sure what other
format it could be.
2022-09-24 10:37:40 -07:00
Daniel Imms 81d68b115c Share rgba vars throughout Color.ts, fast setTheme parseColor 2022-09-24 10:00:57 -07:00
Daniel Imms d62b4531c3 Optimize contexts for reading frequently
This was a recommendation from Edge.
Docs here: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
2022-09-24 06:58:01 -07:00
Daniel Imms 1431210ecd Merge pull request #4125 from BattlefieldDuck/master
Update allowProposedApi comments
2022-09-23 13:27:25 -07:00
Daniel Imms 50b1e1045c Merge pull request #4134 from Tyriar/fix_upper_one_eighth_block
Fix width of upper 1/8 block char
2022-09-23 10:33:19 -07:00
Daniel Imms 09a5da5d33 Fix width of upper 1/8 block char
See microsoft/vscode#161323
2022-09-23 10:11:28 -07:00
Daniel Imms 349acbba34 Merge pull request #4131 from Tyriar/idleWarmup
Perform texture atlas warm up in an idle callback
2022-09-22 13:48:56 -07:00
Daniel Imms 78128762f9 Perform texture atlas warm up in an idle callback
Part of #4103
2022-09-22 13:06:08 -07:00
coderaiser 497cfcb74c Merge branch 'master' into feature/lint-by-putout 2022-09-19 12:00:14 +03:00
TatLead 11c806a4d0 Update xterm.d.ts 2022-09-19 05:54:11 +08:00
TatLead 9fb84cb89d Update xterm-headless.d.ts 2022-09-19 05:53:50 +08:00
Daniel Imms 1ceec0b6fa Merge pull request #4123 from Tyriar/remove_v5_publishing
Remove special v5 beta publishing
2022-09-15 13:02:17 -07:00
coderaiser bbff3e8396 Update src/common/services/InstantiationService.ts 2022-09-15 22:52:50 +03:00
Daniel Imms 73cb96ddec Remove special v5 beta publishing 2022-09-15 12:39:32 -07:00
coderaiser 25eb7d6472 chore: lint using 🐊Putout 2022-09-15 17:19:34 +03:00
Jörg Breitbart 6225cc2b9c optimize certain resize conditions 2022-09-13 14:37:29 +02:00
264 changed files with 17831 additions and 11541 deletions
-23
View File
@@ -1,23 +0,0 @@
FROM node:14-buster
# Configure apt
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils 2>&1
# Verify git and process tools are installed
RUN apt-get install -y git procps
# Install yarn
RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends \
yarn
# Clean up
RUN apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog
+17 -7
View File
@@ -1,10 +1,20 @@
{
"name": "xterm.js",
"dockerFile": "Dockerfile",
"appPort": 3000,
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
"features": {
"ghcr.io/devcontainers/features/node:1": {} // yarn
},
"forwardPorts": [
3000
],
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
}
}
}
+61 -6
View File
@@ -7,16 +7,20 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"demo/tsconfig.json",
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"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",
@@ -33,12 +37,14 @@
"sourceType": "module"
},
"ignorePatterns": [
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js"
],
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"jsdoc"
],
"rules": {
"no-extra-semi": "error",
@@ -85,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"] },
@@ -94,12 +105,34 @@
{ "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": "method", "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].+",
"match": false
} },
{ "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
"regex": "^on[A-Z].+",
"match": false
} },
{ "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
"regex": "^on[A-Z].+",
"match": false
} },
// typeLike
{ "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": [
@@ -128,7 +161,20 @@
"warn",
"always"
],
"jsdoc/check-alignment": 1,
"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": [
@@ -172,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",
@@ -193,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": ["-"]
}
]
}
}
+6 -3
View File
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://stackoverflow.com/questions/tagged/xtermjs
about: Please ask and answer questions here.
- name: FAQ
url: https://github.com/xtermjs/xterm.js/wiki/FAQ
about: See our frequently asked questions before filing a bug report
- name: Support / Q&A
url: https://github.com/xtermjs/xterm.js/discussions/categories/q-a
about: Use GitHub Discussions for community support and general Q&A
+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:
+28 -24
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
@@ -99,6 +97,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file](
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
## Releases
Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```bash
npm install -S xterm@beta
```
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
## Contributing
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
@@ -166,7 +184,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
- [**Replit**](https://replit.com): Collaborative browser based IDE with support for 50+ different languages.
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js.
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
@@ -193,34 +211,20 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Go SSH Web Client**](https://github.com/wuchihsu/go-ssh-web-client): A simple SSH web client using Go, WebSocket and Xterm.js.
- [**web3os**](https://web3os.sh): A decentralized operating system for the next web
- [**Cratecode**](https://cratecode.com): Learn to program for free through interactive online lessons. Cratecode uses xterm.js to give users access to their own Linux environment.
- [**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.
## Releases
Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```bash
npm install -S xterm@beta
```
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
## Contributing
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
## License Agreement
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-attach",
"version": "0.7.0",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+18 -4
View File
@@ -47,16 +47,14 @@ export class AttachAddon implements ITerminalAddon {
}
private _sendData(data: string): void {
// TODO: do something better than just swallowing
// the data if the socket is not in a working condition
if (this._socket.readyState !== 1) {
if (!this._checkOpenSocket()) {
return;
}
this._socket.send(data);
}
private _sendBinary(data: string): void {
if (this._socket.readyState !== 1) {
if (!this._checkOpenSocket()) {
return;
}
const buffer = new Uint8Array(data.length);
@@ -65,6 +63,22 @@ export class AttachAddon implements ITerminalAddon {
}
this._socket.send(buffer);
}
private _checkOpenSocket(): boolean {
switch (this._socket.readyState) {
case WebSocket.OPEN:
return true;
case WebSocket.CONNECTING:
throw new Error('Attach addon was loaded before socket was open');
case WebSocket.CLOSING:
console.warn('Attach addon socket is closing');
return false;
case WebSocket.CLOSED:
throw new Error('Attach addon socket is closed');
default:
throw new Error('Unexpected socket state');
}
}
}
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
@@ -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.2.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+214 -368
View File
@@ -3,77 +3,78 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderLayer } from './Types';
import { ICellData, IColor } from 'common/Types';
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { IGlyphIdentifier } from './atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
import { BaseCharAtlas } from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { AttributeData } from 'common/buffer/AttributeData';
import { IColorSet } from 'browser/Types';
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 { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
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 { ICoreBrowserService } from 'browser/services/Services';
import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils';
import { channels, color, rgba } from 'common/Color';
import { removeElementFromParent } from 'browser/Dom';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { Terminal } from 'xterm';
import { IRenderLayer } from './Types';
export abstract class BaseRenderLayer implements IRenderLayer {
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _scaledCharWidth: number = 0;
private _scaledCharHeight: number = 0;
private _scaledCellWidth: number = 0;
private _scaledCellHeight: number = 0;
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
private _deviceCharWidth: number = 0;
private _deviceCharHeight: number = 0;
private _deviceCellWidth: number = 0;
private _deviceCellHeight: number = 0;
private _deviceCharLeft: number = 0;
private _deviceCharTop: number = 0;
protected _selectionStart: [number, number] | undefined;
protected _selectionEnd: [number, number] | undefined;
protected _columnSelectMode: boolean = false;
protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
private _cellColorResolver: CellColorResolver;
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
protected _charAtlas: BaseCharAtlas | undefined;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
protected _charAtlas!: ITextureAtlas;
protected _charAtlasDisposable = this.register(new MutableDisposable());
public get canvas(): HTMLCanvasElement { return this._canvas; }
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
constructor(
private readonly _terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet,
private _rendererId: number,
protected readonly _themeService: IThemeService,
protected readonly _bufferService: IBufferService,
protected readonly _optionsService: IOptionsService,
protected readonly _decorationService: IDecorationService,
protected readonly _coreBrowserService: ICoreBrowserService
) {
super();
this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
}
this._refreshCharAtlas(this._themeService.colors);
this.register(this._themeService.onChangeColors(e => {
this._refreshCharAtlas(e);
this.reset();
// Trigger selection changed as it's handled separately to regular rendering
this.handleSelectionChanged(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}));
public dispose(): void {
removeElementFromParent(this._canvas);
this._charAtlas?.dispose();
this.register(toDisposable(() => {
this._canvas.remove();
}));
}
private _initCanvas(): void {
@@ -84,20 +85,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
public onOptionsChanged(): void {}
public onBlur(): void {}
public onFocus(): void {}
public onCursorMove(): void {}
public onGridChanged(startRow: number, endRow: number): void {}
public handleBlur(): void {}
public handleFocus(): void {}
public handleCursorMove(): void {}
public handleGridChanged(startRow: number, endRow: number): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
}
public setColors(colorSet: IColorSet): void {
this._refreshCharAtlas(colorSet);
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._selectionModel.update(this._terminal, start, end, columnSelectMode);
}
protected _setTransparency(alpha: boolean): void {
@@ -115,46 +109,50 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(this._colors);
this.onGridChanged(0, this._bufferService.rows - 1);
this._refreshCharAtlas(this._themeService.colors);
this.handleGridChanged(0, this._bufferService.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(colorSet: IColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
private _refreshCharAtlas(colorSet: ReadonlyColorSet): void {
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas = acquireTextureAtlas(this._terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
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);
}
}
public resize(dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
this._scaledCharHeight = dim.scaledCharHeight;
this._scaledCharLeft = dim.scaledCharLeft;
this._scaledCharTop = dim.scaledCharTop;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;
this._deviceCellWidth = dim.device.cell.width;
this._deviceCellHeight = dim.device.cell.height;
this._deviceCharWidth = dim.device.char.width;
this._deviceCharHeight = dim.device.char.height;
this._deviceCharLeft = dim.device.char.left;
this._deviceCharTop = dim.device.char.top;
this._canvas.width = dim.device.canvas.width;
this._canvas.height = dim.device.canvas.height;
this._canvas.style.width = `${dim.css.canvas.width}px`;
this._canvas.style.height = `${dim.css.canvas.height}px`;
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
this._refreshCharAtlas(this._colors);
this._refreshCharAtlas(this._themeService.colors);
}
public abstract reset(): void;
public clearTextureAtlas(): void {
this._charAtlas?.clear();
this._charAtlas?.clearTexture();
}
/**
@@ -166,24 +164,24 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
const cellOffset = Math.ceil(this._scaledCellHeight * 0.5);
const cellOffset = Math.ceil(this._deviceCellHeight * 0.5);
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight - cellOffset - this._coreBrowserService.dpr,
width * this._scaledCellWidth,
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight - cellOffset - this._coreBrowserService.dpr,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
@@ -195,9 +193,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._scaledCellWidth,
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
@@ -208,10 +206,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
for (let xOffset = 0; xOffset < width; xOffset++) {
const xLeft = (x + xOffset) * this._scaledCellWidth;
const xMid = (x + xOffset + 0.5) * this._scaledCellWidth;
const xRight = (x + xOffset + 1) * this._scaledCellWidth;
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
const xLeft = (x + xOffset) * this._deviceCellWidth;
const xMid = (x + xOffset + 0.5) * this._deviceCellWidth;
const xRight = (x + xOffset + 1) * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
const yMidBot = yMid - lineWidth;
const yMidTop = yMid + lineWidth;
this._ctx.moveTo(xLeft, yMid);
@@ -237,12 +235,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.setLineDash([lineWidth * 2, lineWidth]);
const xLeft = x * this._scaledCellWidth;
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
const xLeft = x * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
this._ctx.moveTo(xLeft, yMid);
for (let xOffset = 0; xOffset < width; xOffset++) {
// const xLeft = x * this._scaledCellWidth;
const xRight = (x + width + xOffset) * this._scaledCellWidth;
// const xLeft = x * this._deviceCellWidth;
const xRight = (x + width + xOffset) * this._deviceCellWidth;
this._ctx.lineTo(xRight, yMid);
}
this._ctx.stroke();
@@ -257,9 +255,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]);
const xLeft = x * this._scaledCellWidth;
const xRight = (x + width) * this._scaledCellWidth;
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
const xLeft = x * this._deviceCellWidth;
const xRight = (x + width) * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
this._ctx.moveTo(xLeft, yMid);
this._ctx.lineTo(xRight, yMid);
this._ctx.stroke();
@@ -275,10 +273,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
x * this._deviceCellWidth,
y * this._deviceCellHeight,
this._coreBrowserService.dpr * width,
this._scaledCellHeight);
this._deviceCellHeight);
}
/**
@@ -291,10 +289,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.strokeRect(
x * this._scaledCellWidth + lineWidth / 2,
y * this._scaledCellHeight + (lineWidth / 2),
width * this._scaledCellWidth - lineWidth,
(height * this._scaledCellHeight) - lineWidth);
x * this._deviceCellWidth + lineWidth / 2,
y * this._deviceCellHeight + (lineWidth / 2),
width * this._deviceCellWidth - lineWidth,
(height * this._deviceCellHeight) - lineWidth);
}
/**
@@ -304,7 +302,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
@@ -319,17 +317,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
} else {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
}
@@ -340,7 +338,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param cell The cell data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
* @param color The color of the character.
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
@@ -350,149 +347,73 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._deviceCellWidth, y * this._deviceCellHeight, this._deviceCellWidth, this._deviceCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
}
// Draw the character
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
x * this._deviceCellWidth + this._deviceCharLeft,
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
}
}
/**
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
* @param chars The character or characters.
* @param code The character code.
* @param width The width of the characters.
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
* @param bg The background color, in the format stored within the attributes.
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
const contrastColor = this._getContrastColor(cell, x, y);
const chars = cell.getChars();
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
// skip cache right away if we draw in RGB
// Note: to avoid bad runtime JoinedCellData will be skipped
// in the cache handler itself (atlasDidDraw == false) and
// fall through to uncached later down below
if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
this._drawUncachedChars(cell, x, y, contrastColor);
if (!this._charAtlas) {
return;
}
let fg;
let bg;
if (cell.isInverse()) {
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
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, true);
} else {
bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true);
}
const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
this._currentGlyphIdentifier.bg = bg;
this._currentGlyphIdentifier.fg = fg;
this._currentGlyphIdentifier.bold = !!cell.isBold();
this._currentGlyphIdentifier.dim = !!cell.isDim();
this._currentGlyphIdentifier.italic = !!cell.isItalic();
// Don't try cache the glyph if it uses any decoration foreground/background override.
let hasOverrides = false;
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
if (d.backgroundColorRGB || d.foregroundColorRGB) {
hasOverrides = true;
}
});
const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
if (!atlasDidDraw) {
this._drawUncachedChars(cell, x, y);
if (!glyph.size.x || !glyph.size.y) {
return;
}
}
/**
* Draws one or more characters at one or more cells. The character(s) will be
* clipped to ensure that they fit with the cell(s), including the cell to the
* right if the last character is a wide character.
* @param chars The character.
* @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = TEXT_BASELINE;
if (cell.isInverse()) {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isBgDefault()) {
this._ctx.fillStyle = color.opaque(this._colors.background).css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
this._clipRow(y);
// Apply alpha to dim the character
if (cell.isDim()) {
this._ctx.globalAlpha = DIM_OPACITY;
// Draw the image, use the bitmap if it's available
// HACK: If the canvas doesn't match, delete the generator. It's not clear how this happens but
// something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are
// swapped out unexpectedly
if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) {
this._bitmapGenerator[glyph.texturePage]?.bitmap?.close();
delete this._bitmapGenerator[glyph.texturePage];
}
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
if (!this._bitmapGenerator[glyph.texturePage]) {
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
}
this._bitmapGenerator[glyph.texturePage]!.refresh();
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version;
}
// Draw the character
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
this._ctx.drawImage(
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
glyph.texturePosition.x,
glyph.texturePosition.y,
glyph.size.x,
glyph.size.y,
x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x,
y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y,
glyph.size.x,
glyph.size.y
);
this._ctx.restore();
}
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param y The row to clip.
@@ -501,9 +422,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.beginPath();
this._ctx.rect(
0,
y * this._scaledCellHeight,
this._bufferService.cols * this._scaledCellWidth,
this._scaledCellHeight);
y * this._deviceCellHeight,
this._bufferService.cols * this._deviceCellWidth,
this._deviceCellHeight);
this._ctx.clip();
}
@@ -517,137 +438,62 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`;
}
private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined {
// Get any decoration foreground/background overrides, this must be fetched before the early
// exist but applied after inverse
let bgOverride: number | undefined;
let fgOverride: number | undefined;
let isTop = false;
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
return;
}
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba;
}
isTop = d.options.layer === 'top';
});
// Apply selection foreground if applicable
if (!isTop) {
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
fgOverride = this._colors.selectionForeground.rgba;
}
}
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) {
return undefined;
}
if (!bgOverride && !fgOverride) {
// Try get from cache
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
}
let fgColor = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bgColor = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
const isBold = !!cell.isInverse();
if (isInverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
if (!result) {
if (!fgOverride) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
return undefined;
}
// If it was an override and there was no contrast change, set as the result
result = fgOverride;
}
const color: IColor = {
css: channels.toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
),
rgba: result
};
if (!bgOverride && !fgOverride) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
}
return color;
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._colors.ansi[bgColor].rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.foreground.rgba;
}
return this._colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._colors.ansi[fgColor].rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.background.rgba;
}
return this._colors.foreground.rgba;
}
}
private _isCellInSelection(x: number, y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
if (this._columnSelectMode) {
return x >= start[0] && y >= start[1] &&
x < end[0] && y < end[1];
}
return (y > start[1] && y < end[1]) ||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}
}
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
const enum BitmapGeneratorState {
IDLE = 0,
GENERATING = 1,
GENERATING_INVALID = 2
}
class BitmapGenerator {
private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE;
private _commitTimeout: number | undefined = undefined;
private _bitmap: ImageBitmap | undefined = undefined;
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
public version: number = -1;
constructor(public readonly canvas: HTMLCanvasElement) {
}
public refresh(): void {
// Clear the bitmap immediately as it's stale
this._bitmap?.close();
this._bitmap = undefined;
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
if (isSafari) {
return;
}
if (this._commitTimeout === undefined) {
this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
}
if (this._state === BitmapGeneratorState.GENERATING) {
this._state = BitmapGeneratorState.GENERATING_INVALID;
}
}
private _generate(): void {
if (this._state === BitmapGeneratorState.IDLE) {
this._bitmap?.close();
this._bitmap = undefined;
this._state = BitmapGeneratorState.GENERATING;
window.createImageBitmap(this.canvas).then(bitmap => {
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
this.refresh();
} else {
this._bitmap = bitmap;
}
this._state = BitmapGeneratorState.IDLE;
});
if (this._commitTimeout) {
this._commitTimeout = undefined;
}
}
}
}
+53 -32
View File
@@ -3,45 +3,66 @@
* @license MIT
*/
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
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 implements ITerminalAddon {
export class CanvasAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: CanvasRenderer;
public activate(terminal: Terminal): void {
if (!terminal.element) {
throw new Error('Cannot activate CanvasAddon before Terminal.open');
}
this._terminal = terminal;
const bufferService: IBufferService = (terminal as any)._core._bufferService;
const renderService: IRenderService = (terminal as any)._core._renderService;
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
const charSizeService: ICharSizeService = (terminal as any)._core._charSizeService;
const coreService: ICoreService = (terminal as any)._core.coreService;
const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService;
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
const optionsService: IOptionsService = (terminal as any)._core.optionsService;
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
const screenElement: HTMLElement = (terminal as any)._core.screenElement;
const linkifier = (terminal as any)._core.linkifier2;
this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService);
renderService.setRenderer(this._renderer);
renderService.onResize(bufferService.cols, bufferService.rows);
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderer?.textureAtlas;
}
public dispose(): void {
if (!this._terminal) {
throw new Error('Cannot dispose CanvasAddon because it is activated');
public activate(terminal: Terminal): void {
const core = (terminal as any)._core as ITerminal;
if (!terminal.element) {
this.register(core.onWillOpen(() => this.activate(terminal)));
return;
}
const renderService: IRenderService = (this._terminal as any)._core._renderService;
renderService.setRenderer((this._terminal as any)._core._createRenderer());
renderService.onResize(this._terminal.cols, this._terminal.rows);
this._renderer?.dispose();
this._renderer = undefined;
this._terminal = terminal;
const coreService = core.coreService;
const optionsService = core.optionsService;
const screenElement = core.screenElement!;
const linkifier = core.linkifier2;
const unsafeCore = core as any;
const bufferService: IBufferService = unsafeCore._bufferService;
const renderService: IRenderService = unsafeCore._renderService;
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
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));
renderService.setRenderer(this._renderer);
renderService.handleResize(bufferService.cols, bufferService.rows);
this.register(toDisposable(() => {
renderService.setRenderer((this._terminal as any)._core._createRenderer());
renderService.handleResize(terminal.cols, terminal.rows);
this._renderer?.dispose();
this._renderer = undefined;
}));
}
}
+69 -89
View File
@@ -3,35 +3,37 @@
* @license MIT
*/
import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
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 { 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';
import { Terminal } from 'xterm';
import { CursorRenderLayer } from './CursorRenderLayer';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderLayer } from './Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier2 } from 'browser/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services';
import { removeTerminalFromCache } from './atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
let nextRendererId = 1;
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { TextRenderLayer } from './TextRenderLayer';
import { IRenderLayer } from './Types';
export class CanvasRenderer extends Disposable implements IRenderer {
private _id = nextRendererId++;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
public dimensions: IRenderDimensions;
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
public readonly onRequestRedraw = this._onRequestRedraw.event;
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
constructor(
private _colors: IColorSet,
private readonly _terminal: Terminal,
private readonly _screenElement: HTMLElement,
linkifier2: ILinkifier2,
private readonly _bufferService: IBufferService,
@@ -40,65 +42,47 @@ export class CanvasRenderer extends Disposable implements IRenderer {
characterJoinerService: ICharacterJoinerService,
coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService
decorationService: IDecorationService,
private readonly _themeService: IThemeService
) {
super();
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [
new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService),
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService),
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService),
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService)
new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService),
new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService),
new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
];
this.dimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
for (const layer of this._renderLayers) {
forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
}
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this.onOptionsChanged();
this.register(toDisposable(() => {
for (const l of this._renderLayers) {
l.dispose();
}
removeTerminalFromCache(this._terminal);
}));
}
public dispose(): void {
for (const l of this._renderLayers) {
l.dispose();
}
super.dispose();
removeTerminalFromCache(this._id);
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderLayers[0].cacheCanvas;
}
public onDevicePixelRatioChange(): void {
public handleDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
this.onResize(this._bufferService.cols, this._bufferService.rows);
this.handleResize(this._bufferService.cols, this._bufferService.rows);
}
}
public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
for (const l of this._renderLayers) {
l.setColors(this._colors);
l.reset();
}
}
public onResize(cols: number, rows: number): void {
public handleResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
@@ -108,36 +92,32 @@ export class CanvasRenderer extends Disposable implements IRenderer {
}
// Resize the screen
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
}
public onCharSizeChanged(): void {
this.onResize(this._bufferService.cols, this._bufferService.rows);
public handleCharSizeChanged(): void {
this.handleResize(this._bufferService.cols, this._bufferService.rows);
}
public onBlur(): void {
this._runOperation(l => l.onBlur());
public handleBlur(): void {
this._runOperation(l => l.handleBlur());
}
public onFocus(): void {
this._runOperation(l => l.onFocus());
public handleFocus(): void {
this._runOperation(l => l.handleFocus());
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode));
// Selection foreground requires a full re-render
if (this._colors.selectionForeground) {
if (this._themeService.colors.selectionForeground) {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
public onCursorMove(): void {
this._runOperation(l => l.onCursorMove());
}
public onOptionsChanged(): void {
this._runOperation(l => l.onOptionsChanged());
public handleCursorMove(): void {
this._runOperation(l => l.handleCursorMove());
}
public clear(): void {
@@ -156,7 +136,7 @@ export class CanvasRenderer extends Disposable implements IRenderer {
*/
public renderRows(start: number, end: number): void {
for (const l of this._renderLayers) {
l.onGridChanged(start, end);
l.handleGridChanged(start, end);
}
}
@@ -176,23 +156,23 @@ export class CanvasRenderer extends Disposable implements IRenderer {
// See the WebGL renderer for an explanation of this section.
const dpr = this._coreBrowserService.dpr;
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * dpr);
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr);
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * dpr);
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
this.dimensions.device.canvas.height = this._bufferService.rows * this.dimensions.device.cell.height;
this.dimensions.device.canvas.width = this._bufferService.cols * this.dimensions.device.cell.width;
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
}
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
this.dimensions.scaledCanvasHeight = height;
this.dimensions.scaledCanvasWidth = width;
this.dimensions.device.canvas.height = height;
this.dimensions.device.canvas.width = width;
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
@@ -3,14 +3,17 @@
* @license MIT
*/
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
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 } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
interface ICursorState {
x: number;
@@ -20,30 +23,25 @@ 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(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
private readonly _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _coreService: ICoreService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService
decorationService: IDecorationService,
themeService: IThemeService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._state = {
x: 0,
y: 0,
@@ -54,16 +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)
};
}
public dispose(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = undefined;
}
super.dispose();
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._handleOptionsChanged();
}
public resize(dim: IRenderDimensions): void {
@@ -80,45 +73,42 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation();
this.onOptionsChanged();
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
this._handleOptionsChanged();
}
public onBlur(): void {
this._cursorBlinkStateManager?.pause();
public handleBlur(): void {
this._cursorBlinkStateManager.value?.pause();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onFocus(): void {
this._cursorBlinkStateManager?.resume();
public handleFocus(): void {
this._cursorBlinkStateManager.value?.resume();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onOptionsChanged(): void {
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
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onCursorMove(): void {
this._cursorBlinkStateManager?.restartBlinkAnimation();
public handleCursorMove(): void {
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
}
public onGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
public handleGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isPaused) {
this._render(false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation();
this._cursorBlinkStateManager.value.restartBlinkAnimation();
}
}
@@ -148,12 +138,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
} else {
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;
@@ -165,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;
}
@@ -195,8 +184,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when device pixel ratio is less than 1
if (this._coreBrowserService.dpr < 1) {
// 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);
@@ -213,167 +203,31 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
this._ctx.fillStyle = this._colors.cursorAccent.css;
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
this._fillCharTrueColor(cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillBottomLineAtCells(x, y);
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._colors.cursor.css;
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,32 +3,33 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
import { ICoreBrowserService } from 'browser/services/Services';
import { is256Color } from './atlas/CharAtlasUtils';
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
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;
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
linkifier2: ILinkifier2,
bufferService: IBufferService,
optionsService: IOptionsService,
decorationService: IDecorationService,
coreBrowserService: ICoreBrowserService
coreBrowserService: ICoreBrowserService,
themeService: IThemeService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
}
public resize(dim: IRenderDimensions): void {
@@ -53,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
private _onShowLinkUnderline(e: ILinkifierEvent): void {
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
} else if (e.fg && is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
this._ctx.fillStyle = this._themeService.colors.foreground.css;
}
if (e.y1 === e.y2) {
@@ -77,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
this._state = e;
}
private _onHideLinkUnderline(e: ILinkifierEvent): void {
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
@@ -3,11 +3,11 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreBrowserService } from 'browser/services/Services';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Terminal } from 'xterm';
interface ISelectionState {
start?: [number, number];
@@ -20,16 +20,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
private _state!: ISelectionState;
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
bufferService: IBufferService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService,
optionsService: IOptionsService
optionsService: IOptionsService,
themeService: IThemeService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._clearState();
}
@@ -46,8 +46,9 @@ export class SelectionRenderLayer extends BaseRenderLayer {
super.resize(dim);
// On resize use the base render layer's cached selection values since resize clears _state
// inside reset.
if (this._selectionStart && this._selectionEnd) {
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
this._clearState();
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
}
@@ -58,18 +59,18 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
}
public onBlur(): void {
public handleBlur(): void {
this.reset();
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
public onFocus(): void {
public handleFocus(): void {
this.reset();
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.onSelectionChanged(start, end, columnSelectMode);
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.handleSelectionChanged(start, end, columnSelectMode);
this._redrawSelection(start, end, columnSelectMode);
}
@@ -101,8 +102,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
this._ctx.fillStyle = (this._coreBrowserService.isFocused
? this._colors.selectionBackgroundTransparent
: this._colors.selectionInactiveBackgroundTransparent).css;
? this._themeService.colors.selectionBackgroundTransparent
: this._themeService.colors.selectionInactiveBackgroundTransparent).css;
if (columnSelectMode) {
const startCol = start[0];
+29 -118
View File
@@ -3,18 +3,17 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/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 } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
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
@@ -31,19 +30,20 @@ export class TextRenderLayer extends BaseRenderLayer {
private _workCell = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
alpha: boolean,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _characterJoinerService: ICharacterJoinerService,
decorationService: IDecorationService,
coreBrowserService: ICoreBrowserService
coreBrowserService: ICoreBrowserService,
themeService: IThemeService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._state = new GridCache<CharData>();
this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value)));
}
public resize(dim: IRenderDimensions): void {
@@ -51,8 +51,8 @@ export class TextRenderLayer extends BaseRenderLayer {
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) {
this._characterWidth = dim.device.char.width;
this._characterFont = terminalFont;
this._characterOverlapCache = {};
}
@@ -93,6 +93,13 @@ export class TextRenderLayer extends BaseRenderLayer {
continue;
}
// exit early for NULL and SP
// NOTE: commented out due to #4120 (needs a more clever patch to keep things performant)
// const code = cell.getCode();
// if (code === 0 || code === 32) {
// continue;
// }
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
@@ -167,22 +174,16 @@ export class TextRenderLayer extends BaseRenderLayer {
if (cell.isInverse()) {
if (cell.isFgDefault()) {
nextFillStyle = this._colors.foreground.css;
nextFillStyle = this._themeService.colors.foreground.css;
} else if (cell.isFgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css;
}
} else if (cell.isBgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else if (cell.isBgPalette()) {
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
}
// Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is
// rarely used
if (nextFillStyle && cell.isDim()) {
nextFillStyle = color.multiplyOpacity(css.toColor(nextFillStyle), 0.5).css;
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
}
// Get any decoration foreground/background overrides, this must be fetched before the early
@@ -232,81 +233,10 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(firstRow: number, lastRow: number): void {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
if (cell.isInvisible()) {
return;
}
this._drawChars(cell, x, y);
if (cell.isUnderline() || cell.isStrikethrough()) {
this._ctx.save();
if (cell.isInverse()) {
if (cell.isBgDefault()) {
this._ctx.fillStyle = this._colors.background.css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
if (cell.isStrikethrough()) {
this._fillMiddleLineAtCells(x, y, cell.getWidth());
}
if (cell.isUnderline()) {
if (!cell.isUnderlineColorDefault()) {
if (cell.isUnderlineColorRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
} else {
let fg = cell.getUnderlineColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
switch (cell.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr);
this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr);
break;
case UnderlineStyle.CURLY:
this._curlyUnderlineAtCell(x, y, cell.getWidth());
break;
case UnderlineStyle.DOTTED:
this._dottedUnderlineAtCell(x, y, cell.getWidth());
break;
case UnderlineStyle.DASHED:
this._dashedUnderlineAtCell(x, y, cell.getWidth());
break;
case UnderlineStyle.SINGLE:
default:
this._fillBottomLineAtCells(x, y, cell.getWidth());
break;
}
}
this._ctx.restore();
}
});
this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y));
}
public onGridChanged(firstRow: number, lastRow: number): void {
public handleGridChanged(firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
@@ -321,10 +251,6 @@ export class TextRenderLayer extends BaseRenderLayer {
this._drawForeground(firstRow, lastRow);
}
public onOptionsChanged(): void {
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
@@ -363,19 +289,4 @@ export class TextRenderLayer extends BaseRenderLayer {
this._characterOverlapCache[chars] = overlaps;
return overlaps;
}
/**
* Clear the charcater at the cell specified.
* @param x The column of the char.
* @param y The row of the char.
*/
// private _clearChar(x: number, y: number): void {
// let colsToClear = 1;
// // Clear the adjacent character if it was wide
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
// colsToClear = 2;
// }
// this.clearCells(x, y, colsToClear, 1);
// }
}
+17 -43
View File
@@ -4,24 +4,8 @@
*/
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
// TODO: Use core interfaces
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRequestRedrawEvent {
start: number;
@@ -41,16 +25,14 @@ export interface IRenderer extends IDisposable {
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
handleDevicePixelRatioChange(): void;
handleResize(cols: number, rows: number): void;
handleCharSizeChanged(): void;
handleBlur(): void;
handleFocus(): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleCursorMove(): void;
handleOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
@@ -58,42 +40,34 @@ export interface IRenderer extends IDisposable {
export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
readonly cacheCanvas: HTMLCanvasElement;
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
/**
* Called when the terminal loses focus.
*/
onBlur(): void;
handleBlur(): void;
/**
* * Called when the terminal gets focus.
* Called when the terminal gets focus.
*/
onFocus(): void;
handleFocus(): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(): void;
/**
* Called when options change.
*/
onOptionsChanged(): void;
/**
* Called when the theme changes.
*/
setColors(colorSet: IColorSet): void;
handleCursorMove(): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(startRow: number, endRow: number): void;
handleGridChanged(startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Resize the render layer.
@@ -1,58 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IGlyphIdentifier } from './Types';
import { IDisposable } from 'common/Types';
export abstract class BaseCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
public dispose(): void { }
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
*/
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
/**
* Perform any work needed to warm the cache before it can be used. Used by the default
* implementation of warmUp(), and will only be called once.
*/
private _doWarmUp(): void { }
public clear(): void { }
/**
* Called when we start drawing a new frame.
*
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
* of BaseRenderLayer.
*/
public beginFrame(): void { }
/**
* May be called before warmUp finishes, however it is okay for the implementation to
* do nothing and return false in that case.
*
* @param ctx Where to draw the character onto.
* @param glyph Information about what to draw
* @param x The position on the context to start drawing at
* @param y The position on the context to start drawing at
* @returns The success state. True if we drew the character.
*/
public abstract draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean;
}
@@ -1,96 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { generateConfig, configEquals } from './CharAtlasUtils';
import { BaseCharAtlas } from './BaseCharAtlas';
import { DynamicCharAtlas } from './DynamicCharAtlas';
import { ICharAtlasConfig } from './Types';
import { IColorSet } from 'browser/Types';
import { ITerminalOptions } from 'xterm';
interface ICharAtlasCacheEntry {
atlas: BaseCharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: number[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
*/
export function acquireCharAtlas(
options: Required<ITerminalOptions>,
rendererId: number,
colors: IColorSet,
scaledCharWidth: number,
scaledCharHeight: number,
devicePixelRatio: number
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio);
// Check to see if the renderer already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
}
// The configs differ, release the renderer from the entry
if (entry.ownedBy.length === 1) {
entry.atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the renderer to the cache entry and return
entry.ownedBy.push(rendererId);
return entry.atlas;
}
}
const newEntry: ICharAtlasCacheEntry = {
atlas: new DynamicCharAtlas(
document,
newConfig
),
config: newConfig,
ownedBy: [rendererId]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
*/
export function removeTerminalFromCache(rendererId: number): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only renderer
charAtlasCache[i].atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharAtlasConfig } from './Types';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { IColorSet, IPartialColorSet } from 'browser/Types';
import { ITerminalOptions } from 'xterm';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required<ITerminalOptions>, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IPartialColorSet = {
foreground: colors.foreground,
background: colors.background,
cursor: undefined,
cursorAccent: undefined,
selectionBackground: undefined,
ansi: colors.ansi.slice()
};
return {
devicePixelRatio,
scaledCharWidth,
scaledCharHeight,
fontFamily: options.fontFamily,
fontSize: options.fontSize,
fontWeight: options.fontWeight,
fontWeightBold: options.fontWeightBold,
allowTransparency: options.allowTransparency,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
return false;
}
}
return a.devicePixelRatio === b.devicePixelRatio &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR;
}
@@ -1,411 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
import { IGlyphIdentifier, ICharAtlasConfig } from './Types';
import { BaseCharAtlas } from './BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
import { LRUMap } from './LRUMap';
import { isFirefox, isSafari } from 'common/Platform';
import { IColor } from 'common/Types';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { color } from 'common/Color';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
const TRANSPARENT_COLOR = {
css: 'rgba(0, 0, 0, 0)',
rgba: 0
};
// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
// cache in a single frame, give up on trying to cache anything else, and try to finish the current
// frame ASAP.
//
// This helps to limit the amount of damage a program can do when it would otherwise thrash the
// cache.
const FRAME_CACHE_DRAW_LIMIT = 100;
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
interface IGlyphCacheValue {
index: number;
isEmpty: boolean;
inBitmap: boolean;
}
export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
// Note that this only returns a valid key when code < 256
// Layout:
// 0b00000000000000000000000000000001: italic (1)
// 0b00000000000000000000000000000010: dim (1)
// 0b00000000000000000000000000000100: bold (1)
// 0b00000000000000000000111111111000: fg (9)
// 0b00000000000111111111000000000000: bg (9)
// 0b00011111111000000000000000000000: code (8)
// 0b11100000000000000000000000000000: unused (3)
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
}
export class DynamicCharAtlas extends BaseCharAtlas {
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
// It's ordered so that we can determine when to remove the old entries.
private _cacheMap: LRUMap<IGlyphCacheValue>;
// The texture that the atlas is drawn to
private _cacheCanvas: HTMLCanvasElement;
private _cacheCtx: CanvasRenderingContext2D;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// The number of characters stored in the atlas by width/height
private _width: number;
private _height: number;
private _drawToCacheCount: number = 0;
// An array of glyph keys that are waiting on the bitmap to be generated.
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
private _bitmapCommitTimeout: number | null = null;
// The bitmap to draw from, this is much faster on other browsers than others.
private _bitmap: ImageBitmap | null = null;
constructor(document: Document, private _config: ICharAtlasConfig) {
super();
this._cacheCanvas = document.createElement('canvas');
this._cacheCanvas.width = TEXTURE_WIDTH;
this._cacheCanvas.height = TEXTURE_HEIGHT;
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = this._config.scaledCharWidth;
tmpCanvas.height = this._config.scaledCharHeight;
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
const capacity = this._width * this._height;
this._cacheMap = new LRUMap(capacity);
this._cacheMap.prealloc(capacity);
// This is useful for debugging
// document.body.appendChild(this._cacheCanvas);
}
public dispose(): void {
if (this._bitmapCommitTimeout !== null) {
window.clearTimeout(this._bitmapCommitTimeout);
this._bitmapCommitTimeout = null;
}
}
public beginFrame(): void {
this._drawToCacheCount = 0;
}
public clear(): void {
if (this._cacheMap.size > 0) {
const capacity = this._width * this._height;
this._cacheMap = new LRUMap(capacity);
this._cacheMap.prealloc(capacity);
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean {
// Space is always an empty cell, special case this as it's so common
if (glyph.code === 32) {
return true;
}
// Exit early for uncachable glyphs
if (!this._canCache(glyph)) {
return false;
}
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue !== null && cacheValue !== undefined) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
}
if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
let index;
if (this._cacheMap.size < this._cacheMap.capacity) {
index = this._cacheMap.size;
} else {
// we're out of space, so our call to set will delete this item
index = this._cacheMap.peek()!.index;
}
const cacheValue = this._drawToCache(glyph, index);
this._cacheMap.set(glyphKey, cacheValue);
this._drawFromCache(ctx, cacheValue, x, y);
return true;
}
return false;
}
private _canCache(glyph: IGlyphIdentifier): boolean {
// Only cache ascii and extended characters for now, to be safe. In the future, we could do
// something more complicated to determine the expected width of a character.
//
// If we switch the renderer over to webgl at some point, we may be able to use blending modes
// to draw overlapping glyphs from the atlas:
// https://github.com/servo/webrender/issues/464#issuecomment-255632875
// https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
return glyph.code < 256;
}
private _toCoordinateX(index: number): number {
return (index % this._width) * this._config.scaledCharWidth;
}
private _toCoordinateY(index: number): number {
return Math.floor(index / this._width) * this._config.scaledCharHeight;
}
private _drawFromCache(
ctx: CanvasRenderingContext2D,
cacheValue: IGlyphCacheValue,
x: number,
y: number
): void {
// We don't actually need to do anything if this is whitespace.
if (cacheValue.isEmpty) {
return;
}
const cacheX = this._toCoordinateX(cacheValue.index);
const cacheY = this._toCoordinateY(cacheValue.index);
ctx.drawImage(
cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
cacheX,
cacheY,
this._config.scaledCharWidth,
this._config.scaledCharHeight,
x,
y,
this._config.scaledCharWidth,
this._config.scaledCharHeight
);
}
private _getColorFromAnsiIndex(idx: number): IColor {
if (idx < this._config.colors.ansi.length) {
return this._config.colors.ansi[idx];
}
return DEFAULT_ANSI_COLORS[idx];
}
private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
}
let result: IColor;
if (glyph.bg === INVERTED_DEFAULT_COLOR) {
result = this._config.colors.foreground;
} else if (glyph.bg < 256) {
result = this._getColorFromAnsiIndex(glyph.bg);
} else {
result = this._config.colors.background;
}
if (glyph.dim) {
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, 0.5));
}
return result;
}
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
if (glyph.fg === INVERTED_DEFAULT_COLOR) {
return color.opaque(this._config.colors.background);
}
if (glyph.fg < 256) {
// 256 color support
return this._getColorFromAnsiIndex(glyph.fg);
}
return this._config.colors.foreground;
}
// TODO: We do this (or something similar) in multiple places. We should split this off
// into a shared function.
private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
this._drawToCacheCount++;
this._tmpCtx.save();
// draw the background
const backgroundColor = this._getBackgroundColor(glyph);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
this._tmpCtx.globalCompositeOperation = 'source-over';
// draw the foreground/glyph
const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = TEXT_BASELINE;
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
// Apply alpha to dim the character
if (glyph.dim) {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
let imageData = this._tmpCtx.getImageData(
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
);
let isEmpty = false;
if (!this._config.allowTransparency) {
isEmpty = clearColor(imageData, backgroundColor);
}
// If this charcater is underscore and empty, shift it up until it is visible, try for a maximum
// of 5 pixels.
if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) {
for (let offset = 1; offset <= 5; offset++) {
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset);
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
imageData = this._tmpCtx.getImageData(
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
);
isEmpty = clearColor(imageData, backgroundColor);
if (!isEmpty) {
break;
}
}
}
this._tmpCtx.restore();
// copy the data from imageData to _cacheCanvas
const x = this._toCoordinateX(index);
const y = this._toCoordinateY(index);
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(imageData, x, y);
// Add the glyph and queue it to the bitmap (if the browser supports it)
const cacheValue = {
index,
isEmpty,
inBitmap: false
};
this._addGlyphToBitmap(cacheValue);
return cacheValue;
}
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
return;
}
// Add the glyph to the queue
this._glyphsWaitingOnBitmap.push(cacheValue);
// Check if bitmap generation timeout already exists
if (this._bitmapCommitTimeout !== null) {
return;
}
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
}
private _generateBitmap(): void {
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
this._glyphsWaitingOnBitmap = [];
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
// Set bitmap
this._bitmap = bitmap;
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
// the bitmap was requested
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
const value = glyphsMovingToBitmap[i];
// It doesn't matter if the value was already evicted, it will be
// released from memory after this block if so.
value.inBitmap = true;
}
});
this._bitmapCommitTimeout = null;
}
}
// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
// `new NoneCharAtlas`.
export class NoneCharAtlas extends BaseCharAtlas {
constructor(document: Document, config: ICharAtlasConfig) {
super();
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean {
return false;
}
}
/**
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
* transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent.
*/
function clearColor(imageData: ImageData, color: IColor): boolean {
let isEmpty = true;
const r = color.rgba >>> 24;
const g = color.rgba >>> 16 & 0xFF;
const b = color.rgba >>> 8 & 0xFF;
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (Math.abs(imageData.data[offset] - r) +
Math.abs(imageData.data[offset + 1] - g) +
Math.abs(imageData.data[offset + 2] - b) < 35) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
}
}
return isEmpty;
}
@@ -1,65 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { LRUMap } from './LRUMap';
describe('LRUMap', () => {
it('can be used to store and retrieve values', () => {
const map = new LRUMap(10);
map.set(1, 'valuea');
map.set(2, 'valueb');
map.set(3, 'valuec');
assert.strictEqual(map.get(1), 'valuea');
assert.strictEqual(map.get(2), 'valueb');
assert.strictEqual(map.get(3), 'valuec');
});
it('maintains a size from insertions', () => {
const map = new LRUMap(10);
assert.strictEqual(map.size, 0);
map.set(1, 'value');
assert.strictEqual(map.size, 1);
map.set(2, 'value');
assert.strictEqual(map.size, 2);
});
it('deletes the oldest entry when the capacity is exceeded', () => {
const map = new LRUMap(4);
map.set(1, 'value');
map.set(2, 'value');
map.set(3, 'value');
map.set(4, 'value');
map.set(5, 'value');
assert.isNull(map.get(1));
assert.isNotNull(map.get(2));
assert.isNotNull(map.get(3));
assert.isNotNull(map.get(4));
assert.isNotNull(map.get(5));
assert.strictEqual(map.size, 4);
});
it('prevents a recently accessed entry from getting deleted', () => {
const map = new LRUMap(2);
map.set(1, 'value');
map.set(2, 'value');
map.get(1);
// a would normally get deleted here, except that we called get()
map.set(3, 'value');
assert.isNotNull(map.get(1));
// b got deleted instead of a
assert.isNull(map.get(2));
assert.isNotNull(map.get(3));
});
it('supports mutation', () => {
const map = new LRUMap(10);
map.set(1, 'oldvalue');
map.set(1, 'newvalue');
// mutation doesn't change the size
assert.strictEqual(map.size, 1);
assert.strictEqual(map.get(1), 'newvalue');
});
});
@@ -1,136 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
interface ILinkedListNode<T> {
prev: ILinkedListNode<T> | null;
next: ILinkedListNode<T> | null;
key: number | null;
value: T | null;
}
export class LRUMap<T> {
private _map: { [key: number]: ILinkedListNode<T> } = {};
private _head: ILinkedListNode<T> | null = null;
private _tail: ILinkedListNode<T> | null = null;
private _nodePool: ILinkedListNode<T>[] = [];
public size: number = 0;
constructor(public capacity: number) { }
private _unlinkNode(node: ILinkedListNode<T>): void {
const prev = node.prev;
const next = node.next;
if (node === this._head) {
this._head = next;
}
if (node === this._tail) {
this._tail = prev;
}
if (prev !== null) {
prev.next = next;
}
if (next !== null) {
next.prev = prev;
}
}
private _appendNode(node: ILinkedListNode<T>): void {
const tail = this._tail;
if (tail !== null) {
tail.next = node;
}
node.prev = tail;
node.next = null;
this._tail = node;
if (this._head === null) {
this._head = node;
}
}
/**
* Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
* they're more likely to live next to each other in memory, which seems to improve performance.
*
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
* large maps.
*/
public prealloc(count: number): void {
const nodePool = this._nodePool;
for (let i = 0; i < count; i++) {
nodePool.push({
prev: null,
next: null,
key: null,
value: null
});
}
}
public get(key: number): T | null {
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
const node = this._map[key];
if (node !== undefined) {
this._unlinkNode(node);
this._appendNode(node);
return node.value;
}
return null;
}
/**
* Gets a value from a key without marking it as the most recently used item.
*/
public peekValue(key: number): T | null {
const node = this._map[key];
if (node !== undefined) {
return node.value;
}
return null;
}
public peek(): T | null {
const head = this._head;
return head === null ? null : head.value;
}
public set(key: number, value: T): void {
// This is unsafe: See note above.
let node = this._map[key];
if (node !== undefined) {
// already exists, we just need to mutate it and move it to the end of the list
node = this._map[key];
this._unlinkNode(node);
node.value = value;
} else if (this.size >= this.capacity) {
// we're out of space: recycle the head node, move it to the tail
node = this._head!;
this._unlinkNode(node);
delete this._map[node.key!];
node.key = key;
node.value = value;
this._map[key] = node;
} else {
// make a new element
const nodePool = this._nodePool;
if (nodePool.length > 0) {
// use a preallocated node if we can
node = nodePool.pop()!;
node.key = key;
node.value = value;
} else {
node = {
prev: null,
next: null,
key,
value
};
}
this._map[key] = node;
this.size++;
}
this._appendNode(node);
}
}
-29
View File
@@ -1,29 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'common/services/Services';
import { IPartialColorSet } from 'browser/Types';
export interface IGlyphIdentifier {
chars: string;
code: number;
bg: number;
fg: number;
bold: boolean;
dim: boolean;
italic: boolean;
}
export interface ICharAtlasConfig {
devicePixelRatio: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IPartialColorSet;
}
@@ -12,6 +12,16 @@ declare module 'xterm-addon-canvas' {
export class CanvasAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
/**
* An event that is fired when the a new page is added to the texture atlas.
*/
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
constructor();
/**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-fit",
"version": "0.6.0",
"version": "0.8.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+5 -5
View File
@@ -4,6 +4,7 @@
*/
import { Terminal, ITerminalAddon } from 'xterm';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
interface ITerminalDimensions {
/**
@@ -23,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;
}
@@ -58,8 +57,9 @@ export class FitAddon implements ITerminalAddon {
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
const dims: IRenderDimensions = core._renderService.dimensions;
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
return undefined;
}
@@ -81,8 +81,8 @@ export class FitAddon implements ITerminalAddon {
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
return geometry;
}
+11 -1
View File
@@ -13,10 +13,20 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha"
]
],
"paths": {
"browser/*": [
"../../../src/browser/*"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/browser"
}
]
}
+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.10.0",
"version": "0.13.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+144 -167
View File
@@ -3,8 +3,9 @@
* @license MIT
*/
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm';
import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable, disposeArray, MutableDisposable } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -29,6 +30,10 @@ export interface ISearchPosition {
startRow: number;
}
export interface ISearchAddonOptions {
highlightLimit: number;
}
export interface ISearchResult {
term: string;
col: number;
@@ -47,17 +52,22 @@ type LineCacheEntry = [
lineOffsets: number[]
];
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
}
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
export class SearchAddon implements ITerminalAddon {
export class SearchAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
private _resultDecorations: Map<number, IDecoration[]> | undefined;
private _searchResults: Map<string, ISearchResult> | undefined;
private _onDataDisposable: IDisposable | undefined;
private _onResizeDisposable: IDisposable | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _selectedDecoration: MutableDisposable<IHighlight> = this.register(new MutableDisposable());
private _highlightLimit: number;
private _lastSearchOptions: ISearchOptions | undefined;
private _highlightTimeout: number | undefined;
/**
@@ -70,15 +80,20 @@ export class SearchAddon implements ITerminalAddon {
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
private _resultIndex: number | undefined;
private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
constructor(options?: Partial<ISearchAddonOptions>) {
super();
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
}
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
this.register(this._terminal.onResize(() => this._updateMatches()));
this.register(toDisposable(() => this.clearDecorations()));
}
private _updateMatches(): void {
@@ -87,46 +102,29 @@ export class SearchAddon implements ITerminalAddon {
}
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => {
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
const term = this._cachedSearchTerm;
this._cachedSearchTerm = undefined;
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
}, 200);
}
}
public dispose(): void {
this.clearDecorations();
this._onDataDisposable?.dispose();
this._onResizeDisposable?.dispose();
}
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration?.dispose();
this._searchResults?.clear();
this._resultDecorations?.forEach(decorations => {
for (const d of decorations) {
d.dispose();
}
});
this._resultDecorations?.clear();
this._searchResults = undefined;
this._resultDecorations = undefined;
this._selectedDecoration.clear();
disposeArray(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
}
}
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.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
* @returns Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
@@ -134,11 +132,16 @@ export class SearchAddon implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
const found = this._findNextAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
@@ -153,32 +156,30 @@ export class SearchAddon implements ITerminalAddon {
// new search, clear out the old decorations
this.clearDecorations(true);
this._searchResults = new Map<string, ISearchResult>();
this._resultDecorations = new Map<number, IDecoration[]>();
const resultDecorations = this._resultDecorations;
const searchResultsWithHighlight: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
this._searchResults.set(`${result.row}-${result.col}`, result);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this._highlightLimit) {
break;
}
prevResult = result;
searchResultsWithHighlight.push(prevResult);
result = this._find(
term,
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
if (this._searchResults.size > 1000) {
this.clearDecorations();
this._resultIndex = undefined;
return;
}
for (const match of searchResultsWithHighlight) {
const decoration = this._createResultDecoration(match, searchOptions.decorations!);
if (decoration) {
this._highlightedLines.add(decoration.marker.line);
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
}
}
this._searchResults.forEach(result => {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
if (resultDecoration) {
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
decorationsForLine.push(resultDecoration);
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
}
});
}
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
@@ -223,26 +224,22 @@ export class SearchAddon implements ITerminalAddon {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
this._cachedSearchTerm = undefined;
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startCol = 0;
let startRow = 0;
let currentSelection: IBufferRange | undefined;
if (this._terminal.hasSelection()) {
const incremental = searchOptions ? searchOptions.incremental : false;
// Start from the selection end if there is a selection
// For incremental search, use existing row
currentSelection = this._terminal.getSelectionPosition()!;
startRow = incremental ? currentSelection.start.y : currentSelection.end.y;
startCol = incremental ? currentSelection.start.x : currentSelection.end.x;
if (prevSelectedPos) {
if (this._cachedSearchTerm === term) {
startCol = prevSelectedPos.end.x;
startRow = prevSelectedPos.end.y;
} else {
startCol = prevSelectedPos.start.x;
startRow = prevSelectedPos.start.y;
}
}
this._initLinesCache();
@@ -281,24 +278,12 @@ export class SearchAddon implements ITerminalAddon {
}
// If there is only one result, wrap back and return selection if it exists.
if (!result && currentSelection) {
searchPosition.startRow = currentSelection.start.y;
if (!result && prevSelectedPos) {
searchPosition.startRow = prevSelectedPos.start.y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined) {
this._resultIndex = 0;
} else {
this._resultIndex++;
if (this._resultIndex >= this._searchResults.size) {
this._resultIndex = 0;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -307,7 +292,7 @@ export class SearchAddon implements ITerminalAddon {
* doesn't exist, do nothing.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
* @returns Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
@@ -315,75 +300,74 @@ export class SearchAddon implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
const found = this._findPreviousAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
private _fireResults(searchOptions?: ISearchOptions): void {
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
} else {
this._onDidChangeResults.fire(undefined);
let resultIndex = -1;
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) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
}
this._cachedSearchTerm = term;
return found;
}
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
let result: ISearchResult | undefined;
if (!this._terminal || !term || term.length === 0) {
result = undefined;
this._terminal?.clearSelection();
this.clearDecorations();
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const isReverseSearch = true;
const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: IBufferRange | undefined;
if (this._terminal.hasSelection()) {
currentSelection = this._terminal.getSelectionPosition()!;
// Start from selection start if there is a selection
startRow = currentSelection.start.y;
startCol = currentSelection.start.x;
}
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
if (incremental) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
const isOldResultHighlighted = result && result.row === startRow && result.col === startCol;
if (!isOldResultHighlighted) {
// If selection was not able to be expanded to the right, then try reverse search
if (currentSelection) {
searchPosition.startRow = currentSelection.end.y;
searchPosition.startCol = currentSelection.end.x;
let result: ISearchResult | undefined;
if (prevSelectedPos) {
searchPosition.startRow = startRow = prevSelectedPos.start.y;
searchPosition.startCol = startCol = prevSelectedPos.start.x;
if (this._cachedSearchTerm !== term) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
if (!result) {
// If selection was not able to be expanded to the right, then try reverse search
searchPosition.startRow = startRow = prevSelectedPos.end.y;
searchPosition.startCol = startCol = prevSelectedPos.end.x;
}
result = this._findInLine(term, searchPosition, searchOptions, true);
}
} else {
}
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
@@ -399,8 +383,8 @@ export class SearchAddon implements ITerminalAddon {
}
}
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
@@ -409,22 +393,6 @@ export class SearchAddon implements ITerminalAddon {
}
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
this._resultIndex = this._searchResults.size - 1;
} else {
this._resultIndex--;
if (this._resultIndex === -1) {
this._resultIndex = this._searchResults.size - 1;
}
}
}
// If there is only one result, return true.
if (!result && currentSelection) return true;
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -461,7 +429,8 @@ export class SearchAddon 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
@@ -472,15 +441,16 @@ export class SearchAddon 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 position The position to start the search.
* @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.
* @return The search result if it was found.
* @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 {
const terminal = this._terminal!;
@@ -547,7 +517,8 @@ export class SearchAddon 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++;
@@ -627,7 +598,7 @@ export class SearchAddon implements ITerminalAddon {
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
@@ -662,11 +633,11 @@ export class SearchAddon implements ITerminalAddon {
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whether a result was selected.
* @returns Whether a result was selected.
*/
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;
@@ -675,7 +646,7 @@ export class SearchAddon implements ITerminalAddon {
if (options) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
if (marker) {
this._selectedDecoration = terminal.registerDecoration({
const decoration = terminal.registerDecoration({
marker,
x: result.col,
width: result.size,
@@ -685,8 +656,13 @@ export class SearchAddon implements ITerminalAddon {
color: options.activeMatchColorOverviewRuler
}
});
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
this._selectedDecoration?.onDispose(() => marker.dispose());
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
this._selectedDecoration.value = { decoration, match: result, dispose() { decoration.dispose(); } };
}
}
}
@@ -702,16 +678,13 @@ export class SearchAddon implements ITerminalAddon {
}
/**
* Applies styles to the decoration when it is rendered
* @param element the decoration's element
* @param backgroundColor the background color to apply
* @param borderColor the border color to apply
* Applies styles to the decoration when it is rendered.
* @param element The decoration's element.
* @param borderColor The border color to apply.
* @param isActiveResult Whether the element is part of the active search result.
* @returns
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
if (element.clientWidth <= 0) {
return;
}
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (borderColor) {
@@ -740,13 +713,17 @@ export class SearchAddon implements ITerminalAddon {
x: result.col,
width: result.size,
backgroundColor: options.matchBackground,
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: options.matchOverviewRuler,
position: 'center'
}
});
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
findResultDecoration?.onDispose(() => marker.dispose());
if (findResultDecoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
}
return findResultDecoration;
}
}
@@ -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';
@@ -178,7 +178,7 @@ describe('Search Tests', function (): void {
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
await writeSync(page, 'd abc aabc d');
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
@@ -201,15 +201,64 @@ describe('Search Tests', function (): void {
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 },
{ resultCount: 1000, resultIndex: 1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 3, resultIndex: 0 }
]);
});
});
describe('findPrevious', () => {
it('should not fire unless the decorations option is set', async () => {
@@ -233,6 +282,7 @@ describe('Search Tests', function (): void {
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await page.evaluate(`window.term.clearSelection()`);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
@@ -262,7 +312,7 @@ describe('Search Tests', function (): void {
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
await writeSync(page, 'd abc aabc d');
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 }
@@ -285,15 +335,64 @@ describe('Search Tests', function (): void {
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 3, resultIndex: 1 }
]);
});
});
});
+20 -4
View File
@@ -75,10 +75,28 @@ declare module 'xterm-addon-search' {
activeMatchColorOverviewRuler: string;
}
/**
* Options for the search addon.
*/
export interface ISearchAddonOptions {
/**
* Max number of matches highlighted when decorations are enabled.
* Defaults to 1000 highlighted matches
*/
highlightLimit: number
}
/**
* An xterm.js addon that provides search functionality.
*/
export class SearchAddon implements ITerminalAddon {
/**
* Creates a new search addon.
* @param options Options for the search addon.
*/
constructor(options?: Partial<ISearchAddonOptions>);
/**
* Activates the addon
* @param terminal The terminal the addon is being loaded in.
@@ -121,10 +139,8 @@ declare module 'xterm-addon-search' {
/**
* When decorations are enabled, fires when
* the search results change.
* @returns -1 for resultIndex for a resultCount of 0
* and @returns undefined when the threshold of 1k results
* is exceeded and decorations are disposed of.
* @returns -1 for resultIndex when the threshold of matches is exceeded.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-serialize",
"version": "0.8.0",
"version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -7,9 +7,10 @@ import jsdom = require('jsdom');
import { assert } from 'chai';
import { SerializeAddon } from './SerializeAddon';
import { Terminal } from 'browser/public/Terminal';
import { ColorManager } from 'browser/ColorManager';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService';
import { ThemeService } from 'browser/services/ThemeService';
function sgr(...seq: string[]): string {
return `\x1b[${seq.join(';')}m`;
@@ -44,14 +45,11 @@ class TestSelectionService {
}
describe('xterm-addon-serialize', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: jsdom.DOMWindow;
let serializeAddon: SerializeAddon;
let terminal: Terminal;
let selectionService: any;
before(() => {
serializeAddon = new SerializeAddon();
@@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
@@ -77,10 +74,8 @@ describe('xterm-addon-serialize', () => {
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
terminal.loadAddon(serializeAddon);
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
cm = new ColorManager(document, false);
(terminal as any)._core._colorManager = cm;
(terminal as any)._core._selectionService = selectionService;
(terminal as any)._core._themeService = new ThemeService((terminal as any)._core.optionsService);
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
});
describe('text', () => {
@@ -210,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));
@@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
&& cell1.isOverline() === cell2.isOverline()
&& cell1.isBlink() === cell2.isBlink()
&& cell1.isInvisible() === cell2.isInvisible()
&& cell1.isItalic() === cell2.isItalic()
@@ -129,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`;
@@ -264,6 +266,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
@@ -289,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;
/**
@@ -410,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;
}
@@ -534,7 +536,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
private _htmlContent = '';
private _colors: IColorSet;
private _ansiColors: Readonly<IColor[]>;
constructor(
buffer: IBuffer,
@@ -543,8 +545,13 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
) {
super(buffer);
// https://github.com/xtermjs/xterm.js/issues/3601
this._colors = (_terminal as any)._core._colorManager.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 {
@@ -600,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;
}
@@ -625,7 +632,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
if (cell.isBold()) { content.push('font-weight: bold;'); }
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
if (cell.isItalic()) { content.push('font-style: italic;'); }
@@ -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'));
});
@@ -220,6 +219,18 @@ describe('SerializeAddon', () => {
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with overline', async () => {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
sgr(OVERLINED) + line, // Overlined
sgr(UNDERLINED) + line, // Overlined, Underlined
sgr(NORMAL) + line // Normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
@@ -581,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';
@@ -601,6 +611,7 @@ const BLINK = '5';
const INVERSE = '7';
const INVISIBLE = '8';
const STRIKETHROUGH = '9';
const OVERLINED = '53';
const NO_BOLD = '22';
const NO_DIM = '22';
@@ -610,3 +621,4 @@ const NO_BLINK = '25';
const NO_INVERSE = '27';
const NO_INVISIBLE = '28';
const NO_STRIKETHROUGH = '29';
const NO_OVERLINED = '55';

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