Compare commits

...
Author SHA1 Message Date
Anthony Kim 2c4997f529 Add test for 200x100 pixel 2026-02-11 10:27:12 -08:00
Anthony Kim 16ea2ec0ef Add image demo test for kitty 2026-02-10 21:32:41 -08:00
Anthony Kim eb0f5383c3 question.. 2026-02-09 16:43:55 -08:00
Anthony Kim a4502a6d00 Impelement cursor movement after image placement 2026-02-09 14:30:35 -08:00
Anthony Kim c11a8f43f4 abort in-flight kitty chunks on delete 2026-02-09 13:34:27 -08:00
Anthony Kim d7ba56cd28 default format png -> rgba perspec 2026-02-09 13:19:41 -08:00
Anthony Kim be44833f12 send OK response for a=t, a=T transmissions when image id present 2026-02-09 13:04:34 -08:00
Anthony Kim fcb3cc1a21 add delelteImage, map kitty ID to storage IDs 2026-02-09 11:31:20 -08:00
Anthony Kim 376268157c add f=24 rgb, f=32 rgba format tests 2026-02-09 09:35:00 -08:00
Anthony Kim 5ceb02b9c6 Release decoder after command ahandler consumes bytes 2026-02-08 00:16:01 -08:00
Anthony Kim d320898231 offload raw image data to Blob for off-heap storage 2026-02-07 23:30:06 -08:00
Anthony Kim cf3939af4a Remove redundant _decodedImages bitmap cache 2026-02-07 23:11:19 -08:00
Anthony Kim 596ab845d0 Make copy of bytes with Uint8Array since they can be reused by decoder 2026-02-05 22:04:04 -08:00
Anthony Kim a59d527d1a Mirror const enum since esbuild cant inline const enum 2026-02-05 21:38:33 -08:00
Anthony Kim 08f85cf826 Use wasm-parts v0.3.0., remove manual sharding/leftover, update iip! 2026-02-05 00:20:39 -08:00
Anthony Kim e95ec8d4bc feed to decoder directly, try to clean things before getting upgraded base64 decoder 2026-02-03 17:26:59 -08:00
Anthony Kim d15b426bd4 Readme edit for kittySupport 2026-02-03 13:53:47 -08:00
Anthony Kim c907edd75f Separate out test, add failing//skip test for cursor placements 2026-02-03 13:53:33 -08:00
Anthony Kim 1029e19b57 Pre-calculate + fix bug to make sure we dont reset per chunk 2026-02-02 21:27:47 -08:00
Anthony Kim d44900faa2 Parse control data early as suggested 2026-02-02 20:47:29 -08:00
Anthony Kim 5f724c5924 Reduce MAX_CONTROL_DATA_SIZE + EINVAL for i and I 2026-02-02 20:17:21 -08:00
Anthony Kim ae58176bcb Lint 2026-02-01 13:22:02 -08:00
Anthony Kim 429b2882f1 Terminal eixst 2026-02-01 13:18:46 -08:00
Anthony Kim ffac8c03ab Merge branch 'master' into anthonykim1/scaffoldKittyAddon 2026-02-01 13:11:40 -08:00
Anthony Kim a7c4a160a8 flip inControlData thing like in C 2026-02-01 13:08:41 -08:00
Anthony Kim c9dc994090 TODO for file system stufff 2026-02-01 12:57:45 -08:00
Anthony Kim e4710d3286 Use sharding, decode with streaming 2026-02-01 12:37:09 -08:00
Daniel Imms 8f9045ed88 Merge pull request #5664 from Tyriar/renderer_tests_wsl
Get playwright tests reliably passing in wsl
2026-02-01 08:54:30 -08:00
Daniel Imms b8e5ec7304 Fix lint 2026-02-01 08:41:01 -08:00
Daniel Imms 6a3d2480e0 Get playwright tests reliably passing in wsl 2026-02-01 08:35:49 -08:00
Daniel Imms 735afde77f Merge pull request #5663 from Tyriar/image_demo
Move remaining addon test buttons into their own windows
2026-02-01 08:34:29 -08:00
Daniel Imms 53efc95170 Move remaining addon test buttons into their own windows 2026-02-01 08:26:20 -08:00
Daniel Imms 3512f140de Merge pull request #5658 from Tyriar/outline
Fix demo button outline
2026-02-01 08:23:59 -08:00
Daniel Imms dbe35428cb Merge pull request #5662 from Tyriar/any_fixes
Remove some easy any usages
2026-02-01 08:06:33 -08:00
Daniel Imms 297ac838a3 Fix build 2026-02-01 07:58:36 -08:00
Daniel Imms 5316cb0812 Remove some easy any usages 2026-02-01 07:48:40 -08:00
Daniel Imms c124550971 Merge pull request #5661 from Tyriar/iip
Fix copying array data in IIPHandler
2026-02-01 07:23:39 -08:00
Daniel Imms 6525a7e68e Fix copying array data in IIPHandler
Fixes #5660
2026-02-01 07:15:25 -08:00
Daniel Imms b2133d93f0 Merge pull request #5659 from Tyriar/dead_code2
Remove unreachable code in SGR handling 100
2026-02-01 06:07:15 -08:00
Daniel Imms a5e4f90139 Remove unreachable code in SGR handling 100
The implementation seems to be wrong. This looks like a remnant left over from
term.js before bright SGR was implemented.
2026-02-01 05:59:00 -08:00
Daniel Imms a27b8feb8f Fix demo button outline 2026-01-31 22:56:48 -08:00
Daniel Imms e96748cc79 Merge pull request #5657 from Tyriar/inst2
Correct test copilot instructions
2026-01-31 22:34:43 -08:00
Daniel Imms 334c8170fe Merge pull request #5654 from Tyriar/5612
win32 input mode: Avoid scrolling to bottom on modifier only
2026-01-31 22:34:02 -08:00
Daniel Imms 2bb9662bea Merge pull request #5656 from Tyriar/cancel
Remove cancelEvent/cancel function
2026-01-31 22:26:50 -08:00
Daniel Imms 7cba3c9fa1 Correct test copilot instructions 2026-01-31 22:26:06 -08:00
Daniel Imms bab7db13db Remove cancelEvent/cancel function 2026-01-31 22:18:32 -08:00
Daniel Imms 1c79c5511c Merge pull request #5653 from Tyriar/input_handler_tests
Add remaining input handler tests, unskip modes that don't change
2026-01-31 22:09:48 -08:00
Daniel Imms 7b417f4585 win32 input mode: Avoid scrolling to bottom on modifier only
Fixes #5612
2026-01-31 22:05:33 -08:00
Daniel Imms dc487dfd6f Assert no mode changes and unskip 2026-01-31 22:01:35 -08:00
Daniel Imms 36baf70bea Unskip supported InputHandler tests 2026-01-31 21:05:36 -08:00
Daniel Imms 87e0b9ab10 Merge pull request #5652 from Tyriar/lint_test
Enable linting on test/ folder
2026-01-31 21:04:36 -08:00
Daniel Imms 5e532c13c6 Enable linting on test/ folder 2026-01-31 20:57:51 -08:00
Daniel Imms db131e0c22 Merge pull request #5651 from Tyriar/ih_tests
Implement more InputHandler tests
2026-01-31 18:28:42 -08:00
Daniel Imms 51b9a713da Implement more InputHandler tests
Part of #2117
2026-01-31 18:21:32 -08:00
Daniel Imms 972c069fa3 Merge pull request #5650 from Tyriar/slow_tests_3
Speed up TextDecoder.test.ts
2026-01-31 16:48:31 -08:00
Daniel Imms 2c0ba3c058 Speed up TextDecoder.test.ts
- Reduce calls to decode and clear
- Consolidate numberic checks
- Increase batch size
2026-01-31 16:42:11 -08:00
Daniel Imms e515647f99 Merge pull request #5649 from Tyriar/promise
Clear dangling timeout that keeps unit tests event loop alive
2026-01-31 16:27:25 -08:00
Daniel Imms 7a2794a552 Clear dangling timeout that keeps unit tests event loop alive
This fixes the issue where test-unit could run for up to 5 seconds after
the tests have already finished.
2026-01-31 16:21:03 -08:00
Daniel Imms da2267a10f Merge pull request #5648 from Tyriar/slow_tests_2
Speed up all remaining unit tests below threshold (40ms?)
2026-01-31 16:09:46 -08:00
Daniel Imms 34e68b1ad4 Speed up all remaining unit tests below threshold (40ms?) 2026-01-31 15:50:51 -08:00
Daniel Imms 1c42957461 Merge pull request #5646 from Tyriar/slow_tests
Speed up slow payload limit parser tests
2026-01-31 15:40:10 -08:00
Daniel Imms 77036d3f12 Merge pull request #5647 from Tyriar/fix_benchmark
Fix benchmark usage, add instructions
2026-01-31 15:37:17 -08:00
Daniel Imms ccbf612ddb Fix lint 2026-01-31 15:33:49 -08:00
Daniel Imms eed8423030 Clarify runtime cases 2026-01-31 15:30:16 -08:00
Daniel Imms 431d511c95 Fix benchmark usage, add instructions 2026-01-31 15:28:11 -08:00
Daniel Imms 0ca1bd48be Inline payload limit 2026-01-31 15:21:48 -08:00
Daniel Imms 7ceb15bb83 Reduce payload size to speed up unit tests
Reduces 11s -> 8s for full run
2026-01-31 15:17:10 -08:00
Daniel Imms 5ecdc7da44 Make payload limit configurable by tests 2026-01-31 15:08:05 -08:00
Daniel Imms 2e2270d01f Merge pull request #5645 from Tyriar/events
Optimize Emitter.fire for 0 and 1 listeners
2026-01-31 14:51:01 -08:00
Daniel Imms 042bb4bed9 Optimize Emitter.fire for 0 and 1 listeners
Before:
      Context "Emitter.fire()"
         Context "0 listeners"
            Case "#1" : 1 runs - average throughput: 97.77 MB/s
         Context "1 listener"
            Case "#1" : 1 runs - average throughput: 49.30 MB/s
         Context "2 listeners"
            Case "#1" : 1 runs - average throughput: 35.37 MB/s
         Context "5 listeners"
            Case "#1" : 1 runs - average throughput: 15.32 MB/s

After:
      Context "Emitter.fire()"
         Context "0 listeners"
            Case "#1" : 1 runs - average throughput: 376.94 MB/s
         Context "1 listener"
            Case "#1" : 1 runs - average throughput: 84.29 MB/s
         Context "2 listeners"
            Case "#1" : 1 runs - average throughput: 32.36 MB/s
         Context "5 listeners"
            Case "#1" : 1 runs - average throughput: 15.93 MB/s
2026-01-31 14:42:23 -08:00
Daniel Imms 7e3c41ba49 Merge pull request #5643 from Tyriar/cacheWorkCell
Cache work cell in hot loops
2026-01-31 10:33:25 -08:00
Daniel Imms 21e4301e38 Merge pull request #5642 from Tyriar/startsWith
Prefer startsWith over indexOf === 0
2026-01-31 10:27:15 -08:00
Daniel Imms 3ef43aedb1 Cache work cell in hot loops 2026-01-31 10:26:15 -08:00
Daniel Imms 9eaf5b45dc Merge pull request #5641 from Tyriar/padStart
Use native padStart over custom impl
2026-01-31 10:25:24 -08:00
Daniel Imms 39037ee806 Prefer startsWith over indexOf === 0
Faster and make code clearer
2026-01-31 10:20:44 -08:00
Daniel Imms e542affaca Use native padStart over custom impl 2026-01-31 10:17:52 -08:00
Daniel Imms 038883c41b Merge pull request #5640 from Tyriar/strict_demo
Make demo use strict TS compiler mode
2026-01-31 09:32:36 -08:00
Daniel Imms 6c3d4e4ea4 Fix lint 2026-01-31 09:26:05 -08:00
Daniel Imms 659cfddd8c Merge branch 'master' into strict_demo 2026-01-31 09:24:47 -08:00
Daniel Imms c375696ee6 Make demo use strict TS compiler mode 2026-01-31 09:22:45 -08:00
Daniel Imms bc3c376e83 Merge pull request #5628 from Tyriar/5626
Support color scheme reporting
2026-01-31 09:18:33 -08:00
Daniel Imms 9fca3d8ecf Merge branch 'master' into 5626 2026-01-31 09:12:15 -08:00
Daniel Imms 8eed7b4163 Merge pull request #5639 from Tyriar/nullish
Add @typescript-eslint/prefer-nullish-coalescing eslint rule
2026-01-31 09:03:35 -08:00
Daniel Imms c29938ee83 Merge pull request #5634 from Tyriar/const_enum
Use const enum in EscapeSequences
2026-01-31 09:00:38 -08:00
Daniel Imms 0e679ea6ae Use nullish coalescing in KeyboardService 2026-01-31 08:56:53 -08:00
Daniel Imms d0e8141f7c Merge branch 'master' into nullish 2026-01-31 08:56:14 -08:00
Daniel Imms 1d322c9e45 Fix name to follow enum rules 2026-01-31 08:54:15 -08:00
Daniel Imms bad7e329df Add @typescript-eslint/prefer-nullish-coalescing eslint rule 2026-01-31 08:43:05 -08:00
Daniel Imms ae5e7e55c8 Merge pull request #5638 from Tyriar/tyriar/comma
Add @stylistic/comma-spacing eslint rule
2026-01-31 08:32:49 -08:00
Daniel Imms 5bb2e46508 Merge pull request #5630 from xtermjs/anthonykim1/updateNodePtyVersion
Update node-pty to ^1.2.0-beta.9
2026-01-31 08:31:09 -08:00
Daniel Imms 5938b8823d Add @stylistic/comma-spacing eslint rule 2026-01-31 07:38:16 -08:00
Daniel Imms 69d87c8a30 Merge pull request #5637 from Tyriar/remove_trace
Remove trace decorator and unused platform consts
2026-01-30 07:34:45 -08:00
Daniel Imms a247154650 Merge pull request #5636 from Tyriar/win32__kitty
Move kitty input mode into a class to avoid init cost
2026-01-30 07:29:42 -08:00
Daniel Imms b30d9c3533 Remove trace decorator and unused platform consts 2026-01-30 07:27:38 -08:00
Daniel Imms 60190c1c48 Merge branch 'master' into win32__kitty 2026-01-30 07:23:07 -08:00
Daniel Imms 99d69d2236 Merge pull request #5635 from Tyriar/win32
Move win32 input mode into a class to avoid init cost
2026-01-30 07:23:01 -08:00
Daniel Imms de01d4608c Move kitty input mode into a class to avoid init cost 2026-01-30 07:09:48 -08:00
Daniel Imms d35481c3f9 Move win32 input mode into a class to avoid init cost 2026-01-30 07:07:55 -08:00
Daniel Imms e2d84928b7 Merge pull request #5633 from Tyriar/dead_code
Remove unused TypedArrayUtils.ts
2026-01-30 07:00:49 -08:00
Daniel Imms 9ffd16aa9b Use const enum in EscapeSequences
They'll be inlined and only the used ones will show in bundle
2026-01-30 06:55:50 -08:00
Daniel Imms 77fa03d11c Remove unused TypedArrayUtils.ts 2026-01-30 06:53:02 -08:00
Daniel Imms 01450f044a Merge pull request #5632 from Tyriar/5631
Remove vs dependency in common
2026-01-30 06:39:28 -08:00
Daniel Imms b04d816d8a Undo changes to headless/package.json 2026-01-30 06:32:49 -08:00
Daniel Imms f80dbf3d03 Fix lint, split up namespace and interface 2026-01-30 06:21:15 -08:00
Daniel Imms b58bd452af Fix lint 2026-01-30 06:07:30 -08:00
Daniel Imms e8d99d50a6 Remove vs dependency in common
Fixes #5631
2026-01-30 06:03:16 -08:00
Anthony Kim bae314f67c Try using the wasm base64 decoder 2026-01-29 01:12:09 -08:00
Anthony Kim cb8159c4f2 Start integrating KittyGraphics stuff into imageAddon as suggested 2026-01-29 00:53:10 -08:00
Anthony Kim b87cba4c2a Update node-pty to ^1.2.0-beta.9 2026-01-28 14:04:46 -08:00
Anthony Kim 492d950b64 Add TODOs from jerch's comments 2026-01-28 09:17:44 -08:00
Anthony Kim a1b69e5ac8 TODO on wasm stuff 2026-01-27 23:26:38 -08:00
Anthony Kim d77711936f comments on file system 2026-01-27 23:22:31 -08:00
Anthony Kim 5a79b08661 stale comments 2026-01-27 23:12:13 -08:00
Anthony Kim 3e8fc48b33 Small details 2026-01-27 22:20:21 -08:00
Anthony Kim 8c9afdb364 Outsource a bunch to KittyApcHandler. TODO: go over this 2026-01-27 22:13:47 -08:00
Anthony Kim f89524e577 Create KittyApchandler.ts 2026-01-27 21:25:31 -08:00
Anthony Kim a1c1702508 Assemble and use dataArray for ImageData, make hot-loop faster 2026-01-27 20:25:10 -08:00
Anthony Kim 15c71e28c8 Replace more magic numbers with readable symbols 2026-01-27 19:51:16 -08:00
Anthony Kim fd641370ec Dont use magic number, use symbol 2026-01-27 19:22:59 -08:00
Anthony Kim 259cc67f12 Handle actions, compression first, then parseInt before Switch 2026-01-27 19:20:11 -08:00
Anthony Kim b0b0868e90 Conste num for keys, describe in jsdoc 2026-01-27 19:14:06 -08:00
Anthony Kim d58e8597cd Use Terminal.dimensions intead 2026-01-27 19:04:17 -08:00
Anthony Kim 17d439515f switch from triggerDataEvnet to this._terminal.input 2026-01-27 18:59:48 -08:00
Anthony Kim ae171acb79 Dont forget about action o for compression 2026-01-27 18:44:40 -08:00
Anthony Kim e2dabc77fd Prefer ?? and move disposable location 2026-01-27 17:58:10 -08:00
Anthony Kim 40342ecd1e remove send-png 2026-01-27 17:26:06 -08:00
Anthony Kim 1442f84e4e Fix chunking for large data, allow pixels 2026-01-27 17:21:38 -08:00
Daniel Imms 6f87ae77a7 Support color scheme reporting
CSI ? 996 n
CSI ? 2031 h
CSI ? 2031 l

Fixes #5626
2026-01-27 10:19:26 -08:00
Daniel Imms 5f25e05cd2 Merge pull request #5625 from Tyriar/kitty_shift_disambiguate
Don't disambiguous kitty keyboard on shift only
2026-01-26 14:51:17 -08:00
Daniel Imms 2af9d10171 Don't disambiguous kitty keyboard on shift only
Part of microsoft/vscode#286809
2026-01-26 14:44:34 -08:00
Daniel Imms f471c0add8 Merge pull request #5624 from xtermjs/Tyriar-patch-1
Don't set windows input mode on demo
2026-01-26 13:19:09 -08:00
Daniel Imms 62dc0d9c51 Don't set windows input mode on demo 2026-01-26 13:12:37 -08:00
Anthony Kim 5e95ef652d more comments 2026-01-23 11:11:02 -08:00
Daniel Imms 14525b334f Merge pull request #5618 from xtermjs/anthonykim1/lastState1
Add STATE_LENGTH to equal last state + 1
2026-01-23 04:10:06 -08:00
Anthony Kim bc1d87dade comments 2026-01-22 23:38:10 -08:00
Anthony Kim 849110d300 add query to say OK responses 2026-01-22 18:40:37 -08:00
Anthony Kim 8b6f8f3172 Rename 2026-01-22 16:16:30 -08:00
Anthony Kim 27825f9ed8 param in kittyGraphicsAddon.ts , ci.yml 2026-01-22 15:59:46 -08:00
Anthony Kim a507afb388 Add rendering, tweak send-png 2026-01-22 15:41:48 -08:00
Anthony Kim ec5b421dfb Renderer + og formatting for ci.yml 2026-01-22 15:40:59 -08:00
Anthony Kim 8f0e711a79 ci.yml 2026-01-22 13:09:44 -08:00
Anthony Kim fc0dc3fb07 Dont mess with og formatting for ci.yml 2026-01-22 13:06:42 -08:00
Anthony Kim bea1628015 Edit ci.yml ? 2026-01-22 12:02:46 -08:00
Anthony Kim 447746314e npm install to update pckage-lock.json 2026-01-22 11:58:51 -08:00
Anthony Kim 6a02d7c9f5 Add some playwright tests that will first fail 2026-01-22 11:56:48 -08:00
Anthony Kim 1ace108422 better comments 2026-01-22 10:54:41 -08:00
Anthony Kim 2c1441a52d Add STATE_LENGTH to equal last state + 1 2026-01-22 10:49:21 -08:00
Anthony Kim 304943658d Support Kitty graphics protocol 2026-01-22 10:44:38 -08:00
Daniel Imms 8e67c0093c Merge pull request #5617 from xtermjs/anthonykim1/kittyGraphicsProtocol
Add register apc handler
2026-01-22 04:35:38 -08:00
Anthony Kim ee3b3626e8 Again, format 2026-01-21 16:27:19 -08:00
Anthony Kim b286f21f64 Add APC_STRING since DCS_PASSTHROUGH isnt the last in ParserState anymore 2026-01-21 15:50:51 -08:00
Anthony Kim 3d92a7e2ed Dont be silly with original formatting 2026-01-21 15:13:03 -08:00
Anthony Kim b01ba7db28 Add apc tests parser.test.ts 2026-01-21 14:17:01 -08:00
Anthony Kim 1c73006e39 Switch TransitionTable to Uint16Array 2026-01-21 13:41:29 -08:00
Anthony Kim 03b015d5d3 Add registerApcHandler to xterm-headless.d.ts 2026-01-21 11:45:21 -08:00
Anthony Kim 42d408a9a7 Change ordering in escapeSequnceParser.ts 2026-01-21 11:29:04 -08:00
Anthony Kim 5ecd980a83 Comment 2026-01-21 11:01:09 -08:00
Anthony Kim 63ebe2df5b Stop messing with existing formatting 2026-01-21 11:00:10 -08:00
Anthony Kim 1b2d361b59 Remove comment on Constant.ts for ApcState 2026-01-21 10:59:33 -08:00
Anthony Kim 8e29591b9f Add registerApcHandler to public api 2026-01-21 10:42:36 -08:00
Anthony Kim 48df6117ed Add ApcParser 2026-01-21 08:31:07 -08:00
Daniel Imms 14b6a71391 Merge pull request #5615 from Tyriar/5585
Fix stack overflow in webgl page merge logic
2026-01-19 04:07:10 -08:00
Daniel Imms c24eb60bd3 Fix stack overflow in merge logic
Fixes #5585
2026-01-18 15:45:19 -08:00
Daniel Imms 2521bab72f Merge pull request #5613 from Tyriar/tyriar/5611
Fix some control keys not sending correct sequences
2026-01-16 06:14:01 -08:00
Daniel Imms 0f1c9a47db Fix some control keys not sending correct sequences
Fixes #5611
2026-01-16 06:06:55 -08:00
Daniel Imms 777a46445a Merge pull request #5609 from xtermjs/merogge/show-cursor
add `showCursorImmediately` option
2026-01-13 08:47:50 -08:00
meganrogge a20ff565b0 add showCursorImmediately option 2026-01-13 10:36:35 -06:00
Daniel Imms d28046ebed Merge pull request #5607 from Tyriar/5606
Fix blur due to canvas dim mismatch in glyph renderer
2026-01-13 06:35:40 -08:00
Daniel Imms 4ddd982e42 Fix blur due to canvas dim mismatch in glyph renderer
Fixes #5606
2026-01-13 06:23:10 -08:00
Daniel Imms a3228bd1b8 Merge pull request #5604 from Tyriar/2357_2
Fix capital letters and ctrl+letter in win32-input-mode
2026-01-11 09:10:31 -08:00
Daniel Imms a1fca4feb8 Fix ctrl+letter when inside pwsh->WSL 2026-01-11 09:02:44 -08:00
Daniel Imms afc6f0b7eb Fix caps not working in win32InputMode
Part of #2357
2026-01-11 09:00:08 -08:00
Daniel Imms f4c77d37b4 Merge pull request #5603 from Tyriar/2357
Implement win32-input-mode
2026-01-10 06:04:17 -08:00
Daniel Imms 9d0beb7c38 Use 0b over 0x for flags 2026-01-10 05:57:13 -08:00
Daniel Imms f88650ec1c Use win32InputMode in demo on Windows 2026-01-10 05:54:48 -08:00
Daniel Imms 8b35c67dd3 useWin32 -> useWin32InputMode 2026-01-10 05:52:19 -08:00
Daniel Imms 53dc85eca2 Add copyright 2026-01-10 05:48:48 -08:00
Daniel Imms 0946386cc4 Make unit tests more compact 2026-01-10 05:48:30 -08:00
Daniel Imms ebe18b3da5 Add more test cases 2026-01-10 05:42:17 -08:00
Daniel Imms 6c2792a1d5 Fix tests 2026-01-10 05:33:08 -08:00
Daniel Imms ee47054678 Implement win32-input-mode
Fixes #2357
2026-01-10 05:27:03 -08:00
Daniel Imms 91c4761b82 Merge pull request #5600 from Tyriar/4198_kitty
Implement kitty keyboard protocol (CSI =|?|>|< u)
2026-01-10 04:46:38 -08:00
Daniel Imms c9fc6fca8e Lint 2026-01-10 04:39:26 -08:00
Daniel Imms 73cba6dda3 Clean up, add sgr extension to api 2026-01-10 04:38:06 -08:00
Daniel Imms 50b0f3d7ee Add vtExtensions.kittySgrBoldFaintControl 2026-01-10 04:31:36 -08:00
Daniel Imms 686c67a14b Make mandatory browser services readonly 2026-01-10 04:20:29 -08:00
Daniel Imms ec78500f86 Create IKeyboardService 2026-01-10 04:19:23 -08:00
Daniel Imms 98ea14824a Make lint happy 2026-01-09 13:14:30 -08:00
Daniel Imms 4c905dad33 Update API jsdoc 2026-01-09 13:02:18 -08:00
Daniel Imms e3fdcc7869 Fix API lint 2026-01-09 13:01:22 -08:00
Daniel Imms 0674663a5a Remove unused import 2026-01-09 12:56:45 -08:00
Daniel Imms e07832a420 Merge branch 'master' into 4198_kitty 2026-01-09 12:51:48 -08:00
Daniel Imms 934b5dc8fa Merge pull request #5602 from Tyriar/5601
Add SGR 221 and 222
2026-01-09 12:21:43 -08:00
Daniel Imms 7a544d8346 Add SGR 221 and 222
Fixes #5601
2026-01-09 12:13:40 -08:00
Daniel Imms c624a47780 Maintain different buffer flags, stack limit, tests 2026-01-09 11:59:50 -08:00
Daniel Imms 5a29d489c3 Separate out kitty keyboard handlers 2026-01-09 11:46:57 -08:00
Daniel Imms 4a67836a5b Remove support responses
Wrong place for them
2026-01-09 11:42:00 -08:00
Daniel Imms 3adf6be988 Fix some edge cases 2026-01-09 10:36:13 -08:00
Daniel Imms 566589f7c1 More comprehensive tests 2026-01-09 07:23:12 -08:00
Daniel Imms 8dbc91dc24 Add vtExtensions to demo 2026-01-09 07:15:45 -08:00
Daniel Imms 992da3cc4d Hook up vt extensions option 2026-01-09 07:07:10 -08:00
Daniel Imms 3fbaef06ef Align behavior closer with kitty 2026-01-09 06:53:12 -08:00
Daniel Imms 2265fa2ade Implement kitty keyboard protocol (CSI u)
Fixes #4198
2026-01-09 05:59:57 -08:00
Daniel Imms 756c353237 Merge pull request #5593 from kallsyms/improve-lines-cache-perf
improve SearchLineCache performance
2026-01-09 05:59:54 -08:00
Daniel Imms 49a8ac8fc0 Merge pull request #5599 from Tyriar/flush
Flush writes on resize
2026-01-09 05:37:21 -08:00
Daniel Imms bf7c95b6cc Flush writes on resize
Fixes #5597
2026-01-09 05:25:22 -08:00
Daniel Imms 8112fbf210 Merge pull request #5598 from Tyriar/5588
Fix exception on resize during write
2026-01-08 18:10:41 -08:00
Daniel Imms 16a13f8504 Fix exception on resize during write
Fixes #5588
2026-01-08 18:03:42 -08:00
Daniel Imms 9bd128b315 Merge pull request #5594 from Tyriar/onSelectionChangeClear
Fire onSelectionChange on clear
2026-01-08 17:44:16 -08:00
Daniel Imms 729a670043 Merge pull request #5596 from Tyriar/lint_changes
New npm script to lint only changes
2026-01-08 17:39:34 -08:00
Daniel Imms bd8454db55 Another poll case 2026-01-08 17:37:44 -08:00
Daniel Imms d6f6a6af9a Lint instructions 2026-01-08 17:32:50 -08:00
Daniel Imms d65f9312ea Add lint-changes npm scripts 2026-01-08 17:32:15 -08:00
Daniel Imms a437d12004 Poll to avoid a race 2026-01-08 17:25:26 -08:00
Daniel Imms 437490308c Merge pull request #5595 from Tyriar/download_fonts
Remove download fonts
2026-01-08 17:18:36 -08:00
Daniel Imms 3250c31202 Remove download fonts 2026-01-08 17:09:26 -08:00
Daniel Imms 55d12e03d1 Remove unneeded test 2026-01-08 17:04:35 -08:00
Daniel Imms 6efb319ece Add tests more thorough tests for onSelectionChange 2026-01-08 17:01:56 -08:00
Daniel Imms b69afe5765 Merge remote-tracking branch 'upstream/master' into onSelectionChangeClear 2026-01-08 16:51:54 -08:00
Daniel Imms 613aa6ba23 Fire onSelectionChange when starting to drag 2026-01-08 16:51:45 -08:00
Nick Gregory a8c9128841 improve SearchLineCache performance 2026-01-07 16:04:49 -08:00
Daniel Imms 94a264679e Merge pull request #5591 from Tyriar/5586
Ensure mouse listeners are removed on dispose
2026-01-05 12:38:41 -08:00
Daniel Imms cb35d1b487 Ensure mouse listeners are removed on dispose
Also use ? over ! just in case

Fixes #5586
2026-01-05 12:31:54 -08:00
Daniel Imms c97abced76 Merge pull request #5590 from Tyriar/5587
Add undefined checks in erase calls
2026-01-05 11:17:40 -08:00
Daniel Imms 9db31db886 Add undefined checks in erase calls
This is likely a race condition during a resize which is hard to say
what the right behavior is. Instead of hard failing by throwing an
error, silently ignore if this happens.

Fixes #5587
2026-01-05 11:10:56 -08:00
Daniel Imms ccf9727b85 Merge pull request #5583 from Tyriar/root
Clean up root, remove auto approve settings
2026-01-04 06:44:57 -08:00
Daniel Imms 1dfdde9e27 Clean up root, remove auto approve settings
These are automatically auto approved in upcoming vscode
2026-01-04 06:38:53 -08:00
Daniel Imms 2712e88d0a Merge pull request #5582 from Tyriar/readme2
Update readme 2
2026-01-04 06:28:32 -08:00
Daniel Imms b5da8a28f6 Update readme 2 2026-01-04 06:27:22 -08:00
Daniel Imms 66c4f8b116 Merge pull request #5581 from Tyriar/readme
Update readme
2026-01-04 06:20:48 -08:00
Daniel Imms 9148c80d1a Update readme
- Added x2 scale logo
- Revised wording
- Added new addons
2026-01-04 06:18:50 -08:00
Daniel Imms e1bfc7dd1f Merge pull request #5178 from jerch/webfonts-addon
Add webfonts addon
2026-01-04 05:43:01 -08:00
Daniel Imms c6c4a2ffbc Delete unwanted eslintrc 2026-01-03 15:39:24 -08:00
Daniel Imms 2cf2929869 Publish webfonts 2026-01-03 15:36:36 -08:00
Daniel Imms b0145998c5 Fix lint 2026-01-03 15:36:31 -08:00
Daniel Imms 0e57643f16 Move fonts into folder 2026-01-03 15:33:13 -08:00
Daniel Imms a35223a819 Add web fonts window to demo 2026-01-03 15:31:24 -08:00
Daniel Imms 5e95dc1f10 Sync package lock 2026-01-03 15:24:11 -08:00
Daniel Imms b4e9a7f9ea Merge remote-tracking branch 'upstream/master' into pr/jerch/5178 2026-01-03 15:21:50 -08:00
Daniel Imms 73e3357526 Merge pull request #5579 from Tyriar/proposed
Make everything except unicode APIs stable
2026-01-03 15:04:25 -08:00
Daniel Imms 483c7b4a81 Merge pull request #5580 from Tyriar/5572
Update demo dims on font family change
2026-01-03 14:58:57 -08:00
Daniel Imms f4453b3e18 Merge pull request #5566 from xtermjs/dependabot/npm_and_yarn/qs-6.14.1
Bump qs from 6.14.0 to 6.14.1
2026-01-03 14:58:39 -08:00
Daniel Imms 7389a1097d Update tests to use unicode for proposed check 2026-01-03 14:57:55 -08:00
Daniel Imms 0c7b1cb448 Update demo dims on font family change
Fixes #5572
2026-01-03 14:52:32 -08:00
Daniel Imms 7a3b5339e7 Merge pull request #5578 from Tyriar/5576
Lint demo code
2026-01-03 14:41:48 -08:00
Daniel Imms 8193ceae12 Make everything except unicode APIs stable 2026-01-03 14:40:07 -08:00
Daniel Imms 064e299bb0 Merge pull request #5577 from Tyriar/ext_recommendation
Recommend eslint extension in vscode
2026-01-03 14:38:45 -08:00
Daniel Imms 6211bf078c Lint demo code
Fixes #5576
2026-01-03 14:35:39 -08:00
Daniel Imms a639808196 Recommend eslint extension in vscode
Fixes #5575
2026-01-03 14:32:16 -08:00
Daniel Imms b522e255f0 Merge pull request #5571 from Tyriar/5341
Fix isNode detection when process module is used in webpack
2026-01-03 14:25:33 -08:00
Daniel Imms d7850af9a2 Fix lint 2026-01-03 14:19:36 -08:00
Daniel Imms 4182f2226f Merge pull request #5573 from Tyriar/5570_oom
Fix infinite recursion OOM crash in addon-ligatures
2026-01-03 14:15:11 -08:00
Daniel Imms e2f45b5f4e Fix for modern node that includes a navigator.userAgent 2026-01-03 14:10:44 -08:00
Daniel Imms 8712f8067d Fix infinite recursion OOM crash in addon-ligatures
1. processLookahead/BacktrackPosition created separate entry objects
   for each glyph at each position (O(N^M) with N glyphs, M positions).
   Now all glyphs at a position share one entry object.
2. cloneEntry, cloneTree, flattenEntry, and mergeTreeEntry could
   infinitely recurse or duplicate work on shared/cyclic references.
   Added visited/merged tracking maps to cache results.

Note that parsing CommitMono locks up the renderer for some time still
so there is still a performance issue here, this fixes the OOM though.
This was mostly defensive generated code since the codebase is
unfamiliar to me.

Fixes #5570
2026-01-03 14:06:57 -08:00
Daniel Imms 08c49301b7 Fix isNode detection when process module is used in webpack
Fixes #5341
2026-01-03 11:19:23 -08:00
dependabot[bot] 917a8ded92 Bump qs from 6.14.0 to 6.14.1
Bumps [qs](https://github.com/ljharb/qs) from 6.14.0 to 6.14.1.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-03 17:44:20 +00:00
Daniel Imms d06d278f0c Merge pull request #5569 from Tyriar/5568_font_ligatures
Merge font-ligatures package into addon-ligatures
2026-01-03 09:43:07 -08:00
Daniel Imms f84384c83a Fix lint 2026-01-03 09:31:45 -08:00
Daniel Imms ec03109c39 2 space indent 2026-01-03 09:25:39 -08:00
Daniel Imms 21b8f77afb Move unit tests to right folder, push font fixtures 2026-01-03 09:16:58 -08:00
Daniel Imms 363fc84fd2 Merge font-ligatures into addon-ligatures
Fixes #5568
2026-01-03 09:02:06 -08:00
Daniel Imms 8e4de23c11 Merge pull request #5565 from Tyriar/2797
Support ctrl+^ -> RS, move to physical key
2026-01-01 05:25:52 -08:00
Daniel Imms 1caf6dbe50 Fix tests 2026-01-01 05:19:56 -08:00
Daniel Imms 1c97e40a6a Support ctrl+^ -> RS, move to physical key
Fixes #2797
Part of #1862
2026-01-01 04:41:44 -08:00
Daniel Imms cdbafb231d Merge pull request #5564 from Tyriar/3364
Serialize showCursor
2025-12-31 17:14:02 -08:00
Daniel Imms 44a8c7efbc Fix test expectations 2025-12-31 17:08:05 -08:00
Daniel Imms 6277bb363c Serialize showCursor
Fixes #3364
2025-12-31 08:12:39 -08:00
Daniel Imms 40f2eefec7 Merge pull request #5563 from Tyriar/5489
Fix touch scrolling
2025-12-31 06:33:36 -08:00
Daniel Imms 519b1c5ef7 Fix touch scrolling
Fixes #5489
2025-12-31 06:27:45 -08:00
Daniel Imms 97e356fb83 Merge pull request #5562 from Tyriar/2117_3
Add tests for CSI Pm d, e, f, g, m
2025-12-31 05:24:41 -08:00
Daniel Imms 42872a84dd Add tests for CSI Pm d, e, f, g, m
Fixes #2117
2025-12-31 05:18:17 -08:00
Daniel Imms 4700e18833 Merge pull request #5561 from Tyriar/5558
Fix allowTransparency in lib and demo
2025-12-31 05:08:29 -08:00
Daniel Imms d143340b60 Fix allowTransparency in lib and demo
Fixes #5558
Fixes #5560
2025-12-31 05:00:33 -08:00
Daniel Imms a8f26bb758 Merge pull request #5559 from Tyriar/2117_2
Add tests for CSI `Ps K`, `? Ps K`, `Ps S`, `Ps T`, `Ps X`, `Ps Z`, `Ps ^`, `Ps \``
2025-12-30 07:06:37 -08:00
Daniel Imms da4243cf02 Don't skip new tests, implement CSI Ps ^ 2025-12-30 06:59:27 -08:00
Daniel Imms 54e66f3ac4 Add some more CSI tests 2025-12-30 06:48:28 -08:00
Daniel Imms 7d8ef60860 Merge pull request #5557 from Tyriar/playwright_update2
Update playwright test to latest
2025-12-30 06:47:24 -08:00
Daniel Imms 595c119404 Move skip into dom file 2025-12-30 06:05:46 -08:00
Daniel Imms 481431fbc1 Update playwright test to latest 2025-12-30 06:03:17 -08:00
Daniel Imms 5c2cd474ce Merge pull request #5556 from Tyriar/5555
Migrate evaluate calls to use terminal proxy
2025-12-30 05:43:14 -08:00
Daniel Imms 0b9a7cb856 Migrate MouseTracking and Terminal to playwright proxy
Fixes #5555
2025-12-30 05:37:09 -08:00
Daniel Imms a8a4455189 Migrate InputHandler.test.ts to playwright proxy
Part of #5555
2025-12-30 05:27:28 -08:00
Daniel Imms 11fbdb3db7 Merge pull request #5554 from Tyriar/2117
Add unit test for CSI Ps SP @ - SL
2025-12-30 05:10:44 -08:00
Daniel Imms 88ce0d40e7 Add unit test for CSI Ps SP @ - SL
Part of #2117
2025-12-30 05:04:20 -08:00
Daniel Imms 2785f11860 Merge pull request #5553 from Tyriar/1526
Save cursor and restore more modes
2025-12-30 04:55:51 -08:00
Daniel Imms 857c4529cd Save cursor and restore more modes
Fixes #1526
2025-12-30 04:49:54 -08:00
Daniel Imms d30c3e7204 Merge pull request #5552 from Tyriar/contrib
Rework contributing doc
2025-12-30 04:42:18 -08:00
Daniel Imms 982f5e15ff Merge pull request #5551 from Tyriar/702
Add dimensions API
2025-12-30 04:37:31 -08:00
Daniel Imms ec3e4f2af1 Rework contributing doc
Fixes #5550
2025-12-30 04:33:57 -08:00
Daniel Imms 13914ee01e Remove private use in ImageRenderer 2025-12-30 04:09:56 -08:00
Daniel Imms e27cc2c089 Remove private API access from tests/demo 2025-12-30 04:06:26 -08:00
Daniel Imms 214289aaab Add dimensions API
Fixes #702
2025-12-30 03:58:26 -08:00
Daniel Imms a5af434e3e Merge pull request #5549 from Tyriar/5548
Use OffscreenCanvas over DOM for WidthCache
2025-12-30 03:38:15 -08:00
Daniel Imms a513120348 Get dom/width cache tests passing in node 2025-12-30 03:32:34 -08:00
Daniel Imms ef978b370b Use OffscreenCanvas over DOM for WidthCache
Fixes #5548
2025-12-30 02:57:09 -08:00
Daniel Imms 00ad3eac40 Merge pull request #5547 from Tyriar/3433
Serialize scroll region
2025-12-29 07:59:49 -08:00
Daniel Imms 29d3eb2e52 Serialize scroll region
fixes #3433
2025-12-29 07:53:43 -08:00
Daniel Imms 8b594140e9 Merge pull request #5546 from Tyriar/progress
Add progress indicator custom glyphs (EE00-EE0B)
2025-12-29 07:37:53 -08:00
Daniel Imms 7cf48c1eb5 Add progress indicator custom glyphs (EE00-EE0B)
Fixes #5478
2025-12-29 07:31:52 -08:00
Daniel Imms 8e6b6e7464 Merge pull request #5545 from Tyriar/nf
Add nerd fonts test button
2025-12-29 06:39:13 -08:00
Daniel Imms 1b0554714b Add nerd fonts test button
Fixes #5544
2025-12-29 06:33:27 -08:00
Daniel Imms abdfa2628c Merge pull request #5543 from Tyriar/underline_inspect
Report underline attributes in demo cell inspector
2025-12-29 06:23:38 -08:00
Daniel Imms 5b4d794f7a Report underline attributes in demo cell inspector 2025-12-29 06:17:36 -08:00
Daniel Imms 9eb1da79e3 Merge pull request #5542 from Tyriar/4064
Make curly underline more vivid
2025-12-29 06:14:01 -08:00
Daniel Imms d4ba4c109b Make curly underline more vivid
Fixes #4064
2025-12-29 06:04:29 -08:00
Daniel Imms 1ad23d6081 Merge pull request #5539 from Tyriar/ris
Add RIS metadata comment
2025-12-29 05:06:07 -08:00
Daniel Imms 73c01cae89 Merge pull request #5541 from Tyriar/4114
Fix fit using correct dims on demo
2025-12-29 05:03:58 -08:00
Daniel Imms 5174a7310f Add RIS metadata comment
Fixes #5043
2025-12-29 05:00:13 -08:00
Daniel Imms 1d793c9054 Fix fit using correct dims on demo
This doesn't add the debounce suggested in the issue, but it does
comment about it.

Fixes #4114
2025-12-29 04:58:12 -08:00
Daniel Imms 1b7c61a6fc Merge pull request #5540 from Tyriar/4855
Fix decorations with height > 1
2025-12-29 04:20:51 -08:00
Daniel Imms 9780b44115 Fix decorations with height > 1
Fixes #4855
2025-12-29 04:14:56 -08:00
Daniel Imms 8d532a35c4 Merge pull request #5538 from Tyriar/4731_mobile_warn
Update viewport dims when dpr changes
2025-12-28 16:36:30 -08:00
Daniel Imms d6df8d7902 Update viewport dims when dpr changes
Fixes #4731
2025-12-28 16:30:44 -08:00
Daniel Imms 4b9566cef0 Merge pull request #5535 from Tyriar/5408_2
Fix firefox selection when in shadow dom take 2
2025-12-28 16:05:49 -08:00
Daniel Imms b30bb1e3b0 Fix stack overflow 2025-12-28 15:59:29 -08:00
Loïc Mangeonjean d67e73a4da fix(fit-addon): call getComputedStyle on the right window element 2025-12-28 15:55:21 -08:00
Loïc Mangeonjean e7434a932d fix: fix computed style computation on firefox inside a shadow dom 2025-12-28 15:55:21 -08:00
Daniel Imms cdb0497d0d Merge pull request #5534 from Tyriar/5184_sync_reset
Force reset to use sync render
2025-12-28 15:22:13 -08:00
Daniel Imms 2fb42c554e Force reset to use sync render
Fixes #5184
2025-12-28 15:16:23 -08:00
Daniel Imms 3632496c94 Merge pull request #5533 from Tyriar/5532
Fix addon build issue on macOS
2025-12-28 15:08:12 -08:00
Daniel Imms a6407fb3c6 Fix addon build issue on macOS
Fixes #5532
2025-12-28 15:02:16 -08:00
Daniel Imms 65aa28464c Merge pull request #5530 from Tyriar/4339
Fix double annoucne of too much output
2025-12-28 14:57:09 -08:00
Daniel Imms 2baad39ccf Fix double annoucne of too much output
Fixes #4339
2025-12-28 14:51:12 -08:00
Daniel Imms c975956f2a Merge pull request #5529 from Tyriar/worktree1
Force a sync render after resize occurs
2025-12-28 14:44:44 -08:00
Daniel Imms 4619a755b1 Force a sync render after resize occurs
Fixes #4922
Related microsoft/vscode#230120
2025-12-28 14:38:42 -08:00
Daniel Imms bc795d36c0 Merge pull request #5528 from Tyriar/4528
Serialize rich underline styles
2025-12-28 14:26:35 -08:00
Daniel Imms b989c5a568 Fix lint 2025-12-28 14:20:55 -08:00
Daniel Imms d66214b25e Serialize rich underline styles 2025-12-28 14:16:53 -08:00
Daniel Imms 6157f523a8 Merge pull request #5523 from Tyriar/4628
Add idle timeout which pauses cursor blink
2025-12-28 14:04:49 -08:00
Daniel Imms 3ab93ea083 Merge pull request #5526 from Tyriar/lint
Remove eol-last eslint rule
2025-12-28 14:04:18 -08:00
Daniel Imms 7ef4c55ca9 Merge pull request #5525 from Tyriar/demo_hover
Implement cell inspector in demo
2025-12-28 13:57:35 -08:00
Daniel Imms d393154271 Remove eol-last eslint rule
This one's just troublesome when working with generated code. It
doesn't make a difference in the end anyway.
2025-12-28 13:56:22 -08:00
Daniel Imms 2c32ef5963 Fix lint 2025-12-28 13:53:03 -08:00
Daniel Imms a730533790 Implement cell inspector in demo
Fixes #5524
2025-12-28 13:50:37 -08:00
Daniel Imms 8a5ffc1bb1 Tidy comments 2025-12-28 13:42:22 -08:00
Daniel Imms a966f7d591 Explain idle in cursorBlink option 2025-12-28 13:41:21 -08:00
Daniel Imms 4985c9f3f9 Use shared constant for idle timeout 2025-12-28 13:37:25 -08:00
Daniel Imms 2e1651badd Pull dom renderer blink management into class 2025-12-28 13:35:57 -08:00
Daniel Imms 7d1f74fb8b Use const enums for constants 2025-12-28 13:33:51 -08:00
Daniel Imms f8c1f5a32f Add idle timeout which pauses cursor blink
Fixes #4628
2025-12-28 13:30:58 -08:00
Daniel Imms 79944f7524 Merge pull request #5521 from Tyriar/5314
Add quirks setting to control DECSET/DECRST 12
2025-12-28 08:07:47 -08:00
Daniel Imms 52b6710b19 Hide quirks from demo for now 2025-12-28 08:01:56 -08:00
Daniel Imms 2a283a307c Fix lint 2025-12-28 07:56:31 -08:00
Daniel Imms ab4524873b Add test for DECSET quirks 2025-12-28 07:49:12 -08:00
Daniel Imms eb5f0d709a Merge pull request #5522 from Tyriar/5295
Note in API that reflowCursorLine will not move the cursor
2025-12-28 07:43:50 -08:00
Daniel Imms 8a8f262616 Note in API that reflowCursorLine will not move the cursor
Fixes #5295
2025-12-28 07:37:50 -08:00
Daniel Imms ca5a681363 Add quirks setting to control DECSET/DECRST 12
Fixes #5314
2025-12-28 07:34:55 -08:00
Daniel Imms fb10cd62c9 Merge pull request #5520 from Tyriar/4961
Ignore soft hyphen
2025-12-27 14:57:12 -08:00
Daniel Imms 28b62a5e0b Ignore soft hyphen
Fixes #4961
2025-12-27 14:51:07 -08:00
Daniel Imms bedf7e8326 Merge pull request #5519 from Tyriar/5444
Add onBeforeSearch and onAfterSearch APIs
2025-12-27 14:38:30 -08:00
Daniel Imms f50028abce Add onBeforeSearch and onAfterSearch APIs
Fixes #5444
2025-12-27 14:32:39 -08:00
Daniel Imms 7a4c9cadff Merge pull request #5518 from Tyriar/style
Demo: Improve style, remove headers from windows
2025-12-27 09:03:02 -08:00
Daniel Imms 01c1c477c3 Demo: Improve style, remove headers from windows 2025-12-27 08:57:26 -08:00
Daniel Imms 33daadb4ea Merge pull request #5516 from Tyriar/activeMatchBg
Ensure activeMatchBackground decoration is on top
2025-12-27 08:04:50 -08:00
Daniel Imms ef6013aac9 Merge pull request #5517 from Tyriar/demo_addons
Demo: Add addon tabs, shuffle order
2025-12-27 08:04:31 -08:00
Daniel Imms 82f24ad5a5 Demo: Add addon tabs, shuffle order 2025-12-27 06:47:17 -08:00
Daniel Imms 97c435862b Merge pull request #5439 from nya3jp/keydup
Fix key duplication on Android with physical keyboards
2025-12-27 06:38:28 -08:00
Daniel Imms 6488e839a1 Merge branch 'master' into keydup 2025-12-27 06:32:16 -08:00
Daniel Imms 5d3564bb38 Ensure activeMatchBackground decoration is on top
Fixes #5369
2025-12-27 06:30:02 -08:00
Daniel Imms 50985d30f9 Merge pull request #5515 from Tyriar/5457_ctrl+slash
Add ctrl+/ -> ^_ keybinding
2025-12-27 06:05:48 -08:00
Daniel Imms 733e48ec73 Add ctrl+/ -> ^_ keybinding
Fixes #5457
2025-12-27 05:57:43 -08:00
Daniel Imms 057fefd39e Merge pull request #5514 from Tyriar/demo6
Ensure types are checked in demo, fix errors, emit to out-demo
2025-12-27 05:40:41 -08:00
Daniel Imms 4d48f8624b Ensure types are checked in demo, fix errors, emit to out-demo
Part of #5509
2025-12-27 05:31:21 -08:00
Daniel Imms 995b491a7a Merge pull request #5513 from Tyriar/esbuild_warn
Set esbuild log level to warning
2025-12-27 05:23:44 -08:00
Daniel Imms 4fe118a8ff Set esbuild log level to warning
This suppresses a safe to ignore debug log in server task
2025-12-27 05:18:00 -08:00
Daniel Imms 45375c94e2 Merge pull request #5512 from Tyriar/demo_server
Move server.js to TypeScript
2025-12-27 05:12:14 -08:00
Daniel Imms f83760d027 Add esbuild server build task 2025-12-27 04:57:55 -08:00
Daniel Imms 1a1bdefc83 Move server to TS
Fixes #5509
2025-12-27 04:36:18 -08:00
Daniel Imms e28db9e8a9 Merge pull request #5511 from Tyriar/settings
Simplify vscode settings
2025-12-27 04:09:31 -08:00
Daniel Imms be5f510bfb Merge pull request #5510 from Tyriar/dev
Make default build task use a single terminal via concurrently
2025-12-27 04:07:16 -08:00
Daniel Imms ac6b8ebb10 Simplify vscode settings 2025-12-27 04:03:24 -08:00
Daniel Imms 1ada355048 Make default build task use a single terminal via concurrently 2025-12-27 03:41:47 -08:00
Daniel Imms 4ce3e3106b Merge pull request #5508 from Tyriar/demo4
Fix css not being used in demo
2025-12-27 03:15:58 -08:00
Daniel Imms 942aff6af7 Merge pull request #5507 from Tyriar/ts5.9
Upgrade to typescript 5.9.3
2025-12-27 03:14:38 -08:00
Daniel Imms 635b898dc0 Fix css not being used in demo 2025-12-27 03:09:39 -08:00
Daniel Imms e01d1f2f17 Upgrade to typescript 5.9.3 2025-12-27 03:06:14 -08:00
Daniel Imms bd16eeed90 Merge pull request #5506 from Tyriar/shared_compile
Fix type error in SharedRendererTests.ts
2025-12-27 03:01:27 -08:00
Daniel Imms df8f9fc519 Merge pull request #5505 from Tyriar/canvasExceptions
Remove unused skip canvas exceptions from playwright
2025-12-27 02:59:27 -08:00
Daniel Imms 373bf513f1 Fix type error in SharedRendererTests.ts 2025-12-27 02:55:43 -08:00
Daniel Imms 25a5497329 Remove unused skip canvas exceptions from playwright 2025-12-27 02:53:28 -08:00
Daniel Imms 4406043e6b Merge pull request #5504 from Tyriar/onRender_headless
Implement onRender in xterm-headless
2025-12-26 11:33:01 -08:00
Daniel Imms 82f961f76f Move new onRender behavior into headless Terminal impl 2025-12-26 11:27:38 -08:00
Daniel Imms a26471b9f3 Implement onRender in xterm-headless
This effectively enabled implementing an efficient renderer on top
of xterm-headless. See #5392
2025-12-26 11:19:59 -08:00
Daniel Imms ba989a6e6c Merge pull request #5503 from Tyriar/copilot_approve
Add some npm scripts that copilot can approve
2025-12-26 11:16:36 -08:00
Daniel Imms d4f725c649 Merge pull request #5502 from Tyriar/xtversion
Support reporting XTVERSION
2025-12-26 11:13:53 -08:00
Daniel Imms 79f5198b2d Add some npm scripts that copilot can approve 2025-12-26 11:10:53 -08:00
Daniel Imms d8a93b757a Support reporting XTVERSION
Fixes #5342
2025-12-26 11:06:55 -08:00
Daniel Imms 9453634f56 Merge pull request #5501 from Tyriar/demo3
Make clear css file is for index.html
2025-12-26 10:59:43 -08:00
Daniel Imms 10e12cf743 Make clear css file is for index.html
Follow up on #5500
2025-12-26 10:54:07 -08:00
Daniel Imms 79621a4e3f Merge pull request #5500 from Tyriar/demo2
More demo refactors
2025-12-26 10:51:49 -08:00
Daniel Imms 546c5e081d Inline simplified style to test.html 2025-12-26 10:46:27 -08:00
Daniel Imms e6ad59b1b1 Disable grid layout in test.html 2025-12-26 10:38:39 -08:00
Daniel Imms 8eab98b78d Change banner bg color 2025-12-26 04:41:05 -08:00
Daniel Imms 0ed6fd0db6 Fix order of init 2025-12-26 04:40:22 -08:00
Daniel Imms e3f87ab8a1 More move handlers into addons and test window 2025-12-26 04:36:51 -08:00
Daniel Imms 8119bb0a4a Simplify window interaction 2025-12-26 04:31:06 -08:00
Daniel Imms dc0d7c81d9 Add terminal and addon refs to all windows 2025-12-26 04:24:07 -08:00
Daniel Imms fa87ecdc4e Move more handlers into testWindow.ts 2025-12-26 04:15:13 -08:00
Daniel Imms b82e8f345f Move more handlers out, addon types into shared file 2025-12-26 04:09:24 -08:00
Daniel Imms 02e9e56eef Move more handlers into testWindow.ts 2025-12-26 04:04:08 -08:00
Daniel Imms 4fffb96f25 Move addCjk into window, move window interactions out of createTerminal 2025-12-26 03:51:01 -08:00
Daniel Imms 6fe5a124ca Merge pull request #5498 from Tyriar/demo
Improve demo
2025-12-26 03:29:03 -08:00
Daniel Imms f62043de91 Merge pull request #5497 from xtermjs/Tyriar-patch-1
Box Elements -> Block Elements
2025-12-25 04:27:12 -08:00
Daniel Imms 55e5ec6037 Box Elements -> Block Elements 2025-12-25 04:21:47 -08:00
Daniel Imms 7b2a5cb50f Merge pull request #5496 from xtermjs/Tyriar-patch-1
Replace range separator with hyphen
2025-12-25 04:05:17 -08:00
Daniel Imms 8740dcda4d Replace range separator with hyphen 2025-12-25 03:59:54 -08:00
Daniel Imms c046f5b4b1 More refactors 2025-12-25 03:41:47 -08:00
Daniel Imms 6bde923f07 Register the windows 2025-12-24 17:09:20 -08:00
Daniel Imms 40b2bbef34 Improve demo by making full screen, add control bar 2025-12-24 16:43:35 -08:00
Daniel Imms c04806fc92 Merge pull request #5495 from Tyriar/tyriar/git_branch
Support custom glyphs for Git Branch Symbols (PUA U+F5D0-U+F60D)
2025-12-24 16:42:45 -08:00
Daniel Imms 8b1306f46d More polish 2025-12-24 16:13:11 -08:00
Daniel Imms 6612153c44 Add alignment tests 2025-12-24 16:10:36 -08:00
Daniel Imms 2be529ce85 Polish definitions 2025-12-24 16:05:42 -08:00
Daniel Imms 96aa97ce11 Add extended symbols 2025-12-24 15:51:00 -08:00
Daniel Imms 98f4c3128c Stub second batch of glyphs 2025-12-24 15:35:46 -08:00
Daniel Imms ce1ab6fdc4 Correct order or curved glyphs 2025-12-24 15:31:49 -08:00
Daniel Imms e87e0f5443 Add git branch symbol alignment tests 2025-12-24 15:27:30 -08:00
Daniel Imms 4a8d8bd247 Partially implement git branch symbols
Part of #5477
2025-12-24 15:03:28 -08:00
Daniel Imms edf100a24b Merge pull request #5494 from Tyriar/tyriar/scale
Add char-based custom glyph scaling
2025-12-24 13:23:23 -08:00
Daniel Imms ed2d8b8fdc Merge branch 'master' into tyriar/scale 2025-12-24 12:36:32 -08:00
Daniel Imms faeefb53bf Merge pull request #5481 from BattlefieldDuck/fix/incorrect-theme-reference
Fix incorrect ITheme reference in comments
2025-12-24 12:29:40 -08:00
Daniel Imms 32ff1f2115 Merge pull request #5492 from Tyriar/sinon
Remove need for sinon
2025-12-24 12:25:41 -08:00
Daniel Imms acb59daf33 Fix lint 2025-12-24 12:20:12 -08:00
Daniel Imms a3d6bf6710 Merge pull request #5493 from Tyriar/tyriar/node
Upgrade missed parts from node 18 -> 22
2025-12-24 12:10:26 -08:00
Daniel Imms 5e5c623d8b Create test ts project in ligatures addon 2025-12-24 12:09:28 -08:00
Daniel Imms 168520f727 Upgrade missed parts from node 18 -> 22 2025-12-24 11:39:10 -08:00
Daniel Imms 23f61911e2 Remove need for sinon 2025-12-24 11:35:20 -08:00
Daniel Imms 491fa5586a Merge pull request #5491 from Tyriar/jsdom
Upgrade jsdom
2025-12-24 11:33:33 -08:00
Daniel Imms 2f549d74ae Merge pull request #5490 from Tyriar/lint
Update eslint and nyc
2025-12-24 11:26:35 -08:00
Daniel Imms 9df2430433 Upgrade jsdom 2025-12-24 11:26:19 -08:00
Daniel Imms ce9f6275ed Fix lint-api 2025-12-24 11:19:06 -08:00
Daniel Imms 9ca04799fe Merge remote-tracking branch 'origin/master' into lint 2025-12-24 11:16:01 -08:00
Daniel Imms 83757d1bf0 Polish arrow strokes 2025-12-24 11:13:58 -08:00
Daniel Imms 60fa9122a2 Remove old yarn rule and regen package lock 2025-12-24 10:46:59 -08:00
Daniel Imms a8d40a1edf Update eslint and nyc 2025-12-24 10:44:46 -08:00
Daniel Imms 78cc07f88a Split arrow parts into hybrid scaling 2025-12-24 10:30:34 -08:00
Daniel Imms 72d7c69a29 Add char-based custom glyph scaling
Draw calls can now be scaled by the char bounding box instead of the
cell bounding box. This means these character won't be stretched. Both
can also be used at the same like as is the case with 1FBBC RIGHT OPEN
SQUARED DOT.

Fixes #5488
2025-12-24 10:19:11 -08:00
Daniel Imms 0e0b445784 Merge pull request #5487 from Tyriar/tyriar/5480
Move customGlyphs option into webgl addon
2025-12-24 08:46:27 -08:00
Daniel Imms 5d24866042 Move customGlyphs option into webgl addon
Fixes #5480
2025-12-24 08:35:06 -08:00
Daniel Imms cbea462737 Merge pull request #5486 from Tyriar/tyriar/powerline_extra
Implement remaining powerline extra symbols
2025-12-24 08:23:39 -08:00
Daniel Imms a3ae94af1d Correct range 2025-12-24 08:18:19 -08:00
Daniel Imms 0e35521810 Add powerline alignment tests 2025-12-24 08:15:50 -08:00
Daniel Imms 40590fedfa Merge branch 'master' into tyriar/powerline_extra 2025-12-24 08:07:37 -08:00
Daniel Imms 1551d94de4 Implement remaining powerline extra symbols
- Uses fontforge + helper bin/convert_svg_to_custom_glyph.js
- Some of these are very large. They'll be much smaller after gzip but
  if this becomes a problem we can compile the svgs into a binary format

Fixes #5479
2025-12-24 08:06:20 -08:00
Daniel Imms d605803866 Merge pull request #5485 from Tyriar/tyriar/braille
Support custom gylphs for braille patterns (U+2800-U+28FF)
2025-12-24 07:09:40 -08:00
Daniel Imms a797852dfd Support custom gylphs for braille patterns (U+2800-U+28FF)
Fixes #5476
2025-12-24 07:00:53 -08:00
Daniel Imms d42f2b54ff Merge pull request #5484 from Tyriar/tyriar/fix_sextants
Fix sextant rendering
2025-12-24 06:43:57 -08:00
Daniel Imms 79084c3066 Fix sextant rendering
Broke in #5483
2025-12-24 06:38:21 -08:00
Daniel Imms b4f4946cca Merge pull request #5483 from Tyriar/glyph_cleanup
Consolidate custom glyph definition parts
2025-12-24 06:21:11 -08:00
Daniel Imms ed779b9062 Replace PATH_FUNCTION_WITH_WEIGHT with a generic strokeWidth prop 2025-12-24 06:14:41 -08:00
Daniel Imms 20fdfba16f Replace BLOCK_PATTERN_WITH_REGION with new clipPath 2025-12-24 04:33:32 -08:00
Daniel Imms 11dae84ff7 Support clip path on any draw definition 2025-12-24 04:25:59 -08:00
Daniel Imms 579573e77d Merge pull request #5482 from xtermjs/Tyriar-patch-1
Remove line ending editorconfig file
2025-12-24 04:24:04 -08:00
Daniel Imms 2114abe88c Support multiple glyphs with multiple draw calls 2025-12-24 04:19:14 -08:00
Daniel Imms d4fd96639f Remove line ending editorconfig file
This is normalized by git and ends up causing git status to show files that don't actually need to be committed after line ending normalization.
2025-12-24 04:18:31 -08:00
Daniel Imms 269d8c20ae Merge pull request #5475 from Tyriar/depc
Remove remnants of deprecated options
2025-12-24 04:03:49 -08:00
TatLead 49b49627f1 Fix incorrect ITheme reference in comments 2025-12-24 07:10:12 +08:00
Daniel Imms 663b94011a Remove remnants of deprecated options
Following up on #5462
2025-12-23 08:24:35 -08:00
Daniel Imms 54fcb1ea2f Merge pull request #5474 from Tyriar/demo_warn
Fix warnings in demo
2025-12-23 08:17:29 -08:00
Daniel Imms a9da3fbf05 Merge pull request #5473 from Tyriar/tyriar/5466_legacy_computing
Support entire Symbols for Legacy Computing unicode block (0x1FB00-0x1FBFF), add missing powerline symbol
2025-12-23 08:15:05 -08:00
Daniel Imms 091c3967f7 Fix warnings in demo 2025-12-23 08:12:12 -08:00
Daniel Imms 10d6ce3805 Update customGlyphs option 2025-12-23 08:09:46 -08:00
Daniel Imms eb285af11d Add diagonal alignment tests 2025-12-23 08:05:02 -08:00
Daniel Imms 0976935f87 Implement remaining Terminal graphic characters (1FBC0-1FBCA) 2025-12-23 07:42:57 -08:00
Daniel Imms 884b667f4e Implement stick figures 1FBC5-1FBC9 2025-12-23 07:04:19 -08:00
Daniel Imms b8843732bf Implement Terminal graphic characters (1FBCB-1FBCD) 2025-12-23 04:49:46 -08:00
Daniel Imms 44d8d2c873 Finish implementing Terminal graphic characters (1FBB9-1FBBC) 2025-12-23 04:28:45 -08:00
Daniel Imms 3d3abf9cf2 Implement folder 1FBB9-1FBBA 2025-12-23 04:22:19 -08:00
Daniel Imms 268f7af5db Implement running man 1FBB2 and 1FBB3 2025-12-23 04:15:41 -08:00
Daniel Imms 16b72dfd63 Mark powerline symbol range as reserved 2025-12-23 03:50:34 -08:00
Daniel Imms a0e658a18c Add column number powerline symbol 2025-12-23 03:42:41 -08:00
Daniel Imms ea6385f310 Comment alignment 2025-12-23 03:39:37 -08:00
Daniel Imms 02a9d2b047 Indicate reserved codepoints in demo 2025-12-23 03:30:02 -08:00
Daniel Imms 5c0c553936 Implement Arrows (1FBB4-1FBB8) 2025-12-22 19:46:35 -08:00
Daniel Imms eb743e0e19 Implement Terminal graphic character (1FBFA-1FBFA) 2025-12-22 19:15:00 -08:00
Daniel Imms 29d91fe264 Fill in remaining parts of legacy computing 2025-12-22 16:27:41 -08:00
Daniel Imms e8dcf46f39 Add PATH_NEGATIVE, simplify 1FBBD-1FBBF 2025-12-22 16:22:06 -08:00
Daniel Imms 66c1316f87 Implement ARROWHEAD-SHAPED POINTER 2025-12-22 15:43:53 -08:00
Daniel Imms e1803ed5a5 Implement Triangular shade characters (1FB9C-1FB9F) 2025-12-22 15:30:55 -08:00
Daniel Imms 1c3813f3d4 Implement Negative terminal graphic characters (1FBBD-1FBBF) 2025-12-22 15:28:51 -08:00
Daniel Imms e12aef9287 Implement Block elements (1FBCE-1FBCF) 2025-12-22 15:21:10 -08:00
Daniel Imms f5edba0b64 Improve docs on segmentedDigit 2025-12-22 15:18:42 -08:00
Daniel Imms 5a995e5595 Implement Geometric shapes (1FBE0-1FBEF) 2025-12-22 15:14:40 -08:00
Daniel Imms 2bae212f83 Implement Character cell diagonals (1FBD0-1FBDF) 2025-12-22 14:19:02 -08:00
Daniel Imms bde3680cd1 Implement 2 more parts 2025-12-22 14:15:26 -08:00
Daniel Imms 717399ceb6 Implement diagonal fill chars and light solid line with stroke 2025-12-22 14:04:51 -08:00
Daniel Imms 51121f1913 Stub legacy computing custom glyph ranges
Part of #5466
2025-12-22 13:42:38 -08:00
Daniel Imms 483acd3335 Merge pull request #5472 from Tyriar/tyriar/5466_refactor__unified
Custom glyph: Unify rendering function and move all definitions into a single ordered object
2025-12-22 13:31:36 -08:00
Daniel Imms 9dc8cecea3 Fix imports 2025-12-22 13:04:44 -08:00
Daniel Imms 89c2649504 Merge branch 'master' into tyriar/5466_refactor__unified 2025-12-22 12:57:03 -08:00
Daniel Imms 390076fc3f Order box drawing consistently 2025-12-22 12:56:16 -08:00
Daniel Imms a4edeed85d Add box drawing to custom glyph ranges test 2025-12-22 12:48:23 -08:00
Daniel Imms e61697fc86 Start organizing box drawing characters 2025-12-22 12:46:43 -08:00
Daniel Imms 9c019b44c3 unified -> customGlyph 2025-12-22 12:39:31 -08:00
Daniel Imms 5579d83f7d Move box drawing into unified 2025-12-22 12:39:14 -08:00
Daniel Imms 429065487a Move block elements and like into unified 2025-12-22 12:34:12 -08:00
Daniel Imms 219036cb0f Add powerline to glyph ranges test 2025-12-22 12:26:58 -08:00
Daniel Imms 3e80637145 Move powerline to unified 2025-12-22 12:14:50 -08:00
Daniel Imms 0af884d379 Correct shade rectangle rendering 2025-12-22 12:09:41 -08:00
Daniel Imms 5ae9ddf4b8 Add range to headers in ranges test 2025-12-22 12:06:59 -08:00
Daniel Imms 7ac068844c Fix warnings in unicodeTables.ts 2025-12-22 12:06:27 -08:00
Daniel Imms 5bf3d57d16 Move rectangular shade chars into unified 2025-12-22 12:05:47 -08:00
Daniel Imms 6461829411 Clean up symbols for legacy def 2025-12-22 11:48:45 -08:00
Daniel Imms efd6a52033 Support path without function 2025-12-22 11:46:53 -08:00
Daniel Imms 3d9e976cb3 Move main symbols for legacy computing into unified 2025-12-22 11:41:49 -08:00
Daniel Imms 415de67106 Start move to unified draw definition
Part of #5466
2025-12-22 11:35:41 -08:00
Daniel Imms c17c01086f Merge pull request #5471 from Tyriar/tyriar/5466_refactor
Refactor custom glyph to move into folder, split definition from raster
2025-12-22 11:34:43 -08:00
Daniel Imms ce41b175ee Remove cyclic dep 2025-12-22 11:11:50 -08:00
Daniel Imms 32b4547c5a Tweak symbol names 2025-12-22 11:09:02 -08:00
Daniel Imms b64543aa59 Make medium shade consistent, make all shades less heavy 2025-12-22 10:57:59 -08:00
Daniel Imms f049c42acc Add CustomGlyph to exports from rasterizer 2025-12-22 10:48:50 -08:00
Daniel Imms 28760cf482 Move custom glyphs into new folder 2025-12-22 10:47:35 -08:00
Daniel Imms 4982305b51 Split definitions and rasterizer 2025-12-22 10:46:53 -08:00
Daniel Imms 7fee40ddcd Merge pull request #5470 from Tyriar/tyriar/5466_shade
Custom glyph support for rectangular shade/combo chars
2025-12-22 10:41:54 -08:00
Daniel Imms 180183e876 Custom glyph support for rectangular shade/combo chars
Range: U+1FB8C-U+1FB94

Part of #5466
2025-12-22 10:29:22 -08:00
Daniel Imms 8308ede3f5 Merge pull request #5468 from Tyriar/5466_mosaic
Custom glyph support for mosaic terminal graphics chars
2025-12-22 10:27:53 -08:00
Daniel Imms a6db7b41f6 Suppress naming-convention for file as it conflicts with unicode props 2025-12-22 08:34:43 -08:00
Daniel Imms f11241d9de Custom glyph support for mosaic terminal graphics chars
Range: U+1FB3C-U+1FB6F

Part of #5466
2025-12-22 08:29:03 -08:00
Daniel Imms 937deb9f4b Merge pull request #5467 from Tyriar/5464_block_sextants
Custom glyph support for block sextants (U+1FB00-U+1FB3B)
2025-12-22 08:27:29 -08:00
Daniel Imms 6acc861b06 Lint 2025-12-22 07:54:47 -08:00
Daniel Imms 8f05b1854b Lint 2025-12-22 07:41:18 -08:00
Daniel Imms 0f461e7a3e Custom glyph support for block sextants (U+1FB00-U+1FB3B)
Fixes #5464
2025-12-22 07:33:43 -08:00
Daniel Imms 4237d82923 Merge pull request #5465 from Tyriar/5464_demo_helper
Add demo helper to print Symbols for Legacy Computing
2025-12-22 07:26:42 -08:00
Daniel Imms 187156764a Add demo helper to print Symbols for Legacy Computing
Range: 1FB00–1FBFF

Part of #5464
2025-12-22 07:16:22 -08:00
Shuhei Takahashi d03416467d Fix key duplication on Android with physical keyboards
When typing rapidly on Android with physical keyboards, characters often
duplicate on the terminal. This is because physical keyboard events
always go through IME on Android, even for plain English characters, and
rapid typing causes a race condition in the composition helper.

This patch fixes the issue by making sure textarea changes are handled
correctly.
2025-11-07 12:33:59 +09:00
Jörg Breitbart 085a275038 fix promise return, reject on missing family name, doc polish 2024-10-06 23:53:30 +02:00
Jörg Breitbart 104c63d7e0 cleanup files 2024-10-04 17:52:45 +02:00
Jörg Breitbart 70c9861c39 Merge branch 'master' into webfonts-addon 2024-10-04 17:51:18 +02:00
Jörg Breitbart c93623cb3b CI tests 2024-10-04 14:24:36 +02:00
Jörg Breitbart b0389a2ed1 future proof demo 2024-10-04 05:31:47 +02:00
Jörg Breitbart 7907f13774 add license notes for fonts 2024-10-04 04:14:10 +02:00
Jörg Breitbart 956c7cbcba demo tests 2024-10-04 04:00:20 +02:00
Jörg Breitbart 23bef99132 cleanup API 2024-10-04 02:06:02 +02:00
Jörg Breitbart b104bcd144 remove await remnants 2024-10-04 00:42:38 +02:00
Jörg Breitbart 809b9aa1ad fix test runner 2024-10-04 00:15:49 +02:00
Jörg Breitbart 9a536f758d fix integration test runner 2024-10-03 22:23:31 +02:00
Jörg Breitbart 15a63bae22 change to global replacement 2024-10-03 22:10:40 +02:00
Jörg Breitbart 41640d9402 make linter happy 2024-10-03 22:07:10 +02:00
Jörg Breitbart 15664de472 initial 2024-10-03 21:50:34 +02:00
280 changed files with 23070 additions and 11030 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "xterm.js",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
"image": "mcr.microsoft.com/devcontainers/typescript-node:22-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": 18
"version": 22
} // yarn
},
"forwardPorts": [
-1
View File
@@ -5,7 +5,6 @@ indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.{j,t}s]
max_line_length = 100
-273
View File
@@ -1,273 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"demo/tsconfig.json",
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
"src/vs/tsconfig.json",
"test/benchmark/tsconfig.json",
"test/playwright/tsconfig.json",
"addons/addon-attach/src/tsconfig.json",
"addons/addon-attach/test/tsconfig.json",
"addons/addon-clipboard/src/tsconfig.json",
"addons/addon-clipboard/test/tsconfig.json",
"addons/addon-fit/src/tsconfig.json",
"addons/addon-fit/test/tsconfig.json",
"addons/addon-image/src/tsconfig.json",
"addons/addon-image/test/tsconfig.json",
"addons/addon-ligatures/src/tsconfig.json",
"addons/addon-progress/src/tsconfig.json",
"addons/addon-progress/test/tsconfig.json",
"addons/addon-search/src/tsconfig.json",
"addons/addon-search/test/tsconfig.json",
"addons/addon-serialize/src/tsconfig.json",
"addons/addon-serialize/test/tsconfig.json",
"addons/addon-serialize/benchmark/tsconfig.json",
"addons/addon-unicode11/src/tsconfig.json",
"addons/addon-unicode11/test/tsconfig.json",
"addons/addon-unicode-graphemes/src/tsconfig.json",
"addons/addon-unicode-graphemes/test/tsconfig.json",
"addons/addon-unicode-graphemes/benchmark/tsconfig.json",
"addons/addon-web-links/src/tsconfig.json",
"addons/addon-web-links/test/tsconfig.json",
"addons/addon-webgl/src/tsconfig.json",
"addons/addon-webgl/test/tsconfig.json"
],
"sourceType": "module"
},
"ignorePatterns": [
"addons/*/src/third-party/*.ts",
"src/vs/*",
"out/*",
"out-test/*",
"out-esbuild/*",
"out-esbuild-test/*",
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js",
"**/*.mjs"
],
"plugins": [
"@stylistic/ts",
"@typescript-eslint",
"jsdoc"
],
"rules": {
"@stylistic/ts/indent": [
"warn",
2
],
"@stylistic/ts/semi": [
"warn",
"always"
],
"@stylistic/ts/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"@typescript-eslint/consistent-type-assertions": "warn",
"@typescript-eslint/consistent-type-definitions": "warn",
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@typescript-eslint/explicit-member-accessibility": [
"warn",
{
"accessibility": "explicit",
"overrides": {
"constructors": "off"
}
}
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": [
"warn",
{ "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"] },
// memberLike
{ "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" },
{ "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"],
"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",
"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,
"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": [
"warn",
{
"allowElseIf": false
}
],
"no-eval": "warn",
"no-extra-semi": "error",
"no-irregular-whitespace": "warn",
"no-restricted-imports": [
"warn",
{
"patterns": [
".*\\/out\\/.*"
]
}
],
"no-restricted-syntax": [
"warn",
{
"selector": "CallExpression[callee.name='requestAnimationFrame']",
"message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "CallExpression[callee.name='cancelAnimationFrame']",
"message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']",
"message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']",
"message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']",
"message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService."
}
],
"no-trailing-spaces": "warn",
"no-unsafe-finally": "warn",
"no-unused-vars": ["warn", {
"vars": "all",
"args": "none"
}],
"no-var": "warn",
"one-var": [
"warn",
"never"
],
"object-curly-spacing": [
"warn",
"always"
],
"prefer-const": "warn",
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"],
"exceptions": ["-"]
}
]
},
"overrides": [
{
"files": [
"**/*.api.ts",
"**/*.test.ts"
],
"rules": {
"object-curly-spacing": "off",
"max-len": "off",
"no-unused-vars": "off"
}
}
]
}
-107
View File
@@ -1,107 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@stylistic/ts",
"@typescript-eslint",
"jsdoc"
],
"rules": {
"@stylistic/ts/indent": [
"warn",
2
],
"@stylistic/ts/semi": [
"warn",
"always"
],
"@stylistic/ts/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@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",
"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-extra-semi": "error",
"no-irregular-whitespace": "warn",
"no-trailing-spaces": "warn",
"object-curly-spacing": [
"warn",
"always"
],
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"],
"exceptions": ["-"]
}
]
}
}
+4 -2
View File
@@ -22,9 +22,11 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
**Testing**:
- Unit tests: `npm run test-unit` (Mocha)
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
- Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js`
- Per-addon unit tests: `npm run test-unit -- addons/addon-image/out-esbuild/*.test.js`
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
- Per-addon integration tests: `npm run test-integration --suite=addon-search`
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
## Addon Development Pattern
@@ -0,0 +1,18 @@
---
applyTo: '**/*.benchmark.ts'
---
# Benchmark run instructions
- Full suite: `npm run benchmark`
- Single benchmark file:
- Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js`
- Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js`
- Single context/case:
- Use `-t` to get the path, then:
- `npm run benchmark -- -s "<path>" out-test/benchmark/Event.benchmark.js`
When writing instructions, use `RuntimeCase` to measure pure runtime in ms, use `ThroughputRuntimeCase` when measuring throughput in MB/s.
Notes:
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
- Keep `NODE_PATH=./out` (handled by the npm script).
+9 -2
View File
@@ -62,6 +62,9 @@ jobs:
./addons/addon-web-links/lib/* \
./addons/addon-web-links/out/* \
./addons/addon-web-links/out-*/* \
./addons/addon-web-fonts/lib/* \
./addons/addon-web-fonts/out/* \
./addons/addon-web-fonts/out-*/* \
./addons/addon-webgl/lib/* \
./addons/addon-webgl/out/* \
./addons/addon-webgl/out-*st/*
@@ -199,8 +202,10 @@ jobs:
unzip -o compressed-build.zip
fi
ls -R
- name: Build demo
run: npm run esbuild-demo
- name: Build demo client
run: npm run esbuild-demo-client
- name: Build demo server
run: npm run esbuild-demo-server
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
- name: Integration tests (addon-attach)
@@ -221,6 +226,8 @@ jobs:
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
- name: Integration tests (addon-unicode11)
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
- name: Integration tests (addon-web-fonts)
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts
- name: Integration tests (addon-web-links)
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
- name: Integration tests (addon-webgl)
+1
View File
@@ -3,6 +3,7 @@ node_modules/
.lock-wscript
lib/
out/
out-demo/
out-test/
out-esbuild/
out-esbuild-test/
-7
View File
@@ -1,7 +0,0 @@
Antonis Kalipetis <akalipetis@sourcelair.com>
Antonis Kalipetis <akalipetis@sourcelair.com> <akalipetis@gmail.com>
Daniel Imms <daimms@microsoft.com> <tyriar@tyriar.com>
Paris Kasidiaris <paris@sourcelair.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
Thanasis Daglis <thanasis@sourcelair.com> <ath.daglis@gmail.com>
-1
View File
@@ -1 +0,0 @@
NODE_PATH=./out
-5
View File
@@ -1,5 +0,0 @@
require:
- source-map-support/register
reporter: spec
color: true
check-leaks: true
-1
View File
@@ -1 +0,0 @@
package-lock=false
+1 -1
View File
@@ -1 +1 @@
18
22
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
+1 -1
View File
@@ -61,7 +61,7 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"stopOnEntry": true,
"runtimeVersion": "18",
"runtimeVersion": "22",
"serverReadyAction": {
"action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)"
+1 -20
View File
@@ -1,23 +1,4 @@
{
"files.associations": {
".eslintrc.json.typings": "jsonc"
},
"files.exclude": {
"**/.nyc_output": true
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
"mochaExplorer.envPath": ".mocha.env",
"mochaExplorer.files": [
"out/**/*.test.js",
"addons/**/out/*.test.js",
"out-*/**/*.test.js",
"addons/**/out-*/*.test.js"
],
"mochaExplorer.watch": [
"out/**/*.js",
"addons/**/out/*.js",
"out-*/**/*.js",
"addons/**/out-*/*.js"
]
"typescript.preferences.quoteStyle": "single"
}
+25 -4
View File
@@ -10,13 +10,22 @@
"tasks": [
// Compound tasks
{
"label": "Development",
"dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo"],
"label": "dev",
"detail": "Runs all tasks required to run the demo in a single terminal using concurrently. This does not support problem matching.",
"type": "npm",
"script": "dev",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "dev (separate terminals)",
"detail": "Runs all tasks required to run the demo in separate terminals. This does support problem matching.",
"dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo-client", "esbuild-demo-server"],
"group": "build"
},
// Demo
{
@@ -55,9 +64,21 @@
}
},
{
"label": "esbuild-demo",
"label": "esbuild-demo-client",
"type": "npm",
"script": "esbuild-demo-watch",
"script": "esbuild-demo-client-watch",
"dependsOn": ["esbuild", "tsc"],
"group": "build",
"isBackground": true,
"problemMatcher": "$esbuild-watch",
"presentation": {
"group": "xterm-demo"
}
},
{
"label": "esbuild-demo-server",
"type": "npm",
"script": "esbuild-demo-server-watch",
"dependsOn": ["esbuild", "tsc"],
"group": "build",
"isBackground": true,
+35 -89
View File
@@ -1,104 +1,50 @@
# How to contribute to xterm.js
- [Opening issues for bug reports or feature requests](#opening-issues)
- [Contributing code](#contributing-code)
- [Opening issues](#opening-issues)
- [Answering discussion questions](#answering-discussion-questions)
## Contributing code
You can find issues to work on by looking at issues labeled with [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). It's a good idea to comment on the issue saying that you're looking into it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
- Fork [xterm.js](https://github.com/xtermjs/xterm.js/) ([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
- Make the fix and verify it works in the demo. Be sure to follow thew general code style of the rest of the project.
- If your changes are easy to test or likely to regress in the future, add tests. These could be unit or integration (playwright) tests.
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
> ![TIP]
> Don't put more than one feature or fix in a single pull request. The smaller pull requests are the easier they are to review and merge.
By contributing code to xterm.js you:
- Agree to license the contributed code under xterm.js' [MIT license](LICENSE).
- Confirm that you have the right to contribute and license the code in question. This means that either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.
## Opening issues
The preferred way to report bugs or request features is to use
[GitHub issues](http://github.com/sourcelair/xterm.js/issues). Before
opening an issue, read these pointers.
[GitHub issues](http://github.com/xtermjs/xterm.js/issues).
### Opening issues effectively
### Creating great issues
- Include information about **the browser in which the problem occurred**. Even
if you tested several browsers, and the problem occurred in all of them,
mention this fact in the bug report. Also include browser version numbers and
the operating system that you're on.
- Include information about **the browser in which the problem occurred** or the terminal being used. If you tested several browsers and the problem occurred in all of them, mention this fact in the bug report. Also include browser version numbers and the operating system that you're on.
- Include the version of xterm.js being used, preferably either with the latest `beta` tagged release on npm or reproducing in the demo on the `master` branch.
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the exact steps a maintainer has to take to make the problem occur.
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), provide an HTML document that demonstrates the problem.
- Be polite and follow [the code of conduct](https://github.com/xtermjs/xterm.js?tab=coc-ov-file#readme).
- Mention which release of xterm.js you're using. Preferably, try also with
the current HEAD of the master branch, to ensure the problem has not already been
fixed.
### Issue triaging philosophy
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
exact steps a maintainer has to take to make the problem occur.
It's pretty common for maintainers of large open source projects to suffer from burnout, especially when needing to triage a large number of incoming issues instead of actually building things. Here are some of the steps we take to try mitigate this:
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
- Support questions live in [GH discussions](https://github.com/xtermjs/xterm.js/discussions), issues may be transfered there without further comment and core maintainers may or may not participate in discussions.
- Issues are strictly for well defined features or bugs that are _actionable_.
- Sometimes features are out of scope. A common example of this is a niche feature that the pricipal implementation ([VS Code](https://code.visualstudio.com/)) won't leverage and therefore would be difficult to maintain and likely suffer from bitrot. The reporter may not agree with this, but you could always create an addon if that works or maintain your own fork if it comes to that.
- If a feature does not have a clear way forward or needs more discussion it may be closed ro moved to a discussion.
- If a bug is not easily reproducible it may be closed or moved to a discussion. Generally issues that are labeled are something we want to do or has actionable steps to look into further.
## 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:
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
- Make your changes.
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below).
- Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to xterm.js you:
- Agree to license the contributed code under xterm.js' [MIT
license](LICENSE).
- Confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Test coverage
One area that always needs attention is improving out unit test coverage, you can view the code coverage report on [Azure Pipelines](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) by clicking the Code Coverage tab.
## Testing
### Unit tests
Unit tests are run with `npm run test-unit`:
```sh
# All unit tests
npm run test-unit
# Absolute file path
npm run test-unit out-esbuild/browser/Terminal.test.js
# Filter by wildcard
npm run test-unit out-esbuild/**/Terminal.test.js
# Specific addon unit tests tests
npm run test-unit addons/addon-image/out-esbuild/*.test.js
# Multiple files
npm run test-unit out-esbuild/**/Terminal.test.js out-esbuild/**/InputHandler.test.js
```
These use mocha to run all `.test.js` files within the esbuild output (`out-esbuild/`).
### Integration tests
Integration tests are run with `npm run test-integration`:
```sh
# All integration tests
npm run test-integration
# Core integration tests
npm run test-integration --suite=core
# Specific addon integration tests
npm run test-integration --suite=addon-search
```
These use `@playwright/test` to run all tests within the esbuild test output (`out-esbuild-test/`).
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.
+27 -28
View File
@@ -1,27 +1,27 @@
# [![xterm.js logo](images/logo-full.png)](https://xtermjs.org)
# [![xterm.js](images/logo-full.png)](https://xtermjs.org)
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.
Xterm.js is a frontend component that enables applications to bring fully-featured terminals to their users in the browser. It's used by popular projects such as [VS Code](https://code.visualstudio.com/) (and its forks), [Tabby](https://tabby.sh/) and [Hyper](https://hyper.is/).
## Features
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events.
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Performant**: Xterm.js is *really* fast and includes an optional GPU-accelerated renderer.
- **Rich Unicode support**: Supports CJK, emojis, and IMEs.
- **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, addons, well documented API, etc.
- **Self-contained**: The core library has zero dependencies.
- **Accessible**: Screen reader mode and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, custom glyphs, addons, well documented API, etc.
## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer.
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) through a library like [node-pty](https://github.com/microsoft/node-pty).
## Getting Started
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
First, you need to install the module. We ship exclusively through [npm](https://www.npmjs.com), so you need that installed and then add [@xterm/xterm](https://www.npmjs.com/package/@xterm/xterm) as a dependency by running:
```bash
npm install @xterm/xterm
npm install --save @xterm/xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
@@ -54,12 +54,10 @@ import { Terminal } from '@xterm/xterm';
### Addons
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
```bash
npm i -S @xterm/addon-web-links
npm install --save @xterm/addon-web-links
```
Then import the addon, instantiate it and call `Terminal.loadAddon`:
@@ -80,50 +78,51 @@ The xterm.js team maintains the following addons, but anyone can build them:
- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
- [`@xterm/addon-ligatures`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-ligatures): Enables rendering of ligatures
- [`@xterm/addon-progress`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-progress): Adds support for the progress API (`OSC 9;4`)
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to a VT sequences or HTML
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to VT sequences or HTML
- [`@xterm/addon-unicode-graphemes`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes): Enhanced unicode support including grapheme clustering (experimental)
- [`@xterm/addon-unicode11`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11): Updates character widths to their unicode11 values
- [`@xterm/addon-web-fonts`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-fonts): Easily integrate web fonts
- [`@xterm/addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-links): Adds web link detection and interaction
- [`@xterm/addon-webgl`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl): Renders xterm.js using a `canvas` element's webgl2 context
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
Since xterm.js is typically implemented as a developer tool, generally only modern evergreen browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*. Xterm.js also works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
### Node.js Support
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the serialize addon so it can get all state restored upon reconnection.
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs headless in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the [serialize addon](https://www.npmjs.com/package/@xterm/addon-serialize) so it can get all state restored upon reconnection.
## API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
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.
Some APIs may be marked with *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).
Stable releases are done on an as needed basis. All current and past releases are available on this repo's [releases page](https://github.com/sourcelair/xterm.js/releases), you can see what's planned for upcoming releases looking through the repository [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:
Beta releases are continuously published off the `master` branch. Install the latest beta build with:
```bash
npm install -S @xterm/xterm@beta
npm install --save @xterm/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.
The principal implementation (VS Code) typically uses the latest or near the latest beta build. Generally they are quite stable but can potentially contain bugs or breaking changes. If stability is very important we recommend using the beta build primarily to test out new features and to verify bug fixes, unless you're tracking what's landing and are comfortable taking that risk.
## 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.
Read [CONTRIBUTING.md](https://github.com/xtermjs/xterm.js/blob/master/CONTRIBUTING.md) to learn how to contribute to the project.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
Xterm.js is used in many world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
@@ -232,12 +231,12 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**EmuDevz**](https://afska.github.io/emudevz): A free coding game where players learn how to build an emulator from scratch.
- [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.
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. Please add any new contributions to the end of the list.
## 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-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2017-2026, [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
@@ -12,7 +12,7 @@ interface IAttachOptions {
bidirectional?: boolean;
}
export class AttachAddon implements ITerminalAddon , IAttachApi {
export class AttachAddon implements ITerminalAddon, IAttachApi {
private _socket: WebSocket;
private _bidirectional: boolean;
private _disposables: IDisposable[] = [];
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
-22
View File
@@ -1,22 +0,0 @@
{
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
}
},
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
"license": "BSD-3-Clause"
}
}
}
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
-8
View File
@@ -1,8 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
js-base64@^3.7.5:
version "3.7.7"
resolved "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz"
integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
+17 -13
View File
@@ -3,9 +3,8 @@
* @license MIT
*/
import type { Terminal, ITerminalAddon } from '@xterm/xterm';
import type { Terminal, ITerminalAddon, IRenderDimensions } from '@xterm/xterm';
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ViewportConstants } from 'browser/shared/Constants';
interface ITerminalDimensions {
@@ -23,7 +22,18 @@ interface ITerminalDimensions {
const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 1;
export class FitAddon implements ITerminalAddon , IFitApi {
function getWindow(e: Node): Window {
if (e?.ownerDocument?.defaultView) {
return e.ownerDocument.defaultView;
}
return window;
}
function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
return getWindow(el).getComputedStyle(el, null);
}
export class FitAddon implements ITerminalAddon, IFitApi {
private _terminal: Terminal | undefined;
public activate(terminal: Terminal): void {
@@ -38,12 +48,8 @@ export class FitAddon implements ITerminalAddon , IFitApi {
return;
}
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
// Force a full render
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
core._renderService.clear();
this._terminal.resize(dims.cols, dims.rows);
}
}
@@ -57,11 +63,9 @@ export class FitAddon implements ITerminalAddon , IFitApi {
return undefined;
}
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
const dims: IRenderDimensions = core._renderService.dimensions;
const dims: IRenderDimensions | undefined = this._terminal.dimensions;
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
if (!dims || dims.css.cell.width === 0 || dims.css.cell.height === 0) {
return undefined;
}
@@ -69,10 +73,10 @@ export class FitAddon implements ITerminalAddon , IFitApi {
? 0
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(this._terminal.element);
const elementStyle = _getComputedStyle(this._terminal.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
+2 -3
View File
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+3 -1
View File
@@ -30,7 +30,9 @@ const customSettings: IImageAddonOptions = {
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
iipSizeLimit: 20000000, // size limit of a single IIP sequence
kittySupport: true, // enable Kitty graphics support
kittySizeLimit: 20000000 // size limit of a single Kitty sequence
}
// initialization
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

-817
View File
@@ -1,817 +0,0 @@
{
"name": "@xterm/addon-image",
"version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xterm/addon-image",
"version": "0.8.0",
"license": "MIT",
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/inwasm": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.8.2",
"acorn-walk": "^8.2.0",
"chokidar": "^3.5.3",
"colorette": "^2.0.20",
"glob": "^10.0.0",
"wabt": "^1.0.32"
},
"bin": {
"inwasm": "lib/cli.js"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sixel": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/sixel/-/sixel-0.16.0.tgz",
"integrity": "sha512-xicu6Y6Cyhmv5rjyHxq2r5RnKerlL/nyZEGjOU5bLCshXkZryc9JFJThTCKPOAtWXCfeWquEKFVFfMPcTD25PA==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/wabt": {
"version": "1.0.39",
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"wasm-decompile": "bin/wasm-decompile",
"wasm-interp": "bin/wasm-interp",
"wasm-objdump": "bin/wasm-objdump",
"wasm-stats": "bin/wasm-stats",
"wasm-strip": "bin/wasm-strip",
"wasm-validate": "bin/wasm-validate",
"wasm2c": "bin/wasm2c",
"wasm2wat": "bin/wasm2wat",
"wat2wasm": "bin/wat2wasm"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/xterm-wasm-parts": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"inwasm": "^0.0.13"
}
}
}
}
+1 -1
View File
@@ -25,6 +25,6 @@
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
"xterm-wasm-parts": "^0.3.0"
}
}
+26 -19
View File
@@ -5,16 +5,16 @@
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 Base64Decoder, { type DecodeStatus } 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
// limit hold memory in base64 decoder (encoded bytes)
const KEEP_DATA = 4194304;
const INITIAL_DATA = 1048576;
// Local mirror of const enum (esbuild can't inline const enums from external packages)
const DECODER_OK: DecodeStatus.OK = 0;
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
@@ -31,7 +31,7 @@ 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 _dec: Base64Decoder;
private _metrics = UNSUPPORTED_TYPE;
constructor(
@@ -39,7 +39,11 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private readonly _renderer: ImageRenderer,
private readonly _storage: ImageStorage,
private readonly _coreTerminal: ITerminalExt
) {}
) {
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
const initialBytes = Math.min(INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(KEEP_DATA, maxEncodedBytes, initialBytes);
}
public reset(): void {}
@@ -54,7 +58,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data, start, end)) {
if (this._dec.put(data.subarray(start, end)) !== DECODER_OK) {
this._dec.release();
this._aborted = true;
}
@@ -70,8 +74,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
this._aborted = true;
return;
}
this._dec.init(this._header.size);
if (this._dec.put(data, dataPos, end)) {
this._dec.init();
if (this._dec.put(data.subarray(dataPos, end)) !== DECODER_OK) {
this._dec.release();
this._aborted = true;
}
@@ -89,13 +93,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
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.data8.length === this._header.size) {
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;
}
}
}
}
@@ -105,7 +111,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return true;
}
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
// HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
this._dec.release();
if (!window.createImageBitmap) {
+4 -3
View File
@@ -8,6 +8,7 @@ declare const Buffer: any;
export interface IHeaderFields {
[key: string]: number | string | Uint32Array | null | undefined;
// 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.
@@ -81,7 +82,7 @@ function toName(data: Uint32Array): string {
return new TextDecoder().decode(b);
}
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
inline: toInt,
size: toInt,
name: toName,
@@ -100,7 +101,7 @@ export class HeaderParser {
private _buffer = new Uint32Array(MAX_FIELDCHARS);
private _position = 0;
private _key = '';
public fields: {[key: string]: any} = {};
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
public reset(): void {
this._buffer.fill(0);
@@ -176,7 +177,7 @@ export class HeaderParser {
try {
const v = this._buffer.slice(0, pos);
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
} catch (e) {
} catch {
return false;
}
return true;
+15 -3
View File
@@ -8,6 +8,7 @@ import type { ImageAddon as IImageApi } from '@xterm/addon-image';
import { IIPHandler } from './IIPHandler';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
import { SixelHandler } from './SixelHandler';
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
@@ -22,7 +23,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
iipSizeLimit: 20000000,
kittySupport: true,
kittySizeLimit: 20000000
};
// max palette size supported by the sixel lib (compile time setting)
@@ -48,7 +51,7 @@ const enum GaStatus {
}
export class ImageAddon implements ITerminalAddon , IImageApi {
export class ImageAddon implements ITerminalAddon, IImageApi {
private _opts: IImageAddonOptions;
private _defaultOpts: IImageAddonOptions;
private _storage: ImageStorage | undefined;
@@ -90,7 +93,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
// windowOptions.getCellSizePixels = true;
// windowOptions.getWinSizeChars = true;
// terminal.setOption('windowOptions', windowOptions);
const windowOps = terminal.options.windowOptions || {};
const windowOps = terminal.options.windowOptions ?? {};
windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true;
windowOps.getWinSizeChars = true;
@@ -144,6 +147,15 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
);
}
// Kitty graphics handler
if (this._opts.kittySupport) {
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, this._storage!, terminal);
this._handlers.set('kitty', kittyHandler);
this._disposeLater(
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
);
}
}
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
+3 -3
View File
@@ -38,7 +38,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
* Only the DOM output canvas should be on the terminal's document,
* which gets explicitly checked in `insertLayerToDom`.
*/
const canvas = (localDocument || document).createElement('canvas');
const canvas = (localDocument ?? document).createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
return canvas;
@@ -124,7 +124,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
* Forwarded from internal render service.
*/
public get dimensions(): IRenderDimensions | undefined {
return this._renderService?.dimensions;
return this._terminal.dimensions;
}
/**
@@ -242,7 +242,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
}
if (!this._placeholder) return;
this._ctx.drawImage(
this._placeholderBitmap || this._placeholder!,
this._placeholderBitmap ?? this._placeholder!,
col * width,
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
width * count,
+25 -9
View File
@@ -132,8 +132,10 @@ export class ImageStorage implements IDisposable {
) {
try {
this.setLimit(this._opts.storageLimit);
} catch (e: any) {
console.error(e.message);
} catch (e: unknown) {
if (e instanceof Error) {
console.error(e.message);
}
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
}
this._viewportMetrics = {
@@ -233,9 +235,22 @@ export class ImageStorage implements IDisposable {
}
/**
* Method to add an image to the storage.
* Delete an image by its internal storage ID.
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
*/
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
public deleteImage(id: number): void {
const spec = this._images.get(id);
if (spec) {
spec.marker?.dispose();
this._delImg(id);
}
}
/**
* Method to add an image to the storage.
* Returns the internal image ID assigned to the stored image.
*/
public addImage(img: HTMLCanvasElement | ImageBitmap): number {
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);
@@ -329,6 +344,7 @@ export class ImageStorage implements IDisposable {
// finally add the image
this._images.set(imageId, imgSpec);
return imageId;
}
@@ -381,7 +397,7 @@ export class ImageStorage implements IDisposable {
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;
let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
@@ -400,7 +416,7 @@ export class ImageStorage implements IDisposable {
while (
++col < cols
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
&& (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
&& (e.imageId === imageId)
&& (e.tileId === startTile + count)
) {
@@ -442,7 +458,7 @@ export class ImageStorage implements IDisposable {
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 e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
@@ -487,7 +503,7 @@ export class ImageStorage implements IDisposable {
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;
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) {
@@ -507,7 +523,7 @@ export class ImageStorage implements IDisposable {
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;
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) {
+1 -1
View File
@@ -99,7 +99,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
}
const canvas = ImageRenderer.createCanvas(undefined, width, height);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release();
}
+4 -2
View File
@@ -8,7 +8,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/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 { IParams, IDcsHandler, IOscHandler, IApcHandler, 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';
@@ -22,7 +22,7 @@ export const enum Cell {
}
// export some privates for local usage
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
/**
* Plugin ctor options.
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
kittySupport: boolean;
kittySizeLimit: number;
}
export interface IResetHandler {
@@ -0,0 +1,634 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt } from '../Types';
import { ImageRenderer } from '../ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from '../ImageStorage';
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import {
KittyAction,
KittyFormat,
KittyCompression,
IKittyCommand,
IPendingTransmission,
IKittyImageData,
BYTES_PER_PIXEL_RGB,
BYTES_PER_PIXEL_RGBA,
ALPHA_OPAQUE,
parseKittyCommand
} from './KittyGraphicsTypes';
// Memory limit for base64 decoder (4MB, same as IIPHandler)
const DECODER_KEEP_DATA = 4194304;
const DECODER_INITIAL_DATA = 4194304; // 4MB
// Local mirror of const enum (esbuild can't inline const enums from external packages)
const DECODER_OK: DecodeStatus.OK = 0;
// Maximum control data size
const MAX_CONTROL_DATA_SIZE = 512;
// Semicolon codepoint
const SEMICOLON = 0x3B;
/**
* Kitty graphics protocol handler with streaming base64 decoding.
*/
export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
private _aborted = false;
private _decodeError = false;
private _activeDecoder: Base64Decoder | null = null;
private readonly _maxEncodedBytes: number;
private readonly _initialEncodedBytes: number;
// Streaming related states
/** True while receiving control data (before semicolon). */
private _inControlData = true;
/** Buffer for control data. */
private _controlData = new Uint32Array(MAX_CONTROL_DATA_SIZE);
private _controlLength = 0;
/** Pre-calculated encoded size limit */
private _encodedSizeLimit = 0;
private _totalEncodedSize = 0;
/** Parsed command. These are the control data before semicolon. */
private _parsedCommand: IKittyCommand | null = null;
// Storage related states
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
private _nextImageId = 1;
/** Maps Kitty protocol image ID → ImageStorage internal ID for deletion/lookup. */
private _kittyIdToStorageId: Map<number, number> = new Map();
// TODO: Eliminate double storage — raw image data lives here (as Blob) AND rendered
// ImageBitmaps live in ImageStorage. Currently we only use ImageStorage.addImage(bitmap)
// for tiling + cursor movement + marker-based eviction.
//
// See: https://github.com/xtermjs/xterm.js/pull/5619#issuecomment-3853678815
private _images: Map<number, IKittyImageData> = new Map();
constructor(
private readonly _opts: IImageAddonOptions,
private readonly _renderer: ImageRenderer,
private readonly _storage: ImageStorage,
private readonly _coreTerminal: ITerminalExt
) {
// Convert decoded size limit -> max encoded bytes.
this._maxEncodedBytes = Math.ceil(this._opts.kittySizeLimit * 4 / 3);
// ensure we preallocate more than configured limit while using 4mb initial size.
this._initialEncodedBytes = Math.min(DECODER_INITIAL_DATA, this._maxEncodedBytes);
}
public reset(): void {
for (const pending of this._pendingTransmissions.values()) {
pending.decoder.release();
}
this._pendingTransmissions.clear();
if (this._activeDecoder) {
this._activeDecoder.release();
this._activeDecoder = null;
}
this._images.clear();
this._kittyIdToStorageId.clear();
}
public start(): void {
this._aborted = false;
this._decodeError = false;
this._inControlData = true;
this._controlLength = 0;
this._parsedCommand = null;
// Pre-calculate encoded limit once: base64 is 4 bytes encoded → 3 bytes decoded
this._encodedSizeLimit = this._maxEncodedBytes;
this._totalEncodedSize = 0;
this._activeDecoder = null;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
if (!this._inControlData) {
this._streamPayload(data, start, end);
} else {
// Scan for semicolon
let controlEnd = end;
for (let i = start; i < end; i++) {
if (data[i] === SEMICOLON) {
this._inControlData = false;
controlEnd = i;
break;
}
}
// Copy control data
const copyLength = controlEnd - start;
if (this._controlLength + copyLength > MAX_CONTROL_DATA_SIZE) {
this._aborted = true;
return;
}
this._controlData.set(data.subarray(start, controlEnd), this._controlLength);
this._controlLength += copyLength;
if (!this._inControlData) {
// Found semicolon - parse control data early for validation
this._parsedCommand = parseKittyCommand(this._parseControlDataString());
// Early validation: i+I conflict
if (this._parsedCommand.id !== undefined && this._parsedCommand.imageNumber !== undefined) {
this._sendResponse(this._parsedCommand.id, 'EINVAL:cannot specify both i and I keys', this._parsedCommand.quiet ?? 0);
this._aborted = true;
return;
}
// Delete action doesn't need payload - skip streaming
if (this._parsedCommand.action === KittyAction.DELETE) {
return;
}
// Stream remaining as payload
const payloadStart = controlEnd + 1;
if (payloadStart < end) {
this._streamPayload(data, payloadStart, end);
}
}
}
}
/**
* Stream payload bytes into the base64 decoder.
*/
private _streamPayload(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
// Check size limit (compare encoded bytes against pre-calculated limit)
// Include cumulative size from pending transmission for multi-chunk images
const pendingKey = this._parsedCommand?.id ?? 0;
const pending = this._pendingTransmissions.get(pendingKey);
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
this._totalEncodedSize += end - start;
const cumulativeEncodedSize = previousEncodedSize + this._totalEncodedSize;
if (cumulativeEncodedSize > this._encodedSizeLimit) {
const decoderToRelease = this._activeDecoder ?? pending?.decoder;
if (decoderToRelease) {
decoderToRelease.release();
}
this._activeDecoder = null;
if (pending) {
this._pendingTransmissions.delete(pendingKey);
}
this._aborted = true;
return;
}
if (this._decodeError) return;
if (pending?.decoder && !this._activeDecoder) {
this._activeDecoder = pending.decoder;
}
if (!this._activeDecoder) {
this._activeDecoder = new Base64Decoder(DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
this._activeDecoder.init();
}
if (this._activeDecoder.put(data.subarray(start, end)) !== DECODER_OK) {
this._activeDecoder.release();
this._activeDecoder = null;
this._decodeError = true;
if (pending) {
this._pendingTransmissions.delete(pendingKey);
}
}
}
public end(success: boolean): boolean | Promise<boolean> {
if (this._aborted || !success) {
if (this._activeDecoder) {
this._activeDecoder.release();
this._activeDecoder = null;
}
return true;
}
// No semicolon = no payload (delete, capability query)
if (this._inControlData) {
return this._handleNoPayloadCommand();
}
// Use command parsed early in put() - i+I already validated there
const cmd = this._parsedCommand!;
// Delete action was handled by skipping payload - just execute
if (cmd.action === KittyAction.DELETE) {
return this._handleDelete(cmd);
}
const pendingKey = cmd.id ?? 0;
const isMoreComing = cmd.more === 1;
const pending = this._pendingTransmissions.get(pendingKey);
if (isMoreComing) {
if (this._activeDecoder) {
if (pending) {
pending.totalEncodedSize += this._totalEncodedSize;
pending.decodeError = pending.decodeError || this._decodeError;
} else {
this._pendingTransmissions.set(pendingKey, {
cmd: { ...cmd },
decoder: this._activeDecoder,
totalEncodedSize: this._totalEncodedSize,
decodeError: this._decodeError
});
}
this._activeDecoder = null;
}
return true;
}
let decodeError = this._decodeError;
let finalCmd = cmd;
let decoder = this._activeDecoder;
if (pending) {
finalCmd = pending.cmd;
decoder = pending.decoder;
decodeError = decodeError || pending.decodeError;
this._pendingTransmissions.delete(pendingKey);
}
let imageBytes = new Uint8Array(0);
if (decoder) {
if (decoder.end() !== DECODER_OK) {
decodeError = true;
}
imageBytes = decoder.data8;
}
this._activeDecoder = null;
// Handle command first — handlers create Blob/ImageData from imageBytes,
// which copies the data. Only then is it safe to release the decoder's
// wasm memory that imageBytes points into.
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
if (decoder) {
decoder.release();
}
return result;
}
// Command handling
private _parseControlDataString(): string {
let str = '';
for (let i = 0; i < this._controlLength; i++) {
str += String.fromCodePoint(this._controlData[i]);
}
return str;
}
private _handleNoPayloadCommand(): boolean | Promise<boolean> {
const cmd = parseKittyCommand(this._parseControlDataString());
// Per spec: specifying both i and I is an error
if (cmd.id !== undefined && cmd.imageNumber !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0);
return true;
}
const action = cmd.action ?? 't';
switch (action) {
case KittyAction.DELETE:
return this._handleDelete(cmd);
case KittyAction.QUERY:
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
return true;
default:
return true;
}
}
private _handleCommandWithBytesAndCmd(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
const action = cmd.action ?? 't';
switch (action) {
case KittyAction.TRANSMIT: {
const result = this._handleTransmit(cmd, bytes, decodeError);
if (cmd.id !== undefined && !decodeError && bytes.length > 0) {
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
}
return result;
}
case KittyAction.TRANSMIT_DISPLAY:
return this._handleTransmitDisplay(cmd, bytes, decodeError);
case KittyAction.QUERY:
return this._handleQuery(cmd, bytes, decodeError);
default:
return true;
}
}
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
// Currently only supports direct transmission (t=d, the default).
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
// - t=s Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
// 1. Check cmd.transmission (t key) before treating bytes as image data
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
// 3. For t=d: treat bytes as image data (current behavior)
if (decodeError || bytes.length === 0) return true;
const id = cmd.id ?? this._nextImageId++;
const image: IKittyImageData = {
id,
data: new Blob([bytes as BlobPart]),
width: cmd.width ?? 0,
height: cmd.height ?? 0,
format: (cmd.format ?? KittyFormat.RGBA) as 24 | 32 | 100,
compression: cmd.compression ?? ''
};
this._images.set(id, image);
return true;
}
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
if (decodeError) return true;
const pendingKey = cmd.id ?? 0;
this._handleTransmit(cmd, bytes, decodeError);
// If still accumulating chunks, don't display yet
if (this._pendingTransmissions.has(pendingKey)) return true;
// Display the completed image
const id = cmd.id ?? this._nextImageId - 1;
const image = this._images.get(id);
if (image) {
const result = this._displayImage(image, cmd);
if (cmd.id !== undefined) {
return (result as Promise<boolean>).then(r => {
this._sendResponse(id, 'OK', cmd.quiet ?? 0);
return r;
});
}
return result;
}
return true;
}
private _handleQuery(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
const id = cmd.id ?? 0;
const quiet = cmd.quiet ?? 0;
// Check decode error first (invalid base64)
if (decodeError) {
this._sendResponse(id, 'EINVAL:invalid base64 data', quiet);
return true;
}
// Capability query (no payload) - just respond OK
if (bytes.length === 0) {
this._sendResponse(id, 'OK', quiet);
return true;
}
const format = cmd.format ?? KittyFormat.RGBA;
if (format === KittyFormat.PNG) {
this._sendResponse(id, 'OK', quiet);
} else {
const width = cmd.width ?? 0;
const height = cmd.height ?? 0;
if (!width || !height) {
this._sendResponse(id, 'EINVAL:width and height required for raw pixel data', quiet);
return true;
}
const bytesPerPixel = format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
const expectedBytes = width * height * bytesPerPixel;
if (bytes.length < expectedBytes) {
this._sendResponse(id, `EINVAL:insufficient pixel data`, quiet);
return true;
}
this._sendResponse(id, 'OK', quiet);
}
return true;
}
private _handleDelete(cmd: IKittyCommand): boolean {
const id = cmd.id;
if (id !== undefined) {
// Abort in-flight chunked upload for this specific image
const pending = this._pendingTransmissions.get(id);
if (pending) {
pending.decoder.release();
this._pendingTransmissions.delete(id);
}
this._images.delete(id);
const storageId = this._kittyIdToStorageId.get(id);
if (storageId !== undefined) {
this._storage.deleteImage(storageId);
this._kittyIdToStorageId.delete(id);
}
} else {
// Abort all in-flight chunked uploads
for (const pending of this._pendingTransmissions.values()) {
pending.decoder.release();
}
this._pendingTransmissions.clear();
this._images.clear();
for (const storageId of this._kittyIdToStorageId.values()) {
this._storage.deleteImage(storageId);
}
this._kittyIdToStorageId.clear();
}
return true;
}
private _sendResponse(id: number, message: string, quiet: number): void {
const isOk = message === 'OK';
if (isOk && quiet === 1) return;
if (!isOk && quiet === 2) return;
const response = `\x1b_Gi=${id};${message}\x1b\\`;
this._coreTerminal._core.coreService.triggerDataEvent(response);
}
// Image display
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): boolean | Promise<boolean> {
return this._decodeAndDisplay(image, cmd)
.then(() => true)
.catch(() => true);
}
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
const bitmap = await this._createBitmap(image);
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
// Per spec: c/r default to image's natural cell dimensions
const imgCols = cmd.columns ?? Math.ceil(bitmap.width / cw);
const imgRows = cmd.rows ?? Math.ceil(bitmap.height / ch);
let w = bitmap.width;
let h = bitmap.height;
// Scale bitmap to fit placement rectangle when c/r are specified
if (cmd.columns !== undefined || cmd.rows !== undefined) {
w = Math.round(imgCols * cw);
h = Math.round(imgRows * ch);
}
if (w * h > this._opts.pixelLimit) return;
// Save cursor position before addImage modifies it
const buffer = this._coreTerminal._core.buffer;
const savedX = buffer.x;
const savedY = buffer.y;
const savedYbase = buffer.ybase;
let storageId: number;
if (w !== bitmap.width || h !== bitmap.height) {
const resized = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
storageId = this._storage.addImage(resized);
} else {
storageId = this._storage.addImage(bitmap);
}
this._kittyIdToStorageId.set(image.id, storageId);
// Kitty cursor movement
// Per spec: cursor placed at first column after last image column,
// on the last row of the image. C=1 means don't move cursor.
if (cmd.cursorMovement === 1) {
// C=1: restore cursor to position before image was placed
const scrolled = buffer.ybase - savedYbase;
buffer.x = savedX;
// Can't restore cursor to scrollback?
buffer.y = Math.max(savedY - scrolled, 0);
} else {
// Default (C=0): advance cursor horizontally past the image
// addImage already positioned cursor on the last row via lineFeeds
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
}
}
/**
* Create ImageBitmap from already-decoded image data.
*/
private async _createBitmap(image: IKittyImageData): Promise<ImageBitmap> {
let bytes: Uint8Array = new Uint8Array(await image.data.arrayBuffer());
if (image.compression === KittyCompression.ZLIB) {
bytes = await this._decompressZlib(bytes);
}
if (image.format === KittyFormat.PNG) {
const blob = new Blob([bytes as BlobPart], { type: 'image/png' });
if (!window.createImageBitmap) {
const url = URL.createObjectURL(blob);
const img = new Image();
return new Promise<ImageBitmap>((resolve, reject) => {
img.addEventListener('load', () => {
URL.revokeObjectURL(url);
const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
canvas.getContext('2d')?.drawImage(img, 0, 0);
createImageBitmap(canvas).then(resolve).catch(reject);
});
img.addEventListener('error', reject);
img.src = url;
});
}
return createImageBitmap(blob);
}
// Raw pixel data
const width = image.width;
const height = image.height;
if (!width || !height) {
throw new Error('Width and height required for raw pixel data');
}
const bytesPerPixel = image.format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
const expectedBytes = width * height * bytesPerPixel;
if (bytes.length < expectedBytes) {
throw new Error('Insufficient pixel data');
}
const pixelCount = width * height;
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
const isRgba = image.format === KittyFormat.RGBA;
let srcOffset = 0;
let dstOffset = 0;
for (let i = 0; i < pixelCount; i++) {
data[dstOffset] = bytes[srcOffset];
data[dstOffset + 1] = bytes[srcOffset + 1];
data[dstOffset + 2] = bytes[srcOffset + 2];
data[dstOffset + 3] = isRgba ? bytes[srcOffset + 3] : ALPHA_OPAQUE;
srcOffset += bytesPerPixel;
dstOffset += BYTES_PER_PIXEL_RGBA;
}
return createImageBitmap(new ImageData(data, width, height));
}
private async _decompressZlib(compressed: Uint8Array): Promise<Uint8Array> {
try {
return await this._decompress(compressed, 'deflate');
} catch {
return await this._decompress(compressed, 'deflate-raw');
}
}
private async _decompress(compressed: Uint8Array, format: 'deflate' | 'deflate-raw'): Promise<Uint8Array> {
const ds = new DecompressionStream(format);
const writer = ds.writable.getWriter();
writer.write(compressed as BufferSource);
writer.close();
const chunks: Uint8Array[] = [];
const reader = ds.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
public get images(): ReadonlyMap<number, IKittyImageData> {
return this._images;
}
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
return this._pendingTransmissions;
}
}
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { parseKittyCommand, KittyAction, KittyFormat } from './KittyGraphicsTypes';
describe('KittyGraphicsTypes', () => {
describe('parseKittyCommand', () => {
it('should parse control data with action and format', () => {
const cmd = parseKittyCommand('a=T,f=100');
assert.strictEqual(cmd.action, 'T');
assert.strictEqual(cmd.format, 100);
});
it('should parse control data with all options', () => {
const cmd = parseKittyCommand('a=t,f=32,i=5,s=10,v=20,c=3,r=2,m=1,q=2');
assert.strictEqual(cmd.action, 't');
assert.strictEqual(cmd.format, 32);
assert.strictEqual(cmd.id, 5);
assert.strictEqual(cmd.width, 10);
assert.strictEqual(cmd.height, 20);
assert.strictEqual(cmd.columns, 3);
assert.strictEqual(cmd.rows, 2);
assert.strictEqual(cmd.more, 1);
assert.strictEqual(cmd.quiet, 2);
});
it('should handle empty control data', () => {
const cmd = parseKittyCommand('');
assert.strictEqual(cmd.action, undefined);
assert.strictEqual(cmd.format, undefined);
});
it('should parse transmit action', () => {
const cmd = parseKittyCommand('a=t,f=100');
assert.strictEqual(cmd.action, KittyAction.TRANSMIT);
assert.strictEqual(cmd.format, KittyFormat.PNG);
});
it('should parse delete action', () => {
const cmd = parseKittyCommand('a=d,i=5');
assert.strictEqual(cmd.action, KittyAction.DELETE);
assert.strictEqual(cmd.id, 5);
});
it('should parse empty action as empty string', () => {
const cmd = parseKittyCommand('a=,f=100');
assert.strictEqual(cmd.action, '');
assert.strictEqual(cmd.format, 100);
});
it('should leave action undefined when key is not present', () => {
const cmd = parseKittyCommand('f=100,i=5');
assert.strictEqual(cmd.action, undefined);
assert.strictEqual(cmd.format, 100);
assert.strictEqual(cmd.id, 5);
});
it('should parse compression key', () => {
const cmd = parseKittyCommand('a=t,f=32,o=z');
assert.strictEqual(cmd.action, 't');
assert.strictEqual(cmd.format, 32);
assert.strictEqual(cmd.compression, 'z');
});
it('should parse cursor movement key', () => {
const cmd = parseKittyCommand('a=T,f=100,C=1');
assert.strictEqual(cmd.cursorMovement, 1);
});
it('should parse cursor movement key C=0', () => {
const cmd = parseKittyCommand('a=T,f=100,C=0');
assert.strictEqual(cmd.cursorMovement, 0);
});
it('should parse x and y offset', () => {
const cmd = parseKittyCommand('a=T,x=10,y=20');
assert.strictEqual(cmd.x, 10);
assert.strictEqual(cmd.y, 20);
});
it('should handle keys without values', () => {
const cmd = parseKittyCommand('a=t,f=,i=5');
assert.strictEqual(cmd.action, 't');
assert.ok(isNaN(cmd.format!));
assert.strictEqual(cmd.id, 5);
});
});
});
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*
* Kitty graphics protocol types, constants, and parsing utilities.
*/
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
/**
* Kitty graphics protocol action types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
*/
export const enum KittyAction {
TRANSMIT = 't',
TRANSMIT_DISPLAY = 'T',
QUERY = 'q',
PLACEMENT = 'p',
DELETE = 'd'
}
/**
* Kitty graphics protocol format types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
*/
export const enum KittyFormat {
RGB = 24,
RGBA = 32,
PNG = 100
}
/**
* Kitty graphics protocol compression types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
*/
export const enum KittyCompression {
NONE = '',
ZLIB = 'z'
}
/**
* Kitty graphics protocol control data keys.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
*/
export const enum KittyKey {
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
ACTION = 'a',
// Image format (24=RGB, 32=RGBA, 100=PNG)
FORMAT = 'f',
// Image ID for referencing stored images
ID = 'i',
// Image number (alternative to ID, terminal assigns ID)
IMAGE_NUMBER = 'I',
// Source image width in pixels
WIDTH = 's',
// Source image height in pixels
HEIGHT = 'v',
// The left edge (in pixels) of the image area to display
X_OFFSET = 'x',
// The top edge (in pixels) of the image area to display
Y_OFFSET = 'y',
// Number of terminal columns to display the image over
COLUMNS = 'c',
// Number of terminal rows to display the image over
ROWS = 'r',
// More data flag (1=more chunks coming, 0=final chunk)
MORE = 'm',
// Compression type (z=zlib). This is essential for chunking larger images.
COMPRESSION = 'o',
// Quiet mode (1=suppress OK responses, 2=suppress error responses)
QUIET = 'q',
// Cursor movement policy (0=move cursor after image, 1=don't move cursor)
CURSOR_MOVEMENT = 'C'
}
// Pixel format constants
export const BYTES_PER_PIXEL_RGB = 3;
export const BYTES_PER_PIXEL_RGBA = 4;
export const ALPHA_OPAQUE = 255;
/**
* Parsed Kitty graphics command.
*/
export interface IKittyCommand {
action?: string;
format?: number;
id?: number;
imageNumber?: number;
width?: number;
height?: number;
x?: number;
y?: number;
columns?: number;
rows?: number;
more?: number;
quiet?: number;
cursorMovement?: number;
compression?: string;
payload?: string;
}
/**
* Pending chunked transmission state.
* Stores metadata from the first chunk while accumulating decoded payload data.
*/
export interface IPendingTransmission {
/** The parsed command from the first chunk (contains action, format, dimensions, etc.) */
cmd: IKittyCommand;
/** Decoder used across chunked payloads */
decoder: Base64Decoder;
/** Total encoded (base64) bytes received across all chunks - for size limit enforcement */
totalEncodedSize: number;
/** Whether any chunk has failed to decode */
decodeError: boolean;
}
/**
* Stored Kitty image data.
*/
export interface IKittyImageData {
id: number;
/** Decoded image data stored as Blob (off JS heap) to avoid 2GB heap limit */
data: Blob;
width: number;
height: number;
format: 24 | 32 | 100;
compression?: string;
}
/**
* Parses Kitty graphics control data into a command object.
*/
export function parseKittyCommand(data: string): IKittyCommand {
const cmd: IKittyCommand = {};
const parts = data.split(',');
for (const part of parts) {
const eqIdx = part.indexOf('=');
if (eqIdx === -1) continue;
const key = part.substring(0, eqIdx);
const value = part.substring(eqIdx + 1);
// Handle string keys first
if (key === KittyKey.ACTION) {
cmd.action = value;
continue;
}
if (key === KittyKey.COMPRESSION) {
cmd.compression = value;
continue;
}
const numValue = parseInt(value);
switch (key) {
case KittyKey.FORMAT: cmd.format = numValue; break;
case KittyKey.ID: cmd.id = numValue; break;
case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break;
case KittyKey.WIDTH: cmd.width = numValue; break;
case KittyKey.HEIGHT: cmd.height = numValue; break;
case KittyKey.X_OFFSET: cmd.x = numValue; break;
case KittyKey.Y_OFFSET: cmd.y = numValue; break;
case KittyKey.COLUMNS: cmd.columns = numValue; break;
case KittyKey.ROWS: cmd.rows = numValue; break;
case KittyKey.MORE: cmd.more = numValue; break;
case KittyKey.QUIET: cmd.quiet = numValue; break;
case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
}
}
return cmd;
}
+9 -3
View File
@@ -23,6 +23,8 @@ export interface IImageAddonOptions {
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
kittySupport: boolean;
kittySizeLimit: number;
}
// eslint-disable-next-line
@@ -134,7 +136,9 @@ test.describe('ImageAddon', () => {
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
iipSizeLimit: 20000000,
kittySupport: true,
kittySizeLimit: 20000000
};
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
});
@@ -149,7 +153,9 @@ test.describe('ImageAddon', () => {
storageLimit: 10,
showPlaceholder: false,
iipSupport: false,
iipSizeLimit: 1000
iipSizeLimit: 1000,
kittySupport: false,
kittySizeLimit: 1000
};
await ctx.page.evaluate(opts => {
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
@@ -300,7 +306,7 @@ test.describe('ImageAddon', () => {
* terminal access helpers.
*/
async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await ctx.page.evaluate(`term._core._renderService.dimensions`);
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
return {
cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.css.cell.height),
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+9
View File
@@ -76,6 +76,15 @@ declare module '@xterm/addon-image' {
iipSupport?: boolean;
/** IIP sequence size limit (default 20000000 bytes). */
iipSizeLimit?: number;
/**
* Kitty graphics protocol settings
*/
/** Whether Kitty graphics protocol is enabled (default is true). */
kittySupport?: boolean;
/** Kitty image size limit in bytes (default 20000000 bytes). */
kittySizeLimit?: number;
}
export class ImageAddon implements ITerminalAddon {
-1
View File
@@ -3,7 +3,6 @@ node_modules/
coverage/
lib/
fonts/
.env
.vscode/
+27 -3
View File
@@ -1,6 +1,30 @@
MIT License
Copyright (c) 2018
Copyright (c) 2019, 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.
---
The code that analyzes font ligatures is forked from https://github.com/princjef/font-ligatures with this license:
MIT License
Copyright (c) 2018 Jeffrey Principe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -1,78 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
const fs = require('fs');
const path = require('path');
const util = require('util');
const axios = require('axios').default;
const mkdirp = require('mkdirp');
const yauzl = require('yauzl');
const urls = {
fira: 'https://github.com/tonsky/FiraCode/raw/d42e7276fa925e5f82748f3ec9ea429736611b48/distr/otf/FiraCode-Regular.otf',
iosevka: 'https://github.com/be5invis/Iosevka/releases/download/v1.14.3/01-iosevka-1.14.3.zip'
};
const writeFile = util.promisify(fs.writeFile);
const fontsFolder = path.join(__dirname, '../fonts');
async function download() {
await mkdirp(fontsFolder);
try {
await downloadFiraCode();
await downloadIosevka();
console.log('Loaded all fonts for testing')
} catch (e) {
console.warn('Fonts failed to download, ligature tests will not work', e);
}
}
async function downloadFiraCode() {
const file = path.join(fontsFolder, 'firaCode.otf');
if (await util.promisify(fs.exists)(file)) {
console.log('Fira Code already loaded');
} else {
console.log('Downloading Fira Code...');
await writeFile(
file,
(await axios.get(urls.fira, { responseType: 'arraybuffer' })).data
);
}
}
async function downloadIosevka() {
const file = path.join(fontsFolder, 'iosevka.ttf');
if (await util.promisify(fs.exists)(file)) {
console.log('Iosevka already loaded');
} else {
console.log('Downloading Iosevka...');
const iosevkaContents = (await axios.get(urls.iosevka, { responseType: 'arraybuffer' })).data;
const iosevkaZipfile = await util.promisify(yauzl.fromBuffer)(iosevkaContents);
await new Promise((resolve, reject) => {
iosevkaZipfile.on('entry', entry => {
if (entry.fileName === 'ttf/iosevka-regular.ttf') {
iosevkaZipfile.openReadStream(entry, (err, stream) => {
if (err) {
return reject(err);
}
const writeStream = fs.createWriteStream(file);
stream.pipe(writeStream);
writeStream.on('close', () => resolve());
});
}
});
});
}
}
download();
process.on('unhandledRejection', e => {
console.error(e);
process.exit(1);
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-661
View File
@@ -1,661 +0,0 @@
{
"name": "@xterm/addon-ligatures",
"version": "0.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xterm/addon-ligatures",
"version": "0.9.0",
"license": "MIT",
"dependencies": {
"font-finder": "^1.1.0",
"font-ligatures": "^1.4.1"
},
"devDependencies": {
"@types/sinon": "^5.0.1",
"axios": "^1.6.0",
"mkdirp": "0.5.5",
"sinon": "6.3.5",
"yauzl": "^2.10.0"
},
"engines": {
"node": ">8.0.0"
}
},
"node_modules/@sinonjs/commons": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz",
"integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/formatio": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz",
"integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1",
"@sinonjs/samsam": "^3.1.0"
}
},
"node_modules/@sinonjs/formatio/node_modules/@sinonjs/samsam": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz",
"integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1.3.0",
"array-from": "^2.1.1",
"lodash": "^4.17.15"
}
},
"node_modules/@sinonjs/samsam": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz",
"integrity": "sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true,
"license": "(Unlicense OR Apache-2.0)"
},
"node_modules/@types/sinon": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.1.tgz",
"integrity": "sha512-yxzBCIjE3lp9lYjfBbIK/LRCoXgCLLbIIBIje7eNCcUIIR2CZZtyX5uto2hVoMSMqLrsRrT6mwwUEd0yFgOwpA==",
"dev": true,
"license": "MIT"
},
"node_modules/array-from": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==",
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/font-finder": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz",
"integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==",
"license": "MIT",
"dependencies": {
"get-system-fonts": "^2.0.0",
"promise-stream-reader": "^1.0.1"
},
"engines": {
"node": ">8.0.0"
}
},
"node_modules/font-ligatures": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz",
"integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==",
"license": "MIT",
"dependencies": {
"font-finder": "^1.0.3",
"lru-cache": "^6.0.0",
"opentype.js": "^0.8.0"
},
"engines": {
"node": ">8.0.0"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-system-fonts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.0.tgz",
"integrity": "sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==",
"license": "MIT",
"engines": {
"node": ">8.0.0"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true,
"license": "MIT"
},
"node_modules/just-extend": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
"integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"dev": true,
"license": "MIT"
},
"node_modules/lolex": {
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/nise": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz",
"integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/formatio": "^3.2.1",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"lolex": "^5.0.1",
"path-to-regexp": "^1.7.0"
}
},
"node_modules/nise/node_modules/lolex": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz",
"integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1.7.0"
}
},
"node_modules/opentype.js": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz",
"integrity": "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==",
"license": "MIT",
"dependencies": {
"tiny-inflate": "^1.0.2"
},
"bin": {
"ot": "bin/ot"
}
},
"node_modules/path-to-regexp": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
"integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
"node_modules/promise-stream-reader": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz",
"integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==",
"license": "MIT",
"engines": {
"node": ">8.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/sinon": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz",
"integrity": "sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==",
"deprecated": "16.1.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^1.0.2",
"@sinonjs/formatio": "^3.0.0",
"@sinonjs/samsam": "^2.1.2",
"diff": "^3.5.0",
"lodash.get": "^4.4.2",
"lolex": "^2.7.5",
"nise": "^1.4.5",
"supports-color": "^5.5.0",
"type-detect": "^4.0.8"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz",
"integrity": "sha512-fEndoHfuiSlXtvG0EA1gB4uAdy2dbQ3WfOtQw1/Y8NQ9rxtGbGkENwfGmN0wK7OkxDz1lgc5Ei2LQ0oxKDUcVw==",
"license": "MIT"
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}
+5 -5
View File
@@ -14,7 +14,6 @@
"node": ">8.0.0"
},
"scripts": {
"prepare": "node bin/download-fonts.js",
"build": "tsc -p src",
"watch": "tsc -w -p src",
"prepackage": "npm run build",
@@ -32,14 +31,15 @@
],
"license": "MIT",
"dependencies": {
"font-finder": "^1.1.0",
"font-ligatures": "^1.4.1"
"lru-cache": "^6.0.0",
"opentype.js": "^0.8.0"
},
"devDependencies": {
"@types/sinon": "^5.0.1",
"@types/lru-cache": "^5.1.0",
"@types/opentype.js": "^0.7.0",
"axios": "^1.6.0",
"font-finder": "^1.1.0",
"mkdirp": "0.5.5",
"sinon": "6.3.5",
"yauzl": "^2.10.0"
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ export interface ITerminalAddon {
dispose(): void;
}
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
export class LigaturesAddon implements ITerminalAddon, ILigaturesApi {
private readonly _fallbackLigatures: string[];
private readonly _fontFeatureSettings?: string;
@@ -22,7 +22,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
constructor(options?: Partial<ILigatureOptions>) {
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
this._fallbackLigatures = (options?.fallbackLigatures || [
this._fallbackLigatures = (options?.fallbackLigatures ?? [
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
+12 -10
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Font, loadBuffer } from 'font-ligatures';
import { Font, loadBuffer } from './fontLigatures/index';
import parse from './parse';
@@ -42,11 +42,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
if (status && status.state !== 'granted') {
throw new Error('Permission to access local fonts not granted.');
}
} catch (err: any) {
} catch (err: unknown) {
// A `TypeError` indicates the 'local-fonts'
// permission is not yet implemented, so
// only `throw` if this is _not_ the problem.
if (err.name !== 'TypeError') {
if (err instanceof Error && err.name !== 'TypeError') {
throw err;
}
}
@@ -60,8 +60,10 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err: any) {
console.error(err.name, err.message);
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.name, err.message);
}
}
}
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
@@ -76,13 +78,13 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err: any) {
console.error(err.name, err.message);
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.name, err.message);
}
}
}
if (!fontsPromise) {
fontsPromise = Promise.resolve({});
}
fontsPromise ??= Promise.resolve({});
}
const fonts = await fontsPromise;
@@ -0,0 +1,40 @@
import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types';
export default function flatten(tree: ILookupTree, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry> = new Map()): IFlattenedLookupTree {
const result: IFlattenedLookupTree = {};
for (const [glyphId, entry] of Object.entries(tree.individual)) {
result[glyphId] = flattenEntry(entry, visited);
}
for (const { range, entry } of tree.range) {
const flattened = flattenEntry(entry, visited);
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
result[glyphId] = flattened;
}
}
return result;
}
function flattenEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry>): IFlattenedLookupTreeEntry {
if (visited.has(entry)) {
return visited.get(entry)!;
}
const result: IFlattenedLookupTreeEntry = {};
visited.set(entry, result);
if (entry.forward) {
result.forward = flatten(entry.forward, visited);
}
if (entry.reverse) {
result.reverse = flatten(entry.reverse, visited);
}
if (entry.lookup) {
result.lookup = entry.lookup;
}
return result;
}
@@ -0,0 +1,358 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as path from 'path';
import * as fs from 'fs';
import { assert } from 'chai';
import { loadBuffer } from './index';
interface IFont {
findLigatures(text: string): { outputGlyphs: number[], contextRanges: [number, number][] };
findLigatureRanges(text: string): [number, number][];
}
interface ITestCase {
font: string;
input: string;
glyphs: number[];
ranges: [number, number][];
}
const fira = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
({ font: 'Fira Code', input, glyphs, ranges });
const iosevka = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
({ font: 'Iosevka', input, glyphs, ranges });
const monoid = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
({ font: 'Monoid', input, glyphs, ranges });
const ubuntu = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
({ font: 'Ubuntu Mono', input, glyphs, ranges });
const firaCases: ITestCase[] = [
fira('abc', [133, 145, 146], []),
fira('.=', [1614, 1081], [[0, 2]]),
fira('..=', [1614, 1614, 1083], [[0, 3]]),
fira('.-', [1614, 1080], [[0, 2]]),
fira(':=', [1614, 1055], [[0, 2]]),
fira('=:=', [1614, 1614, 1483], [[0, 3]]),
fira('=!=', [1614, 1614, 1484], [[0, 3]]),
fira('__', [1614, 1099], [[0, 2]]),
fira('==', [1614, 1485], [[0, 2]]),
fira('!=', [1614, 1058], [[0, 2]]),
fira('===', [1614, 1614, 1486], [[0, 3]]),
fira('!==', [1614, 1614, 1059], [[0, 3]]),
fira('=/=', [1614, 1614, 1491], [[0, 3]]),
fira('<-<', [1614, 1614, 1513], [[0, 3]]),
fira('<<-', [1614, 1614, 1522], [[0, 3]]),
fira('<--', [1614, 1614, 1511], [[0, 3]]),
fira('<-', [1614, 1510], [[0, 2]]),
fira('<->', [1614, 1614, 1512], [[0, 3]]),
fira('->', [1614, 1064], [[0, 2]]),
fira('-->', [1614, 1614, 1063], [[0, 3]]),
fira('->>', [1614, 1614, 1065], [[0, 3]]),
fira('>->', [1614, 1614, 1493], [[0, 3]]),
fira('<=<', [1614, 1614, 1519], [[0, 3]]),
fira('<<=', [1614, 1614, 1523], [[0, 3]]),
fira('<==', [1614, 1614, 1517], [[0, 3]]),
fira('<=>', [1614, 1614, 1518], [[0, 3]]),
fira('=>', [1614, 1488], [[0, 2]]),
fira('==>', [1614, 1614, 1487], [[0, 3]]),
fira('=>>', [1614, 1614, 1489], [[0, 3]]),
fira('>=>', [1614, 1614, 1495], [[0, 3]]),
fira('>>=', [1614, 1614, 1498], [[0, 3]]),
fira('>>-', [1614, 1614, 1497], [[0, 3]]),
fira('>-', [1614, 1492], [[0, 2]]),
fira('<~>', [1614, 1614, 1526], [[0, 3]]),
fira('-<', [1614, 1066], [[0, 2]]),
fira('-<<', [1614, 1614, 1067], [[0, 3]]),
fira('=<<', [1614, 1614, 1490], [[0, 3]]),
fira('<~~', [1614, 1614, 1527], [[0, 3]]),
fira('<~', [1614, 1525], [[0, 2]]),
fira('~~', [1614, 1534], [[0, 2]]),
fira('~>', [1614, 1533], [[0, 2]]),
fira('~~>', [1614, 1614, 1535], [[0, 3]]),
fira('<<<', [1614, 1614, 1524], [[0, 3]]),
fira('<<', [1614, 1521], [[0, 2]]),
fira('<=', [1614, 1516], [[0, 2]]),
fira('<>', [1614, 1520], [[0, 2]]),
fira('>=', [1614, 1494], [[0, 2]]),
fira('>>', [1614, 1496], [[0, 2]]),
fira('>>>', [1614, 1614, 1499], [[0, 3]]),
fira('{.', [1001, 977], [[0, 2]]),
fira('{|', [1614, 1049], [[0, 2]]),
fira('[|', [1614, 1050], [[0, 2]]),
fira('<:', [1614, 1506], [[0, 2]]),
fira(':>', [1614, 1056], [[0, 2]]),
fira('|]', [1614, 1474], [[0, 2]]),
fira('|}', [1614, 1473], [[0, 2]]),
fira('.}', [977, 1002], [[0, 2]]),
fira('<|||', [1614, 1614, 1614, 1504], [[0, 4]]),
fira('<||', [1614, 1614, 1503], [[0, 3]]),
fira('<|', [1614, 1502], [[0, 2]]),
fira('<|>', [1614, 1614, 1505], [[0, 3]]),
fira('|>', [1614, 1477], [[0, 2]]),
fira('||>', [1614, 1614, 1472], [[0, 3]]),
fira('|||>', [1614, 1614, 1614, 1470], [[0, 4]]),
fira('<$', [1614, 1507], [[0, 2]]),
fira('<$>', [1614, 1614, 1508], [[0, 3]]),
fira('$>', [1614, 1479], [[0, 2]]),
fira('<+', [1614, 1514], [[0, 2]]),
fira('<+>', [1614, 1614, 1515], [[0, 3]]),
fira('+>', [1614, 1482], [[0, 2]]),
fira('<*', [1614, 1500], [[0, 2]]),
fira('<*>', [1614, 1614, 1501], [[0, 3]]),
fira('*>', [1614, 1047], [[0, 2]]),
fira('/*', [1614, 1092], [[0, 2]]),
fira('*/', [1614, 1048], [[0, 2]]),
fira('///', [1614, 1614, 1097], [[0, 3]]),
fira('//', [1614, 1096], [[0, 2]]),
fira('</', [1614, 1528], [[0, 2]]),
fira('<!--', [1614, 1614, 1614, 1509], [[0, 4]]),
fira('</>', [1614, 1614, 1529], [[0, 3]]),
fira('/>', [1614, 1095], [[0, 2]]),
fira('0xff', [895, 270, 166, 166], [[0, 3]]),
fira('10x10', [896, 895, 270, 896, 895], [[1, 4]]),
fira('9:45', [904, 998, 899, 900], [[0, 2]]),
fira('[:]', [1003, 998, 1004], [[0, 2]]),
fira(';;', [1614, 1091], [[0, 2]]),
fira('::', [1614, 1052], [[0, 2]]),
fira(':::', [1614, 1614, 1053], [[0, 3]]),
fira('..', [1614, 1082], [[0, 2]]),
fira('...', [1614, 1614, 1085], [[0, 3]]),
fira('..<', [1614, 1614, 1084], [[0, 3]]),
fira('!!', [1614, 1057], [[0, 2]]),
fira('??', [1614, 1090], [[0, 2]]),
fira('%%', [1614, 1536], [[0, 2]]),
fira('&&', [1614, 1468], [[0, 2]]),
fira('||', [1614, 1469], [[0, 2]]),
fira('?.', [1614, 1089], [[0, 2]]),
fira('?:', [1614, 1087], [[0, 2]]),
fira('++', [1614, 1480], [[0, 2]]),
fira('+++', [1614, 1614, 1481], [[0, 3]]),
fira('--', [1614, 1061], [[0, 2]]),
fira('---', [1614, 1614, 1062], [[0, 3]]),
fira('**', [1614, 1045], [[0, 2]]),
fira('***', [1614, 1614, 1046], [[0, 3]]),
fira('~=', [1614, 1532], [[0, 2]]),
fira('~-', [1614, 1531], [[0, 2]]),
fira('www', [1614, 1614, 271], [[0, 3]]),
fira('-~', [1614, 1068], [[0, 2]]),
fira('~@', [1614, 1530], [[0, 2]]),
fira('^=', [1614, 1478], [[0, 2]]),
fira('?=', [1614, 1088], [[0, 2]]),
fira('/=', [1614, 1093], [[0, 2]]),
fira('/==', [1614, 1614, 1094], [[0, 3]]),
fira('-|', [1614, 1060], [[0, 2]]),
fira('_|_', [1614, 1614, 1098], [[0, 3]]),
fira('|-', [1614, 1475], [[0, 2]]),
fira('|=', [1614, 1476], [[0, 2]]),
fira('||=', [1614, 1614, 1471], [[0, 3]]),
fira('#!', [1614, 1071], [[0, 2]]),
fira('#=', [1614, 1075], [[0, 2]]),
fira('##', [1614, 1072], [[0, 2]]),
fira('###', [1614, 1614, 1073], [[0, 3]]),
fira('####', [1614, 1614, 1614, 1074], [[0, 4]]),
fira('#{', [1614, 1069], [[0, 2]]),
fira('#[', [1614, 1070], [[0, 2]]),
fira(']#', [1614, 1051], [[0, 2]]),
fira('#(', [1614, 1076], [[0, 2]]),
fira('#?', [1614, 1077], [[0, 2]]),
fira('#_', [1614, 1078], [[0, 2]]),
fira('#_(', [1614, 1614, 1079], [[0, 3]]),
fira('::=', [1614, 1614, 1054], [[0, 3]]),
fira('.?', [1614, 1086], [[0, 2]]),
fira('===>', [1614, 1614, 1486, 1148], [[0, 4]])
];
const iosevkaCases: ITestCase[] = [
iosevka('<-', [31, 3127], [[0, 2]]),
iosevka('<--', [31, 3129, 3139], [[0, 3]]),
iosevka('<---', [31, 3129, 3150, 3139], [[0, 4]]),
iosevka('<-----', [31, 3129, 3150, 3139, 3151, 3151], [[0, 6]]),
iosevka('->', [3126, 33], [[0, 2]]),
iosevka('-->', [3140, 3128, 33], [[0, 3]]),
iosevka('--->', [3140, 3150, 3128, 33], [[0, 4]]),
iosevka('----->', [3153, 3153, 3140, 3150, 3128, 33], [[0, 6]]),
iosevka('<->', [31, 3149, 33], [[0, 3]]),
iosevka('<-->', [31, 3129, 3128, 33], [[0, 4]]),
iosevka('<--->', [31, 3129, 3150, 3128, 33], [[0, 5]]),
iosevka('<----->', [31, 3129, 3150, 3150, 3150, 3128, 33], [[0, 7]]),
iosevka('<=', [3094, 3095], [[0, 2]]),
iosevka('<==', [31, 3158, 3168], [[0, 3]]),
iosevka('<===', [31, 3158, 3179, 3168], [[0, 4]]),
iosevka('<=====', [31, 3158, 3179, 3168, 3180, 3180], [[0, 6]]),
iosevka('=>', [3155, 33], [[0, 2]]),
iosevka('==>', [3169, 3157, 33], [[0, 3]]),
iosevka('===>', [3169, 3179, 3157, 33], [[0, 4]]),
iosevka('=====>', [3182, 3182, 3169, 3179, 3157, 33], [[0, 6]]),
iosevka('<=>', [31, 3178, 33], [[0, 3]]),
iosevka('<==>', [31, 3158, 3157, 33], [[0, 4]]),
iosevka('<===>', [31, 3158, 3179, 3157, 33], [[0, 5]]),
iosevka('<=====>', [31, 3158, 3179, 3179, 3179, 3157, 33], [[0, 7]]),
iosevka('<!--', [31, 3184, 3130, 3139], [[0, 4]]),
iosevka('<!---', [31, 3184, 3130, 3150, 3139], [[0, 5]]),
iosevka('<!-----', [31, 3184, 3130, 3150, 3139, 3151, 3151], [[0, 7]]),
iosevka('a:b', [68, 29, 69], []),
iosevka('a::b', [68, 3012, 3013, 69], [[1, 3]]),
iosevka('a:::b', [68, 3012, 3010, 3013, 69], [[1, 4]]),
iosevka(':=', [3011, 32], [[0, 2]]),
iosevka(':-', [3011, 16], [[0, 2]]),
iosevka(':+', [3011, 14], [[0, 2]]),
iosevka('=:', [32, 3011], [[0, 2]]),
iosevka('-:', [16, 3011], [[0, 2]]),
iosevka('+:', [14, 3011], [[0, 2]]),
iosevka('<*', [31, 3023], [[0, 2]]),
iosevka('*>', [3023, 33], [[0, 2]]),
iosevka('<*>', [31, 3023, 33], [[1, 3]]),
iosevka('<**>', [31, 3023, 3023, 33], [[1, 4]]),
iosevka('<****>', [31, 3023, 3023, 3023, 3023, 33], [[0, 3], [3, 6]]),
iosevka('==', [3083, 3084], [[0, 2]]),
iosevka('!=', [3098, 3084], [[0, 2]]),
iosevka('===', [3083, 3085, 3084], [[0, 3]]),
iosevka('!==', [3099, 3085, 3084], [[0, 3]]),
iosevka('====', [3083, 3085, 3085, 3084], [[0, 4]]),
iosevka('!===', [3100, 3085, 3085, 3084], [[0, 4]])
];
const monoidCases: ITestCase[] = [
monoid('<!--', [775, 775, 775, 643], [[0, 4]]),
monoid('-->', [779, 779, 628], [[0, 3]]),
monoid('<--', [776, 776, 627], [[0, 3]]),
monoid('->>', [780, 780, 626], [[0, 3]]),
monoid('<<-', [777, 777, 625], [[0, 3]]),
monoid('->', [781, 623], [[0, 2]]),
monoid('<-', [778, 624], [[0, 2]]),
monoid('=>', [793, 666], [[0, 2]]),
monoid('<=>', [785, 785, 760], [[0, 3]]),
monoid('<==>', [786, 786, 786, 771], [[0, 4]]),
monoid('==>', [787, 787, 672], [[0, 3]]),
monoid('<==', [788, 788, 671], [[0, 3]]),
monoid('>>=', [791, 791, 758], [[0, 3]]),
monoid('=<<', [792, 792, 759], [[0, 3]]),
monoid('--', [667, 667], [[0, 2]]),
monoid(':=', [29, 761], [[0, 2]]),
monoid('=:=', [789, 789, 665], [[0, 3]]),
monoid('==', [794, 641], [[0, 2]]),
monoid('!==', [782, 782, 646], [[0, 3]]),
monoid('!=', [783, 629], [[0, 2]]),
monoid('<=', [790, 630], [[0, 2]]),
monoid('>=', [792, 631], [[0, 2]]),
monoid('//', [621, 664], [[0, 2]]),
monoid('/**', [18, 753, 753], [[0, 3]]),
monoid('/*', [18, 753], [[0, 2]]),
monoid('*/', [754, 18], [[0, 2]]),
monoid('&&', [633, 775], [[0, 2]]),
monoid('.&', [17, 755], [[0, 2]]),
monoid('||', [634, 635], [[0, 2]]),
monoid('!!', [769, 770], [[0, 2]]),
monoid('::', [772, 773], [[0, 2]]),
monoid('>>', [637, 638], [[0, 2]]),
monoid('<<', [639, 640], [[0, 2]]),
monoid('¯\\_(ツ)_/¯', [113, 765, 66, 767, 613, 768, 66, 766, 113], [[0, 3], [3, 6], [6, 9]]),
monoid('__', [763, 764], [[0, 2]])
];
const ubuntuCases: ITestCase[] = [
ubuntu('==>', [32, 32, 33], [])
];
const fontPaths: Record<string, string> = {
'Fira Code': path.join(__dirname, '../../fonts/FiraCode-Regular.otf'),
'Iosevka': path.join(__dirname, '../../fonts/iosevka-regular.ttf'),
'Monoid': path.join(__dirname, '../../fonts/Monoid-Regular.ttf'),
'Ubuntu Mono': path.join(__dirname, '../../fonts/UbuntuMono-Regular.ttf')
};
const fontCache: Map<string, IFont> = new Map();
function loadFont(fontName: string): IFont {
let font = fontCache.get(fontName);
if (!font) {
const fontPath = fontPaths[fontName];
const buffer = fs.readFileSync(fontPath);
font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
fontCache.set(fontName, font);
}
return font;
}
describe('addon-ligatures - index', () => {
describe('findLigatures', () => {
for (const { font: fontName, input, glyphs, ranges } of [...firaCases, ...iosevkaCases, ...monoidCases, ...ubuntuCases]) {
it(`${fontName}: '${input}'`, () => {
const font = loadFont(fontName);
const result = font.findLigatures(input);
assert.deepEqual(result.outputGlyphs, glyphs);
assert.deepEqual(result.contextRanges, ranges);
});
}
});
describe('findLigatureRanges', () => {
for (const { font: fontName, input, ranges } of [...firaCases, ...iosevkaCases, ...monoidCases, ...ubuntuCases]) {
it(`${fontName}: '${input}'`, () => {
const font = loadFont(fontName);
const result = font.findLigatureRanges(input);
assert.deepEqual(result, ranges);
});
}
});
describe('caching', () => {
it('findLigatures caches successive calls correctly', () => {
const fontPath = fontPaths['Fira Code'];
const buffer = fs.readFileSync(fontPath);
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
const result1 = font.findLigatures('in --> out');
const result2 = font.findLigatures('in --> out');
assert.deepEqual(result1, result2);
});
it('findLigatureRanges caches successive calls correctly', () => {
const fontPath = fontPaths['Fira Code'];
const buffer = fs.readFileSync(fontPath);
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
const result1 = font.findLigatureRanges('in --> out');
const result2 = font.findLigatureRanges('in --> out');
assert.deepEqual(result1, result2);
});
it('caches calls to findLigatures after findLigatureRanges correctly', () => {
const fontPath = fontPaths['Fira Code'];
const buffer = fs.readFileSync(fontPath);
const uncached = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
const uncachedResult1 = uncached.findLigatureRanges('in --> out');
const uncachedResult2 = uncached.findLigatures('in --> out');
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
const result1 = font.findLigatureRanges('in --> out');
const result2 = font.findLigatures('in --> out');
assert.deepEqual(result1, uncachedResult1);
assert.deepEqual(result2, uncachedResult2);
assert.deepEqual(result1, result2.contextRanges);
});
it('caches calls to findLigatureRanges after findLigatures correctly', () => {
const fontPath = fontPaths['Fira Code'];
const buffer = fs.readFileSync(fontPath);
const uncached = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
const uncachedResult1 = uncached.findLigatures('in --> out');
const uncachedResult2 = uncached.findLigatureRanges('in --> out');
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
const result1 = font.findLigatures('in --> out');
const result2 = font.findLigatureRanges('in --> out');
assert.deepEqual(result1, uncachedResult1);
assert.deepEqual(result2, uncachedResult2);
assert.deepEqual(result1.contextRanges, result2);
});
});
});
@@ -0,0 +1,262 @@
import * as opentype from 'opentype.js';
import LRUCache = require('lru-cache');
import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types';
import mergeTrees from './merge';
import walkTree from './walk';
import mergeRange from './mergeRange';
import buildTreeGsubType6Format1 from './processors/6-1';
import buildTreeGsubType6Format2 from './processors/6-2';
import buildTreeGsubType6Format3 from './processors/6-3';
import buildTreeGsubType8Format1 from './processors/8-1';
import flatten from './flatten';
class FontImpl implements IFont {
private _font: opentype.Font;
private _lookupTrees: { tree: IFlattenedLookupTree, processForward: boolean }[] = [];
private _glyphLookups: { [glyphId: string]: number[] } = {};
private _cache?: LRUCache<string, ILigatureData | [number, number][]>;
constructor(font: opentype.Font, options: Required<IOptions>) {
this._font = font;
if (options.cacheSize > 0) {
this._cache = new LRUCache({
max: options.cacheSize,
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
});
}
const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter((f: { tag: string }) => f.tag === 'calt') || [];
const lookupIndices: number[] = caltFeatures
.reduce((acc: number[], val: { feature: { lookupListIndexes: number[] } }) => [...acc, ...val.feature.lookupListIndexes], []);
const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || [];
const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i));
for (const [index, lookup] of lookupGroups.entries()) {
const trees: ILookupTree[] = [];
switch (lookup.lookupType) {
case 6:
for (const [index, table] of lookup.subtables.entries()) {
switch (table.substFormat) {
case 1:
trees.push(buildTreeGsubType6Format1(table, allLookups, index));
break;
case 2:
trees.push(buildTreeGsubType6Format2(table, allLookups, index));
break;
case 3:
trees.push(buildTreeGsubType6Format3(table, allLookups, index));
break;
}
}
break;
case 8:
for (const [index, table] of lookup.subtables.entries()) {
trees.push(buildTreeGsubType8Format1(table, index));
}
break;
}
const tree = flatten(mergeTrees(trees));
this._lookupTrees.push({
tree,
processForward: lookup.lookupType !== 8
});
for (const glyphId of Object.keys(tree)) {
if (!this._glyphLookups[glyphId]) {
this._glyphLookups[glyphId] = [];
}
this._glyphLookups[glyphId].push(index);
}
}
}
public findLigatures(text: string): ILigatureData {
const cached = this._cache && this._cache.get(text);
if (cached && !Array.isArray(cached)) {
return cached;
}
const glyphIds: number[] = [];
for (const char of text) {
glyphIds.push(this._font.charToGlyphIndex(char));
}
// If there are no lookup groups, there's no point looking for
// replacements. This gives us a minor performance boost for fonts with
// no ligatures
if (this._lookupTrees.length === 0) {
return {
inputGlyphs: glyphIds,
outputGlyphs: glyphIds,
contextRanges: []
};
}
const result = this._findInternal(glyphIds.slice());
const finalResult: ILigatureData = {
inputGlyphs: glyphIds,
outputGlyphs: result.sequence,
contextRanges: result.ranges
};
if (this._cache) {
this._cache.set(text, finalResult);
}
return finalResult;
}
public findLigatureRanges(text: string): [number, number][] {
// Short circuit the process if there are no possible ligatures in the
// font
if (this._lookupTrees.length === 0) {
return [];
}
const cached = this._cache && this._cache.get(text);
if (cached) {
return Array.isArray(cached) ? cached : cached.contextRanges;
}
const glyphIds: number[] = [];
for (const char of text) {
glyphIds.push(this._font.charToGlyphIndex(char));
}
const result = this._findInternal(glyphIds);
if (this._cache) {
this._cache.set(text, result.ranges);
}
return result.ranges;
}
private _findInternal(sequence: number[]): { sequence: number[], ranges: [number, number][] } {
const ranges: [number, number][] = [];
let nextLookup = this._getNextLookup(sequence, 0);
while (nextLookup.index !== null) {
const lookup = this._lookupTrees[nextLookup.index];
if (lookup.processForward) {
let lastGlyphIndex = nextLookup.last;
for (let i = nextLookup.first; i < lastGlyphIndex; i++) {
const result = walkTree(lookup.tree, sequence, i, i);
if (result) {
for (let j = 0; j < result.substitutions.length; j++) {
const sub = result.substitutions[j];
if (sub !== null) {
sequence[i + j] = sub;
}
}
mergeRange(
ranges,
result.contextRange[0] + i,
result.contextRange[1] + i
);
// Substitutions can end up extending the search range
if (i + result.length >= lastGlyphIndex) {
lastGlyphIndex = i + result.length + 1;
}
i += result.length - 1;
}
}
} else {
// We don't need to do the lastGlyphIndex tracking here because
// reverse processing isn't allowed to replace more than one
// character at a time.
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
const result = walkTree(lookup.tree, sequence, i, i);
if (result) {
for (let j = 0; j < result.substitutions.length; j++) {
const sub = result.substitutions[j];
if (sub !== null) {
sequence[i + j] = sub;
}
}
mergeRange(
ranges,
result.contextRange[0] + i,
result.contextRange[1] + i
);
i -= result.length - 1;
}
}
}
nextLookup = this._getNextLookup(sequence, nextLookup.index + 1);
}
return { sequence, ranges };
}
/**
* Returns the lookup and glyph range for the first lookup that might
* contain a match.
*
* @param sequence Input glyph sequence
* @param start The first input to try
*/
private _getNextLookup(sequence: number[], start: number): { index: number | null, first: number, last: number } {
const result: { index: number | null, first: number, last: number } = {
index: null,
first: Infinity,
last: -1
};
// Loop through each glyph and find the first valid lookup for it
for (let i = 0; i < sequence.length; i++) {
const lookups = this._glyphLookups[sequence[i]];
if (!lookups) {
continue;
}
for (let j = 0; j < lookups.length; j++) {
const lookupIndex = lookups[j];
if (lookupIndex >= start) {
// Update the lookup information if it's the one we're
// storing or earlier than it.
if (result.index === null || lookupIndex <= result.index) {
result.index = lookupIndex;
if (result.first > i) {
result.first = i;
}
result.last = i + 1;
}
break;
}
}
}
return result;
}
}
/**
* Load the font from it's binary data. The returned value can be used to find
* ligatures for the font.
*
* @param buffer ArrayBuffer of the font to load
*/
export function loadBuffer(buffer: ArrayBuffer, options?: IOptions): IFont {
const font = opentype.parse(buffer);
return new FontImpl(font, {
cacheSize: 0,
...options
});
}
export { IFont as Font, ILigatureData as LigatureData, IOptions as Options };
@@ -0,0 +1,225 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import mergeTrees from './merge';
interface ILookupResult {
contextRange: [number, number];
index: number;
subIndex: number;
length: number;
substitutions: number[];
}
function lookup(substitutionGlyph: number, index?: number, subIndex?: number): ILookupResult {
return {
contextRange: [0, 1],
index: index || 0,
subIndex: subIndex || 0,
length: 1,
substitutions: [substitutionGlyph]
};
}
describe('addon-ligatures - merge', () => {
describe('mergeTrees', () => {
it('combines disjoint trees', () => {
const result = mergeTrees([
{
individual: {
'1': { lookup: lookup(1) }
},
range: []
},
{
individual: {},
range: [{
entry: { lookup: lookup(2) },
range: [2, 4]
}]
},
{
individual: {
'5': { lookup: lookup(3) }
},
range: []
},
{
individual: {},
range: [{
entry: { lookup: lookup(4) },
range: [8, 10]
}]
}
]);
assert.deepEqual(result, {
individual: {
'1': { lookup: lookup(1) },
'5': { lookup: lookup(3) }
},
range: [{
entry: { lookup: lookup(2) },
range: [2, 4]
}, {
entry: { lookup: lookup(4) },
range: [8, 10]
}]
});
});
it('merges matching individual glyphs', () => {
const result = mergeTrees([
{
individual: {
'1': { lookup: lookup(1, 1) }
},
range: []
},
{
individual: {
'1': { lookup: lookup(2, 0) }
},
range: []
},
{
individual: {
'1': { lookup: lookup(3, 2) }
},
range: []
}
]);
assert.deepEqual(result, {
individual: {
'1': { lookup: lookup(2, 0) }
},
range: []
});
});
it('merges range glyphs overlapping individual glyphs', () => {
const result = mergeTrees([
{
individual: {
'1': { lookup: lookup(1, 0) }
},
range: []
},
{
individual: {},
range: [{
entry: { lookup: lookup(2, 1) },
range: [0, 4]
}]
}
]);
assert.deepEqual(result, {
individual: {
'0': { lookup: lookup(2, 1) },
'1': { lookup: lookup(1, 0) }
},
range: [{
entry: { lookup: lookup(2, 1) },
range: [2, 4]
}]
});
});
it('merges individual glyphs overlapping range glyphs', () => {
const result = mergeTrees([
{
individual: {},
range: [{
entry: { lookup: lookup(2, 1) },
range: [0, 4]
}]
},
{
individual: {
'1': { lookup: lookup(1, 0) }
},
range: []
}
]);
assert.deepEqual(result, {
individual: {
'0': { lookup: lookup(2, 1) },
'1': { lookup: lookup(1, 0) }
},
range: [{
entry: { lookup: lookup(2, 1) },
range: [2, 4]
}]
});
});
it('merges multiple overlapping ranges', () => {
const result = mergeTrees([
{
individual: {},
range: [{
entry: { lookup: lookup(1, 2) },
range: [0, 3]
}, {
entry: { lookup: lookup(2, 1) },
range: [6, 12]
}, {
entry: { lookup: lookup(5, 3) },
range: [15, 20]
}, {
entry: { lookup: lookup(7, 4) },
range: [20, 22]
}]
},
{
individual: {},
range: [{
entry: { lookup: lookup(3, 0) },
range: [2, 8]
}, {
entry: { lookup: lookup(4, 0) },
range: [10, 13]
}, {
entry: { lookup: lookup(6, 0) },
range: [16, 21]
}]
}
]);
assert.deepEqual(result, {
individual: {
'2': { lookup: lookup(3, 0) },
'12': { lookup: lookup(4, 0) },
'15': { lookup: lookup(5, 3) },
'20': { lookup: lookup(6, 0) },
'21': { lookup: lookup(7, 4) }
},
range: [{
entry: { lookup: lookup(1, 2) },
range: [0, 2]
}, {
entry: { lookup: lookup(3, 0) },
range: [6, 8]
}, {
entry: { lookup: lookup(3, 0) },
range: [3, 6]
}, {
entry: { lookup: lookup(2, 1) },
range: [8, 10]
}, {
entry: { lookup: lookup(4, 0) },
range: [10, 12]
}, {
entry: { lookup: lookup(6, 0) },
range: [16, 20]
}]
});
});
});
});
@@ -0,0 +1,393 @@
import { ILookupTree, ILookupTreeEntry } from './types';
/**
* Merges the provided trees into a single lookup tree. When conflicting lookups
* are encountered between two trees, the one with the lower index, then the
* lower subindex is chosen.
*
* @param trees Array of trees to merge. Entries in earlier trees are favored
* over those in later trees when there is a choice.
*/
export default function mergeTrees(trees: ILookupTree[]): ILookupTree {
const result: ILookupTree = {
individual: {},
range: []
};
const mergedEntries = new WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>();
for (const tree of trees) {
mergeSubtree(result, tree, mergedEntries);
}
return result;
}
/**
* Recursively merges the data for the mergeTree into the mainTree.
*
* @param mainTree The tree where the values should be merged
* @param mergeTree The tree to be merged into the mainTree
* @param mergedEntries WeakMap to track already merged entry pairs
*/
function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
// Need to fix this recursively (and handle lookups)
for (const [glyphId, value] of Object.entries(mergeTree.individual)) {
// The main tree is guaranteed to have no overlaps between the
// individual and range values, so if we match an invididual, there
// must not be a range
if (mainTree.individual[glyphId]) {
mergeTreeEntry(mainTree.individual[glyphId], value, mergedEntries);
} else {
let matched = false;
for (const [index, { range, entry }] of mainTree.range.entries()) {
const overlap = getIndividualOverlap(Number(glyphId), range);
// Don't overlap
if (overlap.both === null) {
continue;
}
matched = true;
// If they overlap, we have to split the range and then
// merge the overlap
mainTree.individual[glyphId] = value;
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
// When there's an overlap, we also have to fix up the range
// that we had already processed
mainTree.range.splice(index, 1);
for (const glyph of overlap.second) {
if (Array.isArray(glyph)) {
mainTree.range.push({
range: glyph,
entry: cloneEntry(entry)
});
} else {
mainTree.individual[glyph] = cloneEntry(entry);
}
}
}
if (!matched) {
mainTree.individual[glyphId] = value;
}
}
}
for (const { range, entry } of mergeTree.range) {
// Ranges are more complicated, because they can overlap with
// multiple things, individual and range alike. We start by
// eliminating ranges that are already present in another range
let remainingRanges: (number | [number, number])[] = [range];
for (let index = 0; index < mainTree.range.length; index++) {
const { range, entry: resultEntry } = mainTree.range[index];
for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
if (Array.isArray(remainingRange)) {
const overlap = getRangeOverlap(remainingRange, range);
if (overlap.both === null) {
continue;
}
mainTree.range.splice(index, 1);
index--;
const entryToMerge: ILookupTreeEntry = cloneEntry(resultEntry);
if (Array.isArray(overlap.both)) {
mainTree.range.push({
range: overlap.both,
entry: entryToMerge
});
} else {
mainTree.individual[overlap.both] = entryToMerge;
}
mergeTreeEntry(entryToMerge, cloneEntry(entry), mergedEntries);
for (const second of overlap.second) {
if (Array.isArray(second)) {
mainTree.range.push({
range: second,
entry: cloneEntry(resultEntry)
});
} else {
mainTree.individual[second] = cloneEntry(resultEntry);
}
}
remainingRanges = overlap.first;
} else {
const overlap = getIndividualOverlap(remainingRange, range);
if (overlap.both === null) {
continue;
}
// If they overlap, we have to split the range and then
// merge the overlap
mainTree.individual[remainingRange] = cloneEntry(entry);
mergeTreeEntry(mainTree.individual[remainingRange], cloneEntry(resultEntry), mergedEntries);
// When there's an overlap, we also have to fix up the range
// that we had already processed
mainTree.range.splice(index, 1);
index--;
for (const glyph of overlap.second) {
if (Array.isArray(glyph)) {
mainTree.range.push({
range: glyph,
entry: cloneEntry(resultEntry)
});
} else {
mainTree.individual[glyph] = cloneEntry(resultEntry);
}
}
remainingRanges.splice(remainingIndex, 1, ...overlap.first);
break;
}
}
}
// Next, we run the same against any individual glyphs
for (const glyphId of Object.keys(mainTree.individual)) {
for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
if (Array.isArray(remainingRange)) {
const overlap = getIndividualOverlap(Number(glyphId), remainingRange);
if (overlap.both === null) {
continue;
}
// If they overlap, we have to merge the overlap
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
// Update the remaining ranges
remainingRanges.splice(remainingIndex, 1, ...overlap.second);
break;
} else {
if (Number(glyphId) === remainingRange) {
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
break;
}
}
}
}
// Any remaining ranges should just be added directly
for (const remainingRange of remainingRanges) {
if (Array.isArray(remainingRange)) {
mainTree.range.push({
range: remainingRange,
entry: cloneEntry(entry)
});
} else {
mainTree.individual[remainingRange] = cloneEntry(entry);
}
}
}
}
/**
* Recursively merges the entry forr the mergeTree into the mainTree
*
* @param mainTree The entry where the values should be merged
* @param mergeTree The entry to merge into the mainTree
* @param mergedEntries WeakMap to track already merged entry pairs
*/
function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
// Check if we've already merged this pair
let mergedSet = mergedEntries.get(mainTree);
if (mergedSet?.has(mergeTree)) {
return;
}
if (!mergedSet) {
mergedSet = new Set();
mergedEntries.set(mainTree, mergedSet);
}
mergedSet.add(mergeTree);
if (
mergeTree.lookup && (
!mainTree.lookup ||
mainTree.lookup.index > mergeTree.lookup.index ||
(mainTree.lookup.index === mergeTree.lookup.index && mainTree.lookup.subIndex > mergeTree.lookup.subIndex)
)
) {
mainTree.lookup = mergeTree.lookup;
}
if (mergeTree.forward) {
if (!mainTree.forward) {
mainTree.forward = mergeTree.forward;
} else {
mergeSubtree(mainTree.forward, mergeTree.forward, mergedEntries);
}
}
if (mergeTree.reverse) {
if (!mainTree.reverse) {
mainTree.reverse = mergeTree.reverse;
} else {
mergeSubtree(mainTree.reverse, mergeTree.reverse, mergedEntries);
}
}
}
interface IOverlap {
first: (number | [number, number])[];
second: (number | [number, number])[];
both: number | [number, number] | null;
}
/**
* Determines the overlap (if any) between two ranges. Returns the distinct
* ranges for each range and the overlap (if any).
*
* @param first First range
* @param second Second range
*/
function getRangeOverlap(first: [number, number], second: [number, number]): IOverlap {
const result: IOverlap = {
first: [],
second: [],
both: null
};
// Both
if (first[0] < second[1] && second[0] < first[1]) {
const start = Math.max(first[0], second[0]);
const end = Math.min(first[1], second[1]);
result.both = rangeOrIndividual(start, end);
}
// Before
if (first[0] < second[0]) {
const start = first[0];
const end = Math.min(second[0], first[1]);
result.first.push(rangeOrIndividual(start, end));
} else if (second[0] < first[0]) {
const start = second[0];
const end = Math.min(second[1], first[0]);
result.second.push(rangeOrIndividual(start, end));
}
// After
if (first[1] > second[1]) {
const start = Math.max(first[0], second[1]);
const end = first[1];
result.first.push(rangeOrIndividual(start, end));
} else if (second[1] > first[1]) {
const start = Math.max(first[1], second[0]);
const end = second[1];
result.second.push(rangeOrIndividual(start, end));
}
return result;
}
/**
* Determines the overlap (if any) between the individual glyph and the range
* provided. Returns the glyphs and/or ranges that are unique to each provided
* and the overlap (if any).
*
* @param first Individual glyph
* @param second Range
*/
function getIndividualOverlap(first: number, second: [number, number]): IOverlap {
// Disjoint
if (first < second[0] || first > second[1]) {
return {
first: [first],
second: [second],
both: null
};
}
const result: IOverlap = {
first: [],
second: [],
both: first
};
if (second[0] < first) {
result.second.push(rangeOrIndividual(second[0], first));
}
if (second[1] > first) {
result.second.push(rangeOrIndividual(first + 1, second[1]));
}
return result;
}
/**
* Returns an individual glyph if the range is of size one or a range if it is
* larger.
*
* @param start Beginning of the range (inclusive)
* @param end End of the range (exclusive)
*/
function rangeOrIndividual(start: number, end: number): number | [number, number] {
if (end - start === 1) {
return start;
}
return [start, end];
}
/**
* Clones an individual lookup tree entry.
*
* @param entry Lookup tree entry to clone
* @param visited Map to track already cloned entries (prevents infinite loops)
*/
function cloneEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTreeEntry {
if (visited.has(entry)) {
return visited.get(entry)!;
}
const result: ILookupTreeEntry = {};
visited.set(entry, result);
if (entry.forward) {
result.forward = cloneTree(entry.forward, visited);
}
if (entry.reverse) {
result.reverse = cloneTree(entry.reverse, visited);
}
if (entry.lookup) {
result.lookup = {
contextRange: entry.lookup.contextRange.slice() as [number, number],
index: entry.lookup.index,
length: entry.lookup.length,
subIndex: entry.lookup.subIndex,
substitutions: entry.lookup.substitutions.slice()
};
}
return result;
}
/**
* Clones a lookup tree.
*
* @param tree Lookup tree to clone
* @param visited Map to track already cloned entries (prevents infinite loops)
*/
function cloneTree(tree: ILookupTree, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTree {
const individual: { [glyphId: string]: ILookupTreeEntry } = {};
for (const [glyphId, entry] of Object.entries(tree.individual)) {
individual[glyphId] = cloneEntry(entry, visited);
}
return {
individual,
range: tree.range.map(({ range, entry }) => ({
range: range.slice() as [number, number],
entry: cloneEntry(entry, visited)
}))
};
}
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import mergeRange from './mergeRange';
describe('addon-ligatures - mergeRange', () => {
it('inserts a new range before the existing ones', () => {
const result = mergeRange([[1, 2], [2, 3]], 0, 1);
assert.deepEqual(result, [[0, 1], [1, 2], [2, 3]]);
});
it('inserts in between two ranges', () => {
const result = mergeRange([[0, 2], [4, 6]], 2, 4);
assert.deepEqual(result, [[0, 2], [2, 4], [4, 6]]);
});
it('inserts after the last range', () => {
const result = mergeRange([[0, 2], [4, 6]], 6, 8);
assert.deepEqual(result, [[0, 2], [4, 6], [6, 8]]);
});
it('extends the beginning of a range', () => {
const result = mergeRange([[0, 2], [4, 6]], 3, 5);
assert.deepEqual(result, [[0, 2], [3, 6]]);
});
it('extends the end of a range', () => {
const result = mergeRange([[0, 2], [4, 6]], 1, 4);
assert.deepEqual(result, [[0, 4], [4, 6]]);
});
it('extends the last range', () => {
const result = mergeRange([[0, 2], [4, 6]], 5, 7);
assert.deepEqual(result, [[0, 2], [4, 7]]);
});
it('connects two ranges', () => {
const result = mergeRange([[0, 2], [4, 6]], 1, 5);
assert.deepEqual(result, [[0, 6]]);
});
it('connects more than two ranges', () => {
const result = mergeRange([[0, 2], [4, 6], [8, 10], [12, 14]], 1, 10);
assert.deepEqual(result, [[0, 10], [12, 14]]);
});
});
@@ -0,0 +1,66 @@
/**
* Merges the range defined by the provided start and end into the list of
* existing ranges. The merge is done in place on the existing range for
* performance and is also returned.
*
* @param ranges Existing range list
* @param newRangeStart Start position of the range to merge, inclusive
* @param newRangeEnd End position of range to merge, exclusive
*/
export default function mergeRange(ranges: [number, number][], newRangeStart: number, newRangeEnd: number): [number, number][] {
let inRange = false;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (!inRange) {
if (newRangeEnd <= range[0]) {
// Case 1: New range is before the search range
ranges.splice(i, 0, [newRangeStart, newRangeEnd]);
return ranges;
}
if (newRangeEnd <= range[1]) {
// Case 2: New range is either wholly contained within the
// search range or overlaps with the front of it
range[0] = Math.min(newRangeStart, range[0]);
return ranges;
}
if (newRangeStart < range[1]) {
// Case 3: New range either wholly contains the search range
// or overlaps with the end of it
range[0] = Math.min(newRangeStart, range[0]);
inRange = true;
} else {
// Case 4: New range starts after the search range
continue;
}
} else {
if (newRangeEnd <= range[0]) {
// Case 5: New range extends from previous range but doesn't
// reach the current one
ranges[i - 1][1] = newRangeEnd;
return ranges;
}
if (newRangeEnd <= range[1]) {
// Case 6: New range extends from prvious range into the
// current range
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
ranges.splice(i, 1);
inRange = false;
return ranges;
}
// Case 7: New range extends from previous range past the
// end of the current range
ranges.splice(i, 1);
i--;
}
}
if (inRange) {
// Case 8: New range extends past the last existing range
ranges[ranges.length - 1][1] = newRangeEnd;
} else {
// Case 9: New range starts after the last existing range
ranges.push([newRangeStart, newRangeEnd]);
}
return ranges;
}
@@ -0,0 +1,82 @@
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
import { ILookupTree } from '../types';
import { listGlyphsByIndex } from './coverage';
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
/**
* Build lookup tree for GSUB lookup table 6, format 1.
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chaining-context-substitution-format-1-simple-glyph-contexts
*
* @param table JSON representation of the table
* @param lookups List of lookup tables
* @param tableIndex Index of this table in the overall lookup
*/
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat1, lookups: Lookup[], tableIndex: number): ILookupTree {
const result: ILookupTree = {
individual: {},
range: []
};
const firstGlyphs = listGlyphsByIndex(table.coverage);
for (const { glyphId, index } of firstGlyphs) {
const chainRuleSet = table.chainRuleSets[index];
// If the chain rule set is null there's nothing to do with this table.
if (!chainRuleSet) {
continue;
}
for (const [subIndex, subTable] of chainRuleSet.entries()) {
let currentEntries: IEntryMeta[] = getInputTree(
result,
subTable.lookupRecords,
lookups,
0,
glyphId
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
// We walk forward, then backward
for (const [index, glyph] of subTable.input.entries()) {
currentEntries = processInputPosition(
[glyph],
index + 1,
currentEntries,
subTable.lookupRecords,
lookups
);
}
for (const glyph of subTable.lookahead) {
currentEntries = processLookaheadPosition(
[glyph],
currentEntries
);
}
for (const glyph of subTable.backtrack) {
currentEntries = processBacktrackPosition(
[glyph],
currentEntries
);
}
// When we get to the end, insert the lookup information
for (const { entry, substitutions } of currentEntries) {
entry.lookup = {
substitutions,
length: subTable.input.length + 1,
index: tableIndex,
subIndex,
contextRange: [
-1 * subTable.backtrack.length,
1 + subTable.input.length + subTable.lookahead.length
]
};
}
}
}
return result;
}
@@ -0,0 +1,96 @@
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
import { ILookupTree } from '../types';
import mergeTrees from '../merge';
import { listGlyphsByIndex } from './coverage';
import getGlyphClass, { listClassGlyphs } from './classDef';
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
/**
* Build lookup tree for GSUB lookup table 6, format 2.
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chaining-context-substitution-format-2-class-based-glyph-contexts
*
* @param table JSON representation of the table
* @param lookups List of lookup tables
* @param tableIndex Index of this table in the overall lookup
*/
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat2, lookups: Lookup[], tableIndex: number): ILookupTree {
const results: ILookupTree[] = [];
const firstGlyphs = listGlyphsByIndex(table.coverage);
for (const { glyphId } of firstGlyphs) {
const firstInputClass = getGlyphClass(table.inputClassDef, glyphId);
for (const [glyphId, inputClass] of firstInputClass.entries()) {
// istanbul ignore next - invalid font
if (inputClass === null) {
continue;
}
const classSet = table.chainClassSet[inputClass];
// If the class set is null there's nothing to do with this table.
if (!classSet) {
continue;
}
for (const [subIndex, subTable] of classSet.entries()) {
const result: ILookupTree = {
individual: {},
range: []
};
let currentEntries: IEntryMeta[] = getInputTree(
result,
subTable.lookupRecords,
lookups,
0,
glyphId
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
for (const [index, classNum] of subTable.input.entries()) {
currentEntries = processInputPosition(
listClassGlyphs(table.inputClassDef, classNum),
index + 1,
currentEntries,
subTable.lookupRecords,
lookups
);
}
for (const classNum of subTable.lookahead) {
currentEntries = processLookaheadPosition(
listClassGlyphs(table.lookaheadClassDef, classNum),
currentEntries
);
}
for (const classNum of subTable.backtrack) {
currentEntries = processBacktrackPosition(
listClassGlyphs(table.backtrackClassDef, classNum),
currentEntries
);
}
// When we get to the end, all of the entries we've accumulated
// should have a lookup defined
for (const { entry, substitutions } of currentEntries) {
entry.lookup = {
substitutions,
index: tableIndex,
subIndex,
length: subTable.input.length + 1,
contextRange: [
-1 * subTable.backtrack.length,
1 + subTable.input.length + subTable.lookahead.length
]
};
}
results.push(result);
}
}
}
return mergeTrees(results);
}
@@ -0,0 +1,73 @@
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
import { ILookupTree } from '../types';
import { listGlyphsByIndex } from './coverage';
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
/**
* Build lookup tree for GSUB lookup table 6, format 3.
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#63-chaining-context-substitution-format-3-coverage-based-glyph-contexts
*
* @param table JSON representation of the table
* @param lookups List of lookup tables
* @param tableIndex Index of this table in the overall lookup
*/
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat3, lookups: Lookup[], tableIndex: number): ILookupTree {
const result: ILookupTree = {
individual: {},
range: []
};
const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]);
for (const { glyphId } of firstGlyphs) {
let currentEntries: IEntryMeta[] = getInputTree(
result,
table.lookupRecords,
lookups,
0,
glyphId
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
for (const [index, coverage] of table.inputCoverage.slice(1).entries()) {
currentEntries = processInputPosition(
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
index + 1,
currentEntries,
table.lookupRecords,
lookups
);
}
for (const coverage of table.lookaheadCoverage) {
currentEntries = processLookaheadPosition(
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
currentEntries
);
}
for (const coverage of table.backtrackCoverage) {
currentEntries = processBacktrackPosition(
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
currentEntries
);
}
// When we get to the end, all of the entries we've accumulated
// should have a lookup defined
for (const { entry, substitutions } of currentEntries) {
entry.lookup = {
substitutions,
index: tableIndex,
subIndex: 0,
length: table.inputCoverage.length,
contextRange: [
-1 * table.backtrackCoverage.length,
table.inputCoverage.length + table.lookaheadCoverage.length
]
};
}
}
return result;
}
@@ -0,0 +1,69 @@
import { IReverseChainingContextualSingleSubstitutionTable } from '../tables';
import { ILookupTree, ILookupTreeEntry } from '../types';
import { listGlyphsByIndex } from './coverage';
import { processLookaheadPosition, processBacktrackPosition, IEntryMeta } from './helper';
/**
* Build lookup tree for GSUB lookup table 8, format 1.
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#81-reverse-chaining-contextual-single-substitution-format-1-coverage-based-glyph-contexts
*
* @param table JSON representation of the table
* @param tableIndex Index of this table in the overall lookup
*/
export default function buildTree(table: IReverseChainingContextualSingleSubstitutionTable, tableIndex: number): ILookupTree {
const result: ILookupTree = {
individual: {},
range: []
};
const glyphs = listGlyphsByIndex(table.coverage);
for (const { glyphId, index } of glyphs) {
const initialEntry: ILookupTreeEntry = {};
if (Array.isArray(glyphId)) {
result.range.push({
entry: initialEntry,
range: glyphId
});
} else {
result.individual[glyphId] = initialEntry;
}
let currentEntries: IEntryMeta[] = [{
entry: initialEntry,
substitutions: [table.substitutes[index]]
}];
// We walk forward, then backward
for (const coverage of table.lookaheadCoverage) {
currentEntries = processLookaheadPosition(
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
currentEntries
);
}
for (const coverage of table.backtrackCoverage) {
currentEntries = processBacktrackPosition(
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
currentEntries
);
}
// When we get to the end, insert the lookup information
for (const { entry, substitutions } of currentEntries) {
entry.lookup = {
substitutions,
index: tableIndex,
subIndex: 0,
length: 1,
contextRange: [
-1 * table.backtrackCoverage.length,
1 + table.lookaheadCoverage.length
]
};
}
}
return result;
}
@@ -0,0 +1,84 @@
import { ClassDefTable } from '../tables';
/**
* Get the number of the class to which the glyph belongs, or null if it doesn't
* belong to any of them.
*
* @param table JSON representation of the class def table
* @param glyphId Index of the glyph to look for
*/
export default function getGlyphClass(table: ClassDefTable, glyphId: number | [number, number]): Map<number | [number, number], number | null> {
switch (table.format) {
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2
case 2:
if (Array.isArray(glyphId)) {
return getRangeGlyphClass(table, glyphId);
}
return new Map([[
glyphId,
getIndividualGlyphClass(table, glyphId)
]]);
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-1
default:
return new Map([[glyphId, null]]);
}
}
function getRangeGlyphClass(table: ClassDefTable.IFormat2, glyphId: [number, number]): Map<number | [number, number], number | null> {
const classStart: number = glyphId[0];
const currentClass: number | null = getIndividualGlyphClass(table, classStart);
let search: number = glyphId[0] + 1;
const result = new Map<[number, number] | number, number | null>();
while (search < glyphId[1]) {
const clazz = getIndividualGlyphClass(table, search);
if (clazz !== currentClass) {
if (search - classStart <= 1) {
result.set(classStart, currentClass);
} else {
result.set([classStart, search], currentClass);
}
}
search++;
}
if (search - classStart <= 1) {
result.set(classStart, currentClass);
} else {
result.set([classStart, search], currentClass);
}
return result;
}
function getIndividualGlyphClass(table: ClassDefTable.IFormat2, glyphId: number): number | null {
for (const range of table.ranges) {
if (range.start <= glyphId && range.end >= glyphId) {
return range.classId;
}
}
return null;
}
export function listClassGlyphs(table: ClassDefTable, index: number): (number | [number, number])[] {
switch (table.format) {
case 2:
const results: (number | [number, number])[] = [];
for (const range of table.ranges) {
if (range.classId !== index) {
continue;
}
if (range.end === range.start) {
results.push(range.start);
} else {
results.push([range.start, range.end + 1]);
}
}
return results;
default:
return [];
}
}
@@ -0,0 +1,43 @@
import { CoverageTable } from '../tables';
/**
* Get the index of the given glyph in the coverage table, or null if it is not
* present in the table.
*
* @param table JSON representation of the coverage table
* @param glyphId Index of the glyph to look for
*/
export default function getCoverageGlyphIndex(table: CoverageTable, glyphId: number): number | null {
switch (table.format) {
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1
case 1:
const index = table.glyphs.indexOf(glyphId);
return index !== -1
? index
: null;
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2
case 2:
const range = table.ranges
.find(range => range.start <= glyphId && range.end >= glyphId);
return range
? range.index
: null;
}
}
export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number], index: number }[] {
switch (table.format) {
case 1:
return table.glyphs.map((glyphId, index) => ({ glyphId, index }));
case 2:
const results: { glyphId: number | [number, number], index: number }[] = [];
for (const [index, range] of table.ranges.entries()) {
if (range.end === range.start) {
results.push({ glyphId: range.start, index });
} else {
results.push({ glyphId: [range.start, range.end + 1], index });
}
}
return results;
}
}
@@ -0,0 +1,183 @@
import { ILookupTreeEntry, ILookupTree } from '../types';
import { ISubstitutionLookupRecord, Lookup } from '../tables';
import { getIndividualSubstitutionGlyph, getRangeSubstitutionGlyphs } from './substitution';
export interface IEntryMeta {
entry: ILookupTreeEntry;
substitutions: (number | null)[];
}
export function processInputPosition(
glyphs: (number | [number, number])[],
position: number,
currentEntries: IEntryMeta[],
lookupRecords: ISubstitutionLookupRecord[],
lookups: Lookup[]
): IEntryMeta[] {
const nextEntries: IEntryMeta[] = [];
for (const currentEntry of currentEntries) {
currentEntry.entry.forward = {
individual: {},
range: []
};
for (const glyph of glyphs) {
nextEntries.push(...getInputTree(
currentEntry.entry.forward,
lookupRecords,
lookups,
position,
glyph
).map(({ entry, substitution }) => ({
entry,
substitutions: [...currentEntry.substitutions, substitution]
})));
}
}
return nextEntries;
}
export function processLookaheadPosition(
glyphs: (number | [number, number])[],
currentEntries: IEntryMeta[]
): IEntryMeta[] {
const nextEntries: IEntryMeta[] = [];
const processedEntries = new Set<ILookupTreeEntry>();
for (const currentEntry of currentEntries) {
// Skip if we've already processed this entry object
if (processedEntries.has(currentEntry.entry)) {
continue;
}
processedEntries.add(currentEntry.entry);
currentEntry.entry.forward ??= {
individual: {},
range: []
};
// All glyphs at this position share ONE entry - lookahead just needs to match,
// all paths lead to the same result
const sharedEntry: ILookupTreeEntry = {};
for (const glyph of glyphs) {
if (Array.isArray(glyph)) {
currentEntry.entry.forward.range.push({
entry: sharedEntry,
range: glyph
});
} else {
currentEntry.entry.forward.individual[glyph] = sharedEntry;
}
}
nextEntries.push({
entry: sharedEntry,
substitutions: currentEntry.substitutions
});
}
return nextEntries;
}
export function processBacktrackPosition(
glyphs: (number | [number, number])[],
currentEntries: IEntryMeta[]
): IEntryMeta[] {
const nextEntries: IEntryMeta[] = [];
const processedEntries = new Set<ILookupTreeEntry>();
for (const currentEntry of currentEntries) {
// Skip if we've already processed this entry object
if (processedEntries.has(currentEntry.entry)) {
continue;
}
processedEntries.add(currentEntry.entry);
currentEntry.entry.reverse ??= {
individual: {},
range: []
};
// All glyphs at this position share ONE entry - backtrack just needs to match,
// all paths lead to the same result
const sharedEntry: ILookupTreeEntry = {};
for (const glyph of glyphs) {
if (Array.isArray(glyph)) {
currentEntry.entry.reverse.range.push({
entry: sharedEntry,
range: glyph
});
} else {
currentEntry.entry.reverse.individual[glyph] = sharedEntry;
}
}
nextEntries.push({
entry: sharedEntry,
substitutions: currentEntry.substitutions
});
}
return nextEntries;
}
export function getInputTree(tree: ILookupTree, substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: ILookupTreeEntry, substitution: number | null }[] {
const result: { entry: ILookupTreeEntry, substitution: number | null }[] = [];
if (!Array.isArray(glyphId)) {
tree.individual[glyphId] = {};
result.push({
entry: tree.individual[glyphId],
substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId)
});
} else {
const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId);
for (const [range, substitution] of subs) {
const entry: ILookupTreeEntry = {};
if (Array.isArray(range)) {
tree.range.push({ range, entry });
} else {
tree.individual[range] = {};
}
result.push({ entry, substitution });
}
}
return result;
}
function getSubstitutionAtPositionRange(substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map<number | [number, number], number | null> {
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.IType1).subtables) {
const sub = getRangeSubstitutionGlyphs(
substitutionTable,
range
);
if (!Array.from(sub.values()).every(val => val !== null)) {
return sub;
}
}
}
return new Map([[range, null]]);
}
function getSubstitutionAtPosition(substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null {
for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) {
for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.IType1).subtables) {
const sub = getIndividualSubstitutionGlyph(
substitutionTable,
glyphId
);
if (sub !== null) {
return sub;
}
}
}
return null;
}
@@ -0,0 +1,59 @@
import { SubstitutionTable } from '../tables';
import getCoverageGlyphIndex from './coverage';
/**
* Get the substitution glyph for the givne glyph, or null if the glyph was not
* found in the table.
*
* @param table JSON representation of the substitution table
* @param glyphId The index of the glpyh to find substitutions for
*/
export function getRangeSubstitutionGlyphs(table: SubstitutionTable, glyphId: [number, number]): Map<[number, number] | number, number | null> {
const replacementStart: number = glyphId[0];
const currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart);
let search: number = glyphId[0] + 1;
const result = new Map<[number, number] | number, number | null>();
while (search < glyphId[1]) {
const sub = getIndividualSubstitutionGlyph(table, search);
if (sub !== currentReplacement) {
if (search - replacementStart <= 1) {
result.set(replacementStart, currentReplacement);
} else {
result.set([replacementStart, search], currentReplacement);
}
}
search++;
}
if (search - replacementStart <= 1) {
result.set(replacementStart, currentReplacement);
} else {
result.set([replacementStart, search], currentReplacement);
}
return result;
}
export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId: number): number | null {
const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId);
// istanbul ignore next - invalid font
if (coverageIndex === null) {
return null;
}
switch (table.substFormat) {
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1
case 1:
// TODO: determine if there's a rhyme or reason to the 16-bit
// wraparound and if it can ever be a different number
return (glyphId + table.deltaGlyphId) % (2 ** 16);
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
case 2:
return table.substitute[coverageIndex] ?? null;
}
}
@@ -0,0 +1,112 @@
export type SubstitutionTable = SubstitutionTable.IFormat1 | SubstitutionTable.IFormat2;
export namespace SubstitutionTable {
export interface IFormat1 {
substFormat: 1;
coverage: CoverageTable;
deltaGlyphId: number;
}
export interface IFormat2 {
substFormat: 2;
coverage: CoverageTable;
substitute: number[];
}
}
export type CoverageTable = CoverageTable.IFormat1 | CoverageTable.IFormat2;
export namespace CoverageTable {
export interface IFormat1 {
format: 1;
glyphs: number[];
}
export interface IFormat2 {
format: 2;
ranges: {
start: number;
end: number;
index: number;
}[];
}
}
export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.IFormat1 |
ChainingContextualSubstitutionTable.IFormat2 | ChainingContextualSubstitutionTable.IFormat3;
export namespace ChainingContextualSubstitutionTable {
export interface IFormat1 {
substFormat: 1;
coverage: CoverageTable;
chainRuleSets: ChainSubRuleTable[][];
}
export interface IFormat2 {
substFormat: 2;
coverage: CoverageTable;
backtrackClassDef: ClassDefTable;
inputClassDef: ClassDefTable;
lookaheadClassDef: ClassDefTable;
chainClassSet: (null | IChainSubClassRuleTable[])[];
}
export interface IFormat3 {
substFormat: 3;
backtrackCoverage: CoverageTable[];
inputCoverage: CoverageTable[];
lookaheadCoverage: CoverageTable[];
lookupRecords: ISubstitutionLookupRecord[];
}
}
export interface IReverseChainingContextualSingleSubstitutionTable {
substFormat: 1;
coverage: CoverageTable;
backtrackCoverage: CoverageTable[];
lookaheadCoverage: CoverageTable[];
substitutes: number[];
}
export type ClassDefTable = ClassDefTable.IFormat2;
export namespace ClassDefTable {
export interface IFormat2 {
format: 2;
ranges: {
start: number;
end: number;
classId: number;
}[];
}
}
export interface ISubstitutionLookupRecord {
sequenceIndex: number;
lookupListIndex: number;
}
export type ChainSubRuleTable = IChainSubClassRuleTable;
export interface IChainSubClassRuleTable {
backtrack: number[];
input: number[];
lookahead: number[];
lookupRecords: ISubstitutionLookupRecord[];
}
export type Lookup = Lookup.IType1 | Lookup.IType6 | Lookup.IType8;
export namespace Lookup {
export interface IType1 {
lookupType: 1;
lookupFlag: number;
subtables: SubstitutionTable[];
}
export interface IType6 {
lookupType: 6;
lookupFlag: number;
subtables: ChainingContextualSubstitutionTable[];
}
export interface IType8 {
lookupType: 8;
lookupFlag: number;
subtables: IReverseChainingContextualSingleSubstitutionTable[];
}
}
@@ -0,0 +1,86 @@
export interface ISubstitutionResult {
index: number;
contextRange: [number, number];
}
/**
* Information about ligatures found in a sequence of text
*/
export interface ILigatureData {
/**
* The list of font glyphs in the input text.
*/
inputGlyphs: number[];
/**
* The list of font glyphs after performing replacements for font ligatures.
*/
outputGlyphs: number[];
/**
* Sorted array of ranges that must be rendered together to produce the
* ligatures in the output sequence. The ranges are inclusive on the left and
* exclusive on the right.
*/
contextRanges: [number, number][];
}
export interface IFont {
/**
* Scans the provided text for font ligatures, returning an object with
* metadata about the text and any ligatures found.
*
* @param text String to search for ligatures
*/
findLigatures(text: string): ILigatureData;
/**
* Scans the provided text for font ligatures, returning an array of ranges
* where ligatures are located.
*
* @param text String to search for ligatures
*/
findLigatureRanges(text: string): [number, number][];
}
export interface IOptions {
/**
* Optional size of previous results to store, measured in total number of
* characters from input strings. Defaults to no cache (0)
*/
cacheSize?: number;
}
export interface ILookupTree {
individual: {
[glyphId: string]: ILookupTreeEntry;
};
range: {
range: [number, number];
entry: ILookupTreeEntry;
}[];
}
export interface ILookupTreeEntry {
lookup?: ILookupResult;
forward?: ILookupTree;
reverse?: ILookupTree;
}
export interface ILookupResult {
substitutions: (number | null)[];
length: number;
index: number;
subIndex: number;
contextRange: [number, number];
}
export interface IFlattenedLookupTree {
[glyphId: string]: IFlattenedLookupTreeEntry;
}
export interface IFlattenedLookupTreeEntry {
lookup?: ILookupResult;
forward?: IFlattenedLookupTree;
reverse?: IFlattenedLookupTree;
}
@@ -0,0 +1,67 @@
import { IFlattenedLookupTree, ILookupResult } from './types';
export default function walkTree(tree: IFlattenedLookupTree, sequence: number[], startIndex: number, index: number): ILookupResult | undefined {
const glyphId = sequence[index];
const subtree = tree[glyphId];
if (!subtree) {
return undefined;
}
let lookup = subtree.lookup;
if (subtree.reverse) {
const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex);
if (
(!lookup && reverseLookup) ||
(
reverseLookup && lookup && (
lookup.index > reverseLookup.index ||
(lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex)
)
)
) {
lookup = reverseLookup;
}
}
if (++index >= sequence.length || !subtree.forward) {
return lookup;
}
const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index);
if (
(!lookup && forwardLookup) ||
(
forwardLookup && lookup && (
lookup.index > forwardLookup.index ||
(lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex)
)
)
) {
lookup = forwardLookup;
}
return lookup;
}
function walkReverse(tree: IFlattenedLookupTree, sequence: number[], index: number): ILookupResult | undefined {
let subtree = tree[sequence[--index]];
let lookup: ILookupResult | undefined = subtree && subtree.lookup;
while (subtree) {
if (
(!lookup && subtree.lookup) ||
(subtree.lookup && lookup && lookup.index > subtree.lookup.index)
) {
lookup = subtree.lookup;
}
if (--index < 0 || !subtree.reverse) {
break;
}
subtree = subtree.reverse[sequence[index]];
}
return lookup;
}
+2 -2
View File
@@ -4,7 +4,7 @@
*/
import type { Terminal } from '@xterm/xterm';
import { Font } from 'font-ligatures';
import { Font } from './fontLigatures/index';
import load from './font';
@@ -30,7 +30,7 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
let currentFontName: string | undefined = undefined;
let font: Font | undefined = undefined;
let loadingState: LoadingState = LoadingState.UNLOADED;
let loadError: any | undefined = undefined;
let loadError: unknown = undefined;
return term.registerCharacterJoiner((text: string): [number, number][] => {
// If the font hasn't been loaded yet, load it and return an empty result
+1 -2
View File
@@ -4,11 +4,10 @@
*/
import { assert } from 'chai';
import parse from './parse';
// TODO: integrate tests from http://test.csswg.org/suites/css-fonts-4_dev/nightly-unstable/
describe('parse', () => {
describe('addon-ligatures - parse', () => {
it('parses individual families', () => {
assert.deepEqual(parse('monospace'), ['monospace']);
});
+3 -1
View File
@@ -9,7 +9,9 @@
"noUnusedLocals": true,
"preserveWatchOutput": true,
"types": [
"../../../node_modules/@types/mocha"
"../../../node_modules/@types/mocha",
// HACK: src shouldn't use node types but it's needed for index.test.ts
"../../../node_modules/@types/node"
],
"paths": {
"@xterm/addon-ligatures" : [
@@ -4,61 +4,62 @@
*/
import * as path from 'path';
import * as sinon from 'sinon';
import { assert } from 'chai';
import * as fontFinder from 'font-finder';
import * as ligatureSupport from '.';
// Use require to get a mutable module object (ESM imports create read-only bindings)
const fontFinder = require('font-finder');
const ligatureSupport = require('../out-esbuild/index');
const originalList = fontFinder.list;
describe('LigaturesAddon', () => {
let onRefresh: sinon.SinonStub;
let onRefresh: { called: boolean, callCount: number, (...args: any[]): void };
let term: MockTerminal;
// -> forms a ligature in Fira Code and Iosevka, but www only forms a ligature
// in Fira Code
const input = 'a -> b www c';
before(() => {
sinon.stub(fontFinder, 'list').returns(Promise.resolve({
// eslint-disable-next-line @typescript-eslint/naming-convention
fontFinder.list = () => Promise.resolve({
'Fira Code': [{
path: path.join(__dirname, '../fonts/firaCode.otf'),
style: fontFinder.Style.Regular,
type: fontFinder.Type.Monospace,
weight: 400
}],
// eslint-disable-next-line @typescript-eslint/naming-convention
'Iosevka': [{
path: path.join(__dirname, '../fonts/iosevka.ttf'),
style: fontFinder.Style.Regular,
type: fontFinder.Type.Monospace,
weight: 400
}],
// eslint-disable-next-line @typescript-eslint/naming-convention
'Nonexistant Font': [{
path: path.join(__dirname, '../fonts/nonexistant.ttf'),
style: fontFinder.Style.Regular,
type: fontFinder.Type.Monospace,
weight: 400
}]
} as fontFinder.FontList));
});
});
after(() => {
fontFinder.list = originalList;
});
beforeEach(() => {
onRefresh = sinon.stub();
onRefresh = Object.assign((..._args: any[]) => { onRefresh.called = true; onRefresh.callCount++; }, { called: false, callCount: 0 });
term = new MockTerminal(onRefresh);
ligatureSupport.enableLigatures(term as any);
});
it('registers itself correctly', () => {
const term = new MockTerminal(sinon.spy());
const term = new MockTerminal(() => {});
assert.isUndefined(term.joiner);
ligatureSupport.enableLigatures(term as any);
assert.isFunction(term.joiner);
});
it('registers itself correctly when called directly', () => {
const term = new MockTerminal(sinon.spy());
const term = new MockTerminal(() => {});
assert.isUndefined(term.joiner);
ligatureSupport.enableLigatures(term as any);
assert.isFunction(term.joiner);
@@ -72,14 +73,14 @@ describe('LigaturesAddon', () => {
term.options.fontFamily = 'Nonexistant Font, monospace';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.strictEqual(onRefresh.callCount, 0);
});
it('returns nothing if the font is not present on the system', async () => {
term.options.fontFamily = 'notinstalled';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.strictEqual(onRefresh.callCount, 0);
assert.deepEqual(term.joiner!(input), []);
});
@@ -87,7 +88,7 @@ describe('LigaturesAddon', () => {
term.options.fontFamily = 'monospace';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.strictEqual(onRefresh.callCount, 0);
assert.deepEqual(term.joiner!(input), []);
});
@@ -95,7 +96,7 @@ describe('LigaturesAddon', () => {
term.options.fontFamily = '';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.strictEqual(onRefresh.callCount, 0);
assert.deepEqual(term.joiner!(input), []);
});
@@ -103,7 +104,7 @@ describe('LigaturesAddon', () => {
term.options.fontFamily = {} as any;
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.strictEqual(onRefresh.callCount, 0);
});
});
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ESNext",
"lib": [
"es2021"
],
"rootDir": ".",
"outDir": "../out-esbuild-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
]
}
+2 -1
View File
@@ -2,6 +2,7 @@
"files": [],
"include": [],
"references": [
{ "path": "./src" }
{ "path": "./src" },
{ "path": "./test" }
]
}
-383
View File
@@ -1,383 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0":
version "1.8.0"
resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz"
integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==
dependencies:
type-detect "4.0.8"
"@sinonjs/formatio@^3.0.0", "@sinonjs/formatio@^3.2.1":
version "3.2.2"
resolved "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz"
integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
dependencies:
"@sinonjs/commons" "^1"
"@sinonjs/samsam" "^3.1.0"
"@sinonjs/samsam@^2.1.2":
version "2.1.3"
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz"
integrity sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==
"@sinonjs/samsam@^3.1.0":
version "3.3.3"
resolved "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz"
integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
dependencies:
"@sinonjs/commons" "^1.3.0"
array-from "^2.1.1"
lodash "^4.17.15"
"@sinonjs/text-encoding@^0.7.1":
version "0.7.1"
resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
"@types/sinon@^5.0.1":
version "5.0.1"
resolved "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.1.tgz"
integrity sha512-yxzBCIjE3lp9lYjfBbIK/LRCoXgCLLbIIBIje7eNCcUIIR2CZZtyX5uto2hVoMSMqLrsRrT6mwwUEd0yFgOwpA==
array-from@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz"
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.6.0:
version "1.8.2"
resolved "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
diff@^3.5.0:
version "3.5.0"
resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
dependencies:
pend "~1.2.0"
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
font-finder@^1.0.3, font-finder@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz"
integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==
dependencies:
get-system-fonts "^2.0.0"
promise-stream-reader "^1.0.1"
font-ligatures@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz"
integrity sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==
dependencies:
font-finder "^1.0.3"
lru-cache "^6.0.0"
opentype.js "^0.8.0"
form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-system-fonts@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.0.tgz"
integrity sha512-iiM/DavyF2nnLdELzPBSHojzQJVai9WiwrRzn5gp2dutJuerC8qHyBoh4lxfVdKGbnb9eZ4p8Oefbuc3yExB7Q==
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz"
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
just-extend@^4.0.2:
version "4.1.0"
resolved "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz"
integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash@^4.17.15:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lolex@^2.7.5:
version "2.7.5"
resolved "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz"
integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==
lolex@^5.0.1:
version "5.1.2"
resolved "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz"
integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
dependencies:
"@sinonjs/commons" "^1.7.0"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
minimist@^1.2.5:
version "1.2.6"
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@0.5.5:
version "0.5.5"
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
nise@^1.4.5:
version "1.5.3"
resolved "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz"
integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==
dependencies:
"@sinonjs/formatio" "^3.2.1"
"@sinonjs/text-encoding" "^0.7.1"
just-extend "^4.0.2"
lolex "^5.0.1"
path-to-regexp "^1.7.0"
opentype.js@^0.8.0:
version "0.8.0"
resolved "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz"
integrity sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==
dependencies:
tiny-inflate "^1.0.2"
path-to-regexp@^1.7.0:
version "1.9.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz"
integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==
dependencies:
isarray "0.0.1"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
promise-stream-reader@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz"
integrity sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
sinon@6.3.5:
version "6.3.5"
resolved "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz"
integrity sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==
dependencies:
"@sinonjs/commons" "^1.0.2"
"@sinonjs/formatio" "^3.0.0"
"@sinonjs/samsam" "^2.1.2"
diff "^3.5.0"
lodash.get "^4.4.2"
lolex "^2.7.5"
nise "^1.4.5"
supports-color "^5.5.0"
type-detect "^4.0.8"
supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
tiny-inflate@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz"
integrity sha512-fEndoHfuiSlXtvG0EA1gB4uAdy2dbQ3WfOtQw1/Y8NQ9rxtGbGkENwfGmN0wK7OkxDz1lgc5Ei2LQ0oxKDUcVw==
type-detect@^4.0.8, type-detect@4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
@@ -135,6 +135,7 @@ export class DecorationManager extends Disposable {
marker,
x: range[1],
width: range[2],
layer: isActiveResult ? 'top' : 'bottom',
backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,
+16 -3
View File
@@ -4,8 +4,8 @@
*/
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
import { Event } from 'vs/base/common/event';
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { disposableTimeout } from 'vs/base/common/async';
import { SearchLineCache } from './SearchLineCache';
@@ -42,6 +42,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
private _decorationManager: DecorationManager | undefined;
private _resultTracker = this._register(new SearchResultTracker());
private readonly _onAfterSearch = this._register(new Emitter<void>());
public readonly onAfterSearch = this._onAfterSearch.event;
private readonly _onBeforeSearch = this._register(new Emitter<void>());
public readonly onBeforeSearch = this._onBeforeSearch.event;
public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
return this._resultTracker.onDidChangeResults;
}
@@ -98,6 +103,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
throw new Error('Cannot use addon until it has been loaded');
}
this._onBeforeSearch.fire();
this._state.lastSearchOptions = searchOptions;
if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
@@ -108,6 +115,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
this._fireResults(searchOptions);
this._state.cachedSearchTerm = term;
this._onAfterSearch.fire();
return found;
}
@@ -173,6 +182,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
throw new Error('Cannot use addon until it has been loaded');
}
this._onBeforeSearch.fire();
this._state.lastSearchOptions = searchOptions;
if (this._state.shouldUpdateHighlighting(term, searchOptions)) {
@@ -183,6 +194,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
this._fireResults(searchOptions);
this._state.cachedSearchTerm = term;
this._onAfterSearch.fire();
return found;
}
@@ -209,7 +222,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
* @param result The result to select.
* @returns Whether a result was selected.
*/
private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
if (!this._terminal || !this._decorationManager) {
return false;
}
+1 -3
View File
@@ -198,9 +198,7 @@ export class SearchEngine {
}
}
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
// Search from startRow - 1 to top
if (!result) {
+23 -1
View File
@@ -38,6 +38,9 @@ export class SearchLineCache extends Disposable {
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeout = this._register(new MutableDisposable());
private _linesCacheDisposables = this._register(new MutableDisposable());
// Track access to avoid recreating a timeout on every init call which occurs once per search
// result (findNext/findPrevious -> _highlightAllMatches -> find loop).
private _lastAccessTimestamp = 0;
constructor(private readonly _terminal: Terminal) {
super();
@@ -57,15 +60,34 @@ export class SearchLineCache extends Disposable {
);
}
this._linesCacheTimeout.value = disposableTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
this._lastAccessTimestamp = Date.now();
if (!this._linesCacheTimeout.value) {
this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);
}
}
private _destroyLinesCache(): void {
this._linesCache = undefined;
this._lastAccessTimestamp = 0;
this._linesCacheDisposables.clear();
this._linesCacheTimeout.clear();
}
private _scheduleLinesCacheTimeout(delay: number): void {
this._linesCacheTimeout.value = disposableTimeout(() => {
if (!this._linesCache) {
return;
}
const now = Date.now();
const elapsed = now - this._lastAccessTimestamp;
if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {
this._destroyLinesCache();
return;
}
this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);
}, delay);
}
public getLineFromCache(row: number): LineCacheEntry | undefined {
return this._linesCache?.[row];
}
@@ -385,6 +385,34 @@ test.describe('Search Tests', () => {
});
});
test.describe('onBeforeSearch and onAfterSearch', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window.events = [];
window.search.onBeforeSearch(() => window.events.push('before'));
window.search.onAfterSearch(() => window.events.push('after'));
`);
});
test('should fire before and after findNext', async () => {
await ctx.proxy.write('abc');
await ctx.page.evaluate(`window.search.findNext('a')`);
deepStrictEqual(await ctx.page.evaluate('window.events'), ['before', 'after']);
});
test('should fire before and after findPrevious', async () => {
await ctx.proxy.write('abc');
await ctx.page.evaluate(`window.search.findPrevious('a')`);
deepStrictEqual(await ctx.page.evaluate('window.events'), ['before', 'after']);
});
test('should fire for each search call', async () => {
await ctx.proxy.write('abc abc');
await ctx.page.evaluate(`window.search.findNext('abc')`);
await ctx.page.evaluate(`window.search.findNext('abc')`);
deepStrictEqual(await ctx.page.evaluate('window.events'), ['before', 'after', 'before', 'after']);
});
});
test.describe('Regression tests', () => {
test.describe('#2444 wrapped line content not being found', () => {
let fixture: string;
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+11 -2
View File
@@ -152,8 +152,17 @@ declare module '@xterm/addon-search' {
public clearActiveDecoration(): void;
/**
* When decorations are enabled, fires when
* the search results change.
* Fires after a search is performed.
*/
readonly onAfterSearch: IEvent<void>;
/**
* Fires before a search is performed.
*/
readonly onBeforeSearch: IEvent<void>;
/**
* When decorations are enabled, fires when the search results change.
*/
readonly onDidChangeResults: IEvent<ISearchResultChangeEvent>;
}
+175 -21
View File
@@ -112,13 +112,123 @@ describe('SerializeAddon', () => {
}), 'world');
});
});
describe('underline styles', () => {
it('should serialize single underline with style', async () => {
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
});
it('should serialize double underline', async () => {
await writeP(terminal, sgr('4:2') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:2mtest\u001b[0m');
});
it('should serialize curly underline', async () => {
await writeP(terminal, sgr('4:3') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:3mtest\u001b[0m');
});
it('should serialize dotted underline', async () => {
await writeP(terminal, sgr('4:4') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:4mtest\u001b[0m');
});
it('should serialize dashed underline', async () => {
await writeP(terminal, sgr('4:5') + 'test' + sgr('24'));
assert.equal(serializeAddon.serialize(), '\u001b[4:5mtest\u001b[0m');
});
it('should serialize underline with RGB color', async () => {
await writeP(terminal, sgr('4', '58;2;255;128;64') + 'test' + sgr('24'));
const result = serializeAddon.serialize();
assert.ok(result.includes('4:1'), result);
assert.ok(result.includes('58:2::255:128:64'), result);
});
it('should serialize underline with palette color', async () => {
await writeP(terminal, sgr('4', '58;5;46') + 'test' + sgr('24'));
const result = serializeAddon.serialize();
assert.ok(result.includes('4:1'), result);
assert.ok(result.includes('58:5:46'), result);
});
});
describe('scroll region', () => {
let scrollTerminal: Terminal;
let scrollAddon: SerializeAddon;
beforeEach(() => {
scrollTerminal = new Terminal({ cols: 10, rows: 5, allowProposedApi: true });
scrollAddon = new SerializeAddon();
scrollTerminal.loadAddon(scrollAddon);
});
it('should serialize scroll region when margins are set', async () => {
await writeP(scrollTerminal, '\x1b[2;4r');
const buffer = (scrollTerminal as any)._core.buffer;
assert.equal(buffer.scrollTop, 1, 'scrollTop should be 1');
assert.equal(buffer.scrollBottom, 3, 'scrollBottom should be 3');
const result = scrollAddon.serialize();
assert.ok(result.includes('\x1b[2;4r'), result);
});
it('should not serialize scroll region when excludeModes is true', async () => {
await writeP(scrollTerminal, '\x1b[2;4r');
const result = scrollAddon.serialize({ excludeModes: true });
assert.ok(!result.includes('\x1b[2;4r'), result);
});
it('should restore scroll region correctly when deserialized', async () => {
await writeP(scrollTerminal, '\x1b[2;4r');
const serialized = scrollAddon.serialize();
const terminal2 = new Terminal({ cols: 10, rows: 5, allowProposedApi: true });
terminal2.loadAddon(new SerializeAddon());
await writeP(terminal2, serialized);
const buffer = (terminal2 as any)._core.buffer;
assert.equal(buffer.scrollTop, 1);
assert.equal(buffer.scrollBottom, 3);
});
});
describe('cursor visibility', () => {
it('should serialize hidden cursor', async () => {
await writeP(terminal, 'hello\x1b[?25l');
assert.equal(terminal.modes.showCursor, false);
const result = serializeAddon.serialize();
assert.ok(result.includes('\x1b[?25l'), result);
});
it('should not serialize visible cursor (default state)', async () => {
await writeP(terminal, 'hello');
assert.equal(terminal.modes.showCursor, true);
const result = serializeAddon.serialize();
assert.ok(!result.includes('\x1b[?25l'), result);
assert.ok(!result.includes('\x1b[?25h'), result);
});
it('should not serialize cursor visibility when excludeModes is true', async () => {
await writeP(terminal, 'hello\x1b[?25l');
const result = serializeAddon.serialize({ excludeModes: true });
assert.ok(!result.includes('\x1b[?25l'), result);
});
it('should restore hidden cursor correctly when deserialized', async () => {
await writeP(terminal, 'hello\x1b[?25l');
const serialized = serializeAddon.serialize();
const terminal2 = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
terminal2.loadAddon(new SerializeAddon());
await writeP(terminal2, serialized);
assert.equal(terminal2.modes.showCursor, false);
});
});
});
describe('html', () => {
it('empty terminal with selection turned off', () => {
const output = serializeAddon.serializeAsHTML();
assert.notEqual(output, '');
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) ?? []).length, 2);
});
it('empty terminal with no selection', () => {
@@ -135,7 +245,7 @@ describe('SerializeAddon', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) ?? []).length, 1, output);
});
it('basic terminal with html unsafe chars', async () => {
@@ -145,7 +255,7 @@ describe('SerializeAddon', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal((output.match(/<div><span>&lt;a>&amp;pi;<\/span><\/div>/g) || []).length, 1, output);
assert.equal((output.match(/<div><span>&lt;a>&amp;pi;<\/span><\/div>/g) ?? []).length, 1, output);
});
it('serializes rows within a provided range', async () => {
@@ -157,7 +267,7 @@ describe('SerializeAddon', () => {
startCol: 4
}
});
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) || [];
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) ?? [];
assert.equal(rowMatches.length, 1, output);
assert.ok(rowMatches[0]?.includes('hello'));
assert.ok(!output.includes('bye'));
@@ -168,77 +278,121 @@ describe('SerializeAddon', () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with italic styling', async () => {
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with inverse styling', async () => {
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with underline styling', async () => {
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with double underline styling', async () => {
await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with curly underline styling', async () => {
await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with dotted underline styling', async () => {
await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with dashed underline styling', async () => {
await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with underline color (palette)', async () => {
await writeP(terminal, ' ' + sgr('4', '58;5;46') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.ok(output.includes('text-decoration: underline;'), output);
assert.ok(output.includes('text-decoration-color: #00ff00;'), output);
});
it('cells with underline color (RGB)', async () => {
await writeP(terminal, ' ' + sgr('4', '58;2;255;128;64') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.ok(output.includes('text-decoration: underline;'), output);
assert.ok(output.includes('text-decoration-color: #ff8040;'), output);
});
it('cells with invisible styling', async () => {
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with dim styling', async () => {
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with strikethrough styling', async () => {
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with combined styling', async () => {
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) ?? []).length, 1, output);
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) ?? []).length, 1, output);
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) ?? []).length, 1, output);
});
it('cells with color styling', async () => {
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);
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with background styling', async () => {
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
});
it('empty terminal with custom options', async () => {
@@ -251,14 +405,14 @@ describe('SerializeAddon', () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) ?? []).length, 1, output);
});
it('empty terminal with background included', async () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
});
it('cells with custom color styling', async () => {
@@ -268,7 +422,7 @@ describe('SerializeAddon', () => {
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);
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) ?? []).length, 1, output);
});
it('cells with color styling - xterm headless', async () => {
@@ -278,7 +432,7 @@ describe('SerializeAddon', () => {
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);
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
});
});
});
+129 -24
View File
@@ -9,6 +9,7 @@ import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } fro
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
import { IAttributeData, IColor } from 'common/Types';
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
import { UnderlineStyle } from 'common/buffer/Constants';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -82,10 +83,24 @@ function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boole
&& cell1.getBgColor() === cell2.getBgColor();
}
function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
// If neither cell has underline, consider them equal regardless of internal underline color
// values
if (!cell1.isUnderline() && !cell2.isUnderline()) {
return true;
}
const cell1Data = cell1 as unknown as IAttributeData;
const cell2Data = cell2 as unknown as IAttributeData;
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
}
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
&& equalUnderline(cell1, cell2)
&& cell1.isOverline() === cell2.isOverline()
&& cell1.isBlink() === cell2.isBlink()
&& cell1.isInvisible() === cell2.isInvisible()
@@ -274,7 +289,27 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (flagsChanged) {
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 (!equalUnderline(cell, oldCell)) {
const cellData = cell as unknown as IAttributeData;
const style = cellData.getUnderlineStyle();
if (style === UnderlineStyle.NONE) {
sgrSeq.push(24);
} else {
// Use SGR 4:X format for underline styles
sgrSeq.push('4:' + style as unknown as number);
// Handle underline color
if (!cellData.isUnderlineColorDefault()) {
const color = cellData.getUnderlineColor();
if (cellData.isUnderlineColorRGB()) {
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
} else {
sgrSeq.push('58:5:' + color as unknown as number);
}
}
}
} else 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); }
@@ -422,7 +457,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
}
export class SerializeAddon implements ITerminalAddon , ISerializeApi {
export class SerializeAddon implements ITerminalAddon, ISerializeApi {
private _terminal: Terminal | undefined;
public activate(terminal: Terminal): void {
@@ -478,6 +513,25 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
return '';
}
/**
* Serializes the scroll region (DECSTBM) if it's not set to the full terminal size.
* Uses internal API access since scroll region is not exposed in the public API.
*/
private _serializeScrollRegion(terminal: Terminal): string {
// HACK: Internal API access since scroll region is not exposed in the public API
const buffer = (terminal as any)._core.buffer;
const scrollTop: number = buffer.scrollTop;
const scrollBottom: number = buffer.scrollBottom;
// Only serialize if scroll region is not the default (full terminal size)
if (scrollTop !== 0 || scrollBottom !== terminal.rows - 1) {
// DECSTBM uses 1-based indices: CSI Ps ; Ps r
return `\x1b[${scrollTop + 1};${scrollBottom + 1}r`;
}
return '';
}
private _serializeModes(terminal: Terminal): string {
let content = '';
const modes = terminal.modes;
@@ -505,6 +559,12 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
}
}
// Cursor visibility (DECTCEM)
// Default: visible
if (!modes.showCursor) {
content += '\x1b[?25l';
}
return content;
}
@@ -527,9 +587,10 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
}
}
// Modes
// Modes and scroll region
if (!options?.excludeModes) {
content += this._serializeModes(this._terminal);
content += this._serializeScrollRegion(this._terminal);
}
return content;
@@ -540,7 +601,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
throw new Error('Cannot use addon until it has been loaded');
}
return this._serializeBufferAsHTML(this._terminal, options || {});
return this._serializeBufferAsHTML(this._terminal, options ?? {});
}
public dispose(): void { }
@@ -569,20 +630,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
}
}
private _padStart(target: string, targetLength: number, padString: string): string {
targetLength = targetLength >> 0;
padString = padString ?? ' ';
if (target.length > targetLength) {
return target;
}
targetLength -= target.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return padString.slice(0, targetLength) + target;
}
protected _beforeSerialize(rows: number, start: number, end: number): void {
this._htmlContent += '<html><body><!--StartFragment--><pre>';
@@ -619,7 +666,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
(color >> 8) & 255,
(color ) & 255
];
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
}
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
return this._ansiColors[color].css;
@@ -627,6 +674,42 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
return undefined;
}
private _getUnderlineColor(cell: IBufferCell): string | undefined {
const cellData = cell as unknown as IAttributeData;
if (cellData.isUnderlineColorDefault()) {
return undefined;
}
const color = cellData.getUnderlineColor();
if (cellData.isUnderlineColorRGB()) {
const rgb = [
(color >> 16) & 255,
(color >> 8) & 255,
(color ) & 255
];
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
}
// Palette color
return this._ansiColors[color].css;
}
private _getUnderlineStyle(cell: IBufferCell): string {
const cellData = cell as unknown as IAttributeData;
switch (cellData.getUnderlineStyle()) {
case UnderlineStyle.SINGLE:
return 'underline';
case UnderlineStyle.DOUBLE:
return 'underline double';
case UnderlineStyle.CURLY:
return 'underline wavy';
case UnderlineStyle.DOTTED:
return 'underline dotted';
case UnderlineStyle.DASHED:
return 'underline dashed';
default:
return 'underline';
}
}
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
const content: string[] = [];
@@ -647,14 +730,36 @@ 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() && 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;'); }
// Handle text-decoration (underline, overline, strikethrough, blink)
const decorations: string[] = [];
if (cell.isUnderline()) {
decorations.push(this._getUnderlineStyle(cell));
}
if (cell.isOverline()) {
decorations.push('overline');
}
if (cell.isStrikethrough()) {
decorations.push('line-through');
}
if (cell.isBlink()) {
decorations.push('blink');
}
if (decorations.length > 0) {
content.push('text-decoration: ' + decorations.join(' ') + ';');
}
// Handle underline color
if (cell.isUnderline()) {
const underlineColor = this._getUnderlineColor(cell);
if (underlineColor) {
content.push('text-decoration-color: ' + underlineColor + ';');
}
}
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
if (cell.isItalic()) { content.push('font-style: italic;'); }
if (cell.isDim()) { content.push('opacity: 0.5;'); }
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
return content;
}
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
@@ -9,19 +9,15 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes';
import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi {
export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi {
private _provider15Graphemes?: UnicodeGraphemeProvider;
private _provider15?: UnicodeGraphemeProvider;
private _unicode?: IUnicodeHandling;
private _oldVersion: string = '';
public activate(terminal: Terminal): void {
if (! this._provider15) {
this._provider15 = new UnicodeGraphemeProvider(false);
}
if (! this._provider15Graphemes) {
this._provider15Graphemes = new UnicodeGraphemeProvider(true);
}
this._provider15 ??= new UnicodeGraphemeProvider(false);
this._provider15Graphemes ??= new UnicodeGraphemeProvider(true);
const unicode = terminal.unicode;
this._unicode = unicode;
unicode.register(this._provider15);
@@ -1,7 +1,7 @@
import UnicodeTrie from './unicode-trie';
const trieRaw = "AAARAAAAAABwxwAAAb4LQfTtmw+sVmUdx58LL/ffe/kjzNBV80gW1F3yR+6CvbJiypoZa0paWmAWSluErSBbFtYkkuZykq6QamGJ4WRqo2kFGy6dYWtEq6G1MFAJbRbOVTQr+x7f5+x97q/n/3me87wXzm/3s+f/7/d7/p7znnvOlvGMbQM7wIPgEbAPHABPgcPgefAS+BfYwuv/F/Q2OulBxKcK6TMRPxu8FcwFbwcjYCFYDC4Cl4ArwNXgGvBJsA58UdBDwy+jbBO4La8DtoEd4H7wkNBuN+KPgn3gADgIngaHwFHwF/AyeAWMm4C+TGi3LdiJ/EnIex04A2RgFpgD5oKFYDG4CLwHXAo+IKSvAqt4/evA9bz9jWA6+Cq3dyvCP8HWNwX93wF38/ROcD94SCjP2+1B+BiPP4HwgOD/7xD/I08fRniMx48jPAFeBeuF+n29jE0G08FZvaPHYWZvh9mcEfAOjlhXx/qGfd2QvLO3zccmtMnzliC9lPt+GenD1nyMiK/LNf1cycs+gfAzPJ6vtxe4jhuQtx5sBLeA28G3eb3v8/Beif4HkPewxu5G6N/rMP4qfgEdvwZPgj+AZ8Cx3nYfxiE8Dk6AV0FfH/YEOB28AbwJDIPzQAtcAC4Gl/Z19F+J+NVCehWPr0b46b7RvixvdPg8yr7U10l/BfFN4La8DdgGdoAHwU/AI2AfOACeAofB8+AlcAKwfvyBKeCM/o7NrF9PXmdWv9/Ynot2I7ztIg8dF5I2a8i63CjZU+9Fm2Wcy4U4ZQVYyeOrwVoev57UuxHcJKRvFuJXgnU8/nUebtbYrKmpCUOx31P7UVNTU1NTU1NTU1OGLTz8Xr/77+W7+9vP0or0MxPMbXaizY8FW3sQ3wseB/t5/kGEh8DR/vbzwL8i/Af4Dy8fP8BYE0weaKenI/wV/DhrQG97JspngzlgLpgHzgPzwUhdVpfVZXVZXRa87HxwAVgQ4Pn5WEd85l5TUzOasvezFw/E3b/LoP9D4CpwrcTWWsGXNQOj748/G9k3G56d1KYxmbELwQbwKFiJvBM8nDWlHa5E+AOwCzwLzjkNeeB28NvTeB1OYyr0gQ1g99R23nGE50xj7MPgc+A+8K5Bxj4FHgB/G2z/T9XEzCZjd/S0WYX4Pc3/r/Nn5I0f6qQXIP5x8ENwBMyYyNhHJ3b0pOCuLrBvM941NTU1JyNHEp+BrC8dMyalt1/m3uWfhmeULzRGp9d3wf0WZSN8+prCr60Wz09tuNmx35sl9Y825HXvRN39KNveaL8flb9f913kbec67kHeTsR3gYcH2uV7ED4m2HhCYi/X9ZuBzvuXv0f8iKIfx5B/XCg7gTgbVPdvAsomCuWnD45eK28UyvL3Jt+s0fU2TVnOXJQvJHUWIb0ELAWXgCt4+UcMumSsEtpch/g6ouMGpG/ieZsc9N/q4YsLd3D9WyPbsWEbfNgO7hN82TWY/n8xKbmsC3xQsYKf+7sjrx2TH+u4H3vhx+OO6+X9hmtXN7C/4r15EPaeBs9J7L7YBeeED/k7wn8fbIf/Rji+yVizmd4vW6bB19cb/PU9w7MxMA60bzPHgM8+zG623+OnzOf55yNc3Gw/k303wveBy3nZcoTXgNVgLfiCRNcG5N3SbIebwZ08fhe4l8d/BH7K4yI/4+HPwS/BAfBks+PzIaHuc3x+ivSL4GUyZ68I6fwZYRNMG2qnz+Th2QjfMtTx/1zE5w61nyN+Q7C3aKgdin1dgrylYBn4INdhGn/Z2FfFiqH01/SUXMvnPD+jC+j85N/RqRhR/DYaS6T+P09K1mD+vzW+5zVqqeVUl0wTz2lK8odJHRGXfBufdGLSoSo3+ZFJ6sl0qvJVNmhI4z4i06mrZ6uT1le1z5h5HE3tMiHPtQ5javu+ItMXUr/MXpmwmyRL3D6U7UwIMyYfczGu0qdqb2pbhcw4xQkhWQBMerrZ/liXrGTbsQwTwrEu4zSczKLrd7fCSKiKn+zSo8BWXMe8myXWOivrUxWi60OPoQ7VIasbQ0S/Ukk3rZVullNhHEL1rYoxUF0PTfm6elWJzq54ZsU4z11ohOy0oxT2izFqCNj4TesXcWZo6+Jfqr1O+1O1beqDagypj2J9F1u2daucj3Eknmq/6PaHrK7Mb1o35DiW1a/a76LuhlDXZX25SOz11S33ErKxDb2/fc/bFKI6axskn+4/W90u9mOtbRf7smsoTdvOfwoRz0t6DaP9k81v6P7Re5aUQudTd303rX+bZzBl97/KR7E+Xbux9lLI+aNr1PfaYLpPDiW2/vrYTX1drMIeXbMye6HXlw8292Jl7ZXxLxRlxXbcaH9drjFlxfa3Qozx8NWRi834lPVZbD+SmN7EJPzc9TVCSVXXDps9L+513b2J7fMu176V2YOhx1A3JrJ8KrLxUumpcu5j/lYT+2tzLRVDZmhjO442a1Clu0ox9VPVXzE/lcS4V0k1D6LI1pJsz8fct9SGbO5l/rmKzTlvsxdj3IvRtC2uv0t1fotltvd2VaCy5Sp5m0EhnZG4CCNxXZrWp/VUIrOjapfnNw11ZNI0V/GWzKNuxtzGKKTEtJeR0NVmpojbtBuW5On0u0is9ZMxvU8ZM+8vEyadtu10oqtP9Q4rcJEm85+Two/QkpGwjI6YkgkhtUfzZOW6fFVexuRri+qj9TJJHZkdmW5abiu0rs6uj2TMfmx06bISUj9tZ9Lja8dVQtox6WpxTJKfW3M4MSTmvU4sWy1CU6BF4jIfdNeDjHWuO1lCWIm2Jr2ixNZvklD2fP0Q6+vsmO4hqN1hJvfDtV5G8mTlsvau4qPP1a64L1skT6QYEzEtq0PzGZOfCbSdSmcKTP7Qs86Ej/1hEpelaV6IMdT5ayu2+nT9tmnnO746XbLxE8t0qOrYtJWhmk9bvaLfsrotRVw1PnR+bcafSUKZ6Mps7smobybJLH2R6WqRkJa1DHV0UmbfUcksiSF0HExSpp+uY0zbTklMaCm7blzEtg8h1rNMXNaYi05ZXsbC75sQ/4+aUxFV2jL50Q3jE0rK2rVtN09By8OHoo1vH2LPSdE323mr2sdu0pUZiDkWLRKWnfeQY6taKzHF9n/GPv8jd/0/egiRvYMR24fU79iY3s9Qva9RlYR8n8HHtq9fMcT1HRWfdZXiHd9YInt/iI4PTaf+BimXKvdXYU+3hlRpHzs2dVK/cxhDn+xs0I2jzxjL5kpXz1VU72aLtkK/97sALKyQqu25SshvG6h08/cLrlKswRklKXvvXfa+pZt+y8nah5YUv2Oo/ap/X2URdRfico9K69hcp6r6XaCz5Wo/hs/iNTGF6N6tV92/9ZS0Wba9SlT3pKF/e6W674+x9ly+VRL73cPU8ygb31D3eSqfVd+iqET0y3YMYojoO11XqrTt2nPxmeq1HYeqxkmUMt8DiesjpoTSr+qDrD+qPZDiOZxMdH0pRPX8MFUfQtv0Xbs+a1a1NnRryNZ/2+tsaPG5ZoX0RXZei88yZGdo4UMPj/cwv/kMJboxLISuQbE+1VW12Mx7FWOrW3M9Hv7Y+uxyraPSo8B2TGPuLdOeZha+hBKf8Sjsm/oR+7pmsx/oeOraFWdXleeV6oyl41zm+mgSuq9C6ox1TsU8D+m4dwMmf8v2nz7Tm+fYfj7HV1K/x1HWjquvY+2dllxM64ue87Su772zzbXIVC+WxLZTRR9MdkMTypZNH1z6G0tUvoccwxA+hfLNdV+a7MaQqscztMi+7QnxDZXvd1dldWQOyMbApb1Jd2h91Ffx+y9Xfb7tClokboOvrRhrbVpFFO8z+65t2/u4su9MUx028znH01/TGVDmHAj13W1o+1USw+eUfYtpO+b82rRNsb6oPpV+1fdBqddB6n3WDXvdJDZrJ0QfQp6bsc/kqq4BIddHWXGdN1pmWveh58F1zYUW1zmOITHXWOg1XrZvZSWUf77tq1ofqear6muaT1lIQp3bofabSafJVlnfYo9B6LGr8uzz2Xchvzfw+T9PlgiV/A8=";
declare const Buffer: any;
declare const Buffer: { from(s: string, encoding: string): Uint8Array } | undefined;
function _dec(s: string): Uint8Array {
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
const bs = atob(s);
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+1 -1
View File
@@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon } from '@xterm/xterm';
import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11';
import { UnicodeV11 } from './UnicodeV11';
export class Unicode11Addon implements ITerminalAddon , IUnicode11Api {
export class Unicode11Addon implements ITerminalAddon, IUnicode11Api {
public activate(terminal: Terminal): void {
terminal.unicode.register(new UnicodeV11());
}
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2024, 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.

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