buninu-browser 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +116 -0
  20. package/frontends/wayland/input.js +56 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -0,0 +1,606 @@
1
+ // Address bar / search bar (always visible)
2
+ // Line 1 of the terminal always shows the current URL
3
+ // Alt+L to enter edit mode → Enter to navigate, Escape to cancel
4
+
5
+ import { mkdirSync, writeFileSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+ // Wayland adapter: no terminal cursor escapes.
9
+ const hideCursor = () => {};
10
+ import { loadConfig, loadJsonFile } from './config.js';
11
+ import { searchBookmarks } from './bookmarks.js';
12
+ import { displayWidth, fitWidth, promptView, visualColToCharIdx } from './prompt-width.js';
13
+
14
+ const PROMPTS = { url: ' > ', cmd: 'cmd> ' };
15
+ const HISTORY_MAX = 100;
16
+ const DOUBLE_CLICK_MS = 400;
17
+ // The sequences terminals send for these keys (normal and application cursor
18
+ // mode, VT220 and rxvt Home/End), as jsmdcui recognizes them.
19
+ const LEFT_KEYS = new Set(['\x1b[D', '\x1bOD', '\x1b[1;5D', '\x1b[5D']);
20
+ const RIGHT_KEYS = new Set(['\x1b[C', '\x1bOC', '\x1b[1;5C', '\x1b[5C']);
21
+ const HOME_KEYS = new Set(['\x1b[H', '\x1bOH', '\x1b[1~', '\x1b[7~']);
22
+ const END_KEYS = new Set(['\x1b[F', '\x1bOF', '\x1b[4~', '\x1b[8~']);
23
+ export const HISTORY_PATH = join(homedir(), '.casty', 'history.json');
24
+
25
+ function hasScheme(str) {
26
+ return /^[a-z][a-z0-9+.-]*:/i.test(str);
27
+ }
28
+
29
+ function isLocalAddress(str) {
30
+ return /^(?:localhost|127(?:\.\d{1,3}){3})(?::\d+)?(?:[/?#]|$)/i.test(str)
31
+ || /^\[::1\](?::\d+)?(?:[/?#]|$)/i.test(str);
32
+ }
33
+
34
+ function isPublicHostname(str) {
35
+ return /^[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?::\d+)?(?:[/?#]|$)/.test(str);
36
+ }
37
+
38
+ export function toURL(input) {
39
+ if (isLocalAddress(input)) return 'http://' + input;
40
+ if (hasScheme(input)) return input;
41
+ if (isPublicHostname(input)) return 'https://' + input;
42
+
43
+ // /b [query] → bookmark search
44
+ const bm = input.match(/^\/b(?:\s+(.+))?$/);
45
+ if (bm) {
46
+ const results = searchBookmarks(bm[1] || '');
47
+ if (results.length > 0) return results[0].url;
48
+ return null; // No match
49
+ }
50
+
51
+ const config = loadConfig();
52
+ return config.searchUrl + encodeURIComponent(input);
53
+ }
54
+
55
+ // padEnd by display width, cut to fit the line
56
+ function padEndByWidth(str, totalW) {
57
+ const fitted = fitWidth(str, totalW);
58
+ return fitted + ' '.repeat(Math.max(0, totalW - displayWidth(fitted)));
59
+ }
60
+
61
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
62
+
63
+ // Entered lines per mode ({ url: [...], cmd: [...] }), oldest first, like
64
+ // jsmdcui's prompt history. file: null keeps it in memory only.
65
+ export class PromptHistory {
66
+ constructor(file = HISTORY_PATH) {
67
+ this.file = file;
68
+ const data = file ? loadJsonFile(file, {}) : {};
69
+ this.entries = {};
70
+ for (const mode of Object.keys(PROMPTS)) {
71
+ this.entries[mode] = Array.isArray(data?.[mode]) ? data[mode].filter(e => typeof e === 'string') : [];
72
+ }
73
+ }
74
+
75
+ list(mode) { return this.entries[mode]; }
76
+
77
+ add(mode, line) {
78
+ if (!line) return;
79
+ const list = this.entries[mode];
80
+ const index = list.indexOf(line);
81
+ if (index >= 0) list.splice(index, 1);
82
+ list.push(line);
83
+ if (list.length > HISTORY_MAX) list.splice(0, list.length - HISTORY_MAX);
84
+ if (!this.file) return;
85
+ try {
86
+ mkdirSync(dirname(this.file), { recursive: true });
87
+ writeFileSync(this.file, JSON.stringify(this.entries));
88
+ } catch {}
89
+ }
90
+ }
91
+
92
+ export class UrlBar {
93
+ constructor({ history = new PromptHistory(), commandEntry = false } = {}) {
94
+ this.commandEntry = commandEntry;
95
+ this.currentUrl = '';
96
+ this.editing = false;
97
+ this.loading = false;
98
+ this._spinIdx = 0;
99
+ this._status = null;
100
+ this.text = '';
101
+ this.cursor = 0;
102
+ this._savedHead = '';
103
+ this._savedTail = '';
104
+ this.selectionAnchor = null;
105
+ this._drag = null;
106
+ this._resolve = null;
107
+ this._dirty = true; // Needs re-render
108
+ this._spinTimer = null; // Spinner update interval
109
+ this.mode = 'url'; // 'url' (address) or 'cmd' (Ctrl+E command line)
110
+ this.history = history;
111
+ this._historyIndex = 0;
112
+ this._savedInput = '';
113
+ this._lastClick = { time: 0, zone: null };
114
+ this._scrollX = 0; // Horizontal scroll of the edit line, as in jsmdcui
115
+ }
116
+
117
+ get prompt() { return PROMPTS[this.mode]; }
118
+ get selectionRange() {
119
+ return this.selectionAnchor === null || this.selectionAnchor === this.cursor ? null
120
+ : [Math.min(this.selectionAnchor, this.cursor), Math.max(this.selectionAnchor, this.cursor)];
121
+ }
122
+ get selectedText() {
123
+ const range = this.selectionRange;
124
+ return range ? [...this.text].slice(...range).join('') : '';
125
+ }
126
+ get selectAll() {
127
+ const range = this.selectionRange;
128
+ return Boolean(range && range[0] === 0 && range[1] === [...this.text].length);
129
+ }
130
+ set selectAll(value) {
131
+ this.selectionAnchor = value ? 0 : null;
132
+ if (value) this.cursor = [...this.text].length;
133
+ }
134
+ moveCursor(position, extend = false) {
135
+ if (extend) this.selectionAnchor ??= this.cursor;
136
+ else this._deselect();
137
+ this.cursor = Math.max(0, Math.min([...this.text].length, position));
138
+ this.render();
139
+ }
140
+
141
+
142
+ // Update current URL and re-render (if not editing)
143
+ setUrl(url) {
144
+ this.currentUrl = url;
145
+ this._dirty = true;
146
+ if (!this.editing) this.render();
147
+ }
148
+
149
+ // Status message (downloads, etc.)
150
+ setStatus(msg) { this._status = msg; this._dirty = true; }
151
+ clearStatus() { this._status = null; this._dirty = true; }
152
+
153
+ // Render only if content changed (for frame callback)
154
+ renderIfDirty() {
155
+ if (this._dirty) {
156
+ this.render();
157
+ } else if (this.editing) {
158
+ // Kitty frames position the terminal cursor on row 2. Keep video live
159
+ // while editing, restoring only the input cursor rather than the row.
160
+ const view = this._editView(this.columns ?? 80);
161
+ const visibility = this.selectionRange ? '\x1b[?25l' : '\x1b[?25h';
162
+ process.stdout.write(`\x1b[1;${view.cursorCol + 1}H${visibility}`);
163
+ }
164
+ }
165
+
166
+ // Render on line 1
167
+ render() {
168
+ this._dirty = false;
169
+ this._updateSpinTimer();
170
+ const cols = this.columns ?? 80;
171
+ if (!this.editing) {
172
+ process.stdout.write(`\x1b[1;1H${this._displayLine(cols)}\x1b[0m`);
173
+ return;
174
+ }
175
+ const view = this._editView(cols);
176
+ // Like jsmdcui, hide the block cursor during selection: it otherwise
177
+ // makes the exclusive right endpoint look like one more selected cell.
178
+ const cursorVisibility = this.selectionRange ? '\x1b[?25l' : '\x1b[?25h';
179
+ process.stdout.write(`\x1b[?25l\x1b[1;1H${this._editLine(cols, view)}\x1b[0m\x1b[1;${view.cursorCol + 1}H${cursorVisibility}`);
180
+ }
181
+
182
+ // The edit line's horizontal scroll: long text scrolls so the cursor stays
183
+ // visible, and Home brings back its start (jsmdcui's prompt).
184
+ _editView(cols) {
185
+ // At the start of the text (Home, or Left there) the whole prompt shows
186
+ // too, instead of only scrolling as far as the cursor as jsmdcui does.
187
+ if (this.cursor === 0) this._scrollX = 0;
188
+ const before = [...this.text].slice(0, this.cursor).join('');
189
+ const view = promptView(this.prompt, this.text, before.length, cols, this._scrollX);
190
+ this._scrollX = view.scrollX;
191
+ return view;
192
+ }
193
+
194
+ // Start/stop spinner timer (100ms interval instead of every frame)
195
+ _updateSpinTimer() {
196
+ if (this.loading && !this.commandEntry && !this._spinTimer) {
197
+ this._spinTimer = setInterval(() => { this._dirty = true; }, 100);
198
+ } else if ((!this.loading || this.commandEntry) && this._spinTimer) {
199
+ clearInterval(this._spinTimer);
200
+ this._spinTimer = null;
201
+ }
202
+ }
203
+
204
+ _displayLine(cols) {
205
+ if (this.commandEntry) {
206
+ const content = this._status || this.currentUrl;
207
+ const color = this.loading ? 214 : 81;
208
+ return `\x1b[48;5;236m\x1b[38;5;${color}m${fitWidth(' @ ', cols)}\x1b[38;5;250m${padEndByWidth(content, Math.max(0, cols - 3))}`;
209
+ }
210
+ let prefix;
211
+ if (this.loading) {
212
+ prefix = ' ' + SPINNER[this._spinIdx++ % SPINNER.length] + ' ';
213
+ } else {
214
+ prefix = ' ';
215
+ }
216
+ const content = this._status || this.currentUrl;
217
+ const full = prefix + content;
218
+ return `\x1b[38;5;250m\x1b[48;5;236m${padEndByWidth(full, cols)}`;
219
+ }
220
+
221
+ _editLine(cols, view = this._editView(cols)) {
222
+ const prefix = this.prompt;
223
+ const visible = fitWidth((prefix + this.text).slice(view.start), cols - view.lead);
224
+ const shownPrefix = prefix.slice(Math.min(view.start, prefix.length));
225
+ const label = ' '.repeat(view.lead) + visible.slice(0, shownPrefix.length);
226
+ const text = visible.slice(shownPrefix.length);
227
+ const pad = ' '.repeat(Math.max(0, cols - view.lead - displayWidth(visible)));
228
+ const range = this.selectionRange;
229
+ let highlighted = text;
230
+ if (range) {
231
+ const chars = [...this.text];
232
+ const visibleStart = Math.max(0, view.start - prefix.length);
233
+ const from = Math.max(0, Math.min(text.length, chars.slice(0, range[0]).join('').length - visibleStart));
234
+ const to = Math.max(0, Math.min(text.length, chars.slice(0, range[1]).join('').length - visibleStart));
235
+ if (to > from) highlighted = text.slice(0, from) + '\x1b[7m' + text.slice(from, to) + '\x1b[27m' + text.slice(to);
236
+ }
237
+ return `\x1b[97m\x1b[48;5;24m${label}${highlighted}${pad}`;
238
+ }
239
+
240
+ // Start editing. Resolves { mode: 'url', url } (url null when cancelled or
241
+ // no bookmark matched) or { mode: 'cmd', line }, or null when cancelled.
242
+ // command: true starts in "cmd> " mode with an empty line; text: start from
243
+ // this text (not selected, cursor at the end) instead.
244
+ startEditing({ command = false, text = null } = {}) {
245
+ this.editing = true;
246
+ this.mode = command ? 'cmd' : 'url';
247
+ this.text = text ?? (command ? '' : this.currentUrl);
248
+ this.cursor = [...this.text].length;
249
+ this.selectAll = !command && text === null;
250
+ this._drag = null;
251
+ this._lastClick = { time: 0, zone: null };
252
+ this._scrollX = 0;
253
+ this._resetHistory();
254
+ this.render();
255
+ let resolve;
256
+ const promise = new Promise(r => { resolve = r; });
257
+ this._resolve = resolve;
258
+ return promise;
259
+ }
260
+
261
+ // Leave editing as if Escape was pressed
262
+ cancelEditing() {
263
+ if (this.editing) this._finishEditing(null);
264
+ }
265
+
266
+ // End editing mode
267
+ _finishEditing(result) {
268
+ this.editing = false;
269
+ this._drag = null;
270
+ hideCursor();
271
+
272
+ let value = null;
273
+ if (result) {
274
+ this.history.add(this.mode, result);
275
+ if (this.mode === 'cmd') {
276
+ value = { mode: 'cmd', line: result };
277
+ } else {
278
+ const url = toURL(result);
279
+ if (url === null) {
280
+ // Bookmark not found
281
+ this.setStatus('Bookmark not found');
282
+ setTimeout(() => this.clearStatus(), 2000);
283
+ }
284
+ value = { mode: 'url', url };
285
+ }
286
+ }
287
+
288
+ this.render();
289
+ if (this._resolve) {
290
+ this._resolve(value);
291
+ this._resolve = null;
292
+ }
293
+ }
294
+
295
+ // Switch between the address and command prompts, keeping the text
296
+ toggleMode() {
297
+ this.mode = this.mode === 'url' ? 'cmd' : 'url';
298
+ this._scrollX = 0;
299
+ this._deselect();
300
+ this._resetHistory();
301
+ this.render();
302
+ }
303
+
304
+ _resetHistory() {
305
+ this._historyIndex = this.history.list(this.mode).length;
306
+ this._savedInput = '';
307
+ }
308
+
309
+ _setText(text) {
310
+ this.text = text;
311
+ this.cursor = [...text].length;
312
+ this._deselect();
313
+ this.render();
314
+ }
315
+
316
+ historyUp() {
317
+ const list = this.history.list(this.mode);
318
+ if (this._historyIndex === list.length) this._savedInput = this.text;
319
+ if (this._historyIndex > 0) this._setText(list[--this._historyIndex]);
320
+ }
321
+
322
+ historyDown() {
323
+ const list = this.history.list(this.mode);
324
+ if (this._historyIndex < list.length) {
325
+ this._historyIndex++;
326
+ this._setText(this._historyIndex === list.length ? this._savedInput : list[this._historyIndex]);
327
+ }
328
+ }
329
+
330
+ // Left press on line 1 (col is 1-based), as in jsmdcui:
331
+ // - double click: left third = next history entry, middle = previous, right = Enter
332
+ // - click on the prompt (" > " / "cmd> "): switch modes, keeping the text
333
+ // - elsewhere: move the cursor
334
+ handleClick(col, now = Date.now()) {
335
+ if (!this.editing) return;
336
+ const cols = this.columns ?? 80;
337
+ const third = Math.floor(cols / 3);
338
+ const zone = col <= third ? 'left' : col <= third * 2 ? 'middle' : 'right';
339
+ const double = this._lastClick.zone === zone && now - this._lastClick.time < DOUBLE_CLICK_MS;
340
+ this._lastClick = double ? { time: 0, zone: null } : { time: now, zone };
341
+ if (double) {
342
+ if (zone === 'left') this.historyDown();
343
+ else if (zone === 'middle') this.historyUp();
344
+ else this._finishEditing(this.text.trim() || null);
345
+ return;
346
+ }
347
+ // Map the column through the scrolled edit line, as jsmdcui does; the
348
+ // prompt (" > " / "cmd> ") switches modes, the text moves the cursor.
349
+ const prefix = this.prompt;
350
+ const total = prefix + this.text;
351
+ const { start, lead } = this._editView(cols);
352
+ const index = visualColToCharIdx(total, start, Math.max(0, col - 1 - lead));
353
+ if (index < prefix.length) {
354
+ this.toggleMode();
355
+ return;
356
+ }
357
+ this.cursor = [...total.slice(prefix.length, index)].length;
358
+ this._deselect();
359
+ this.render();
360
+ }
361
+
362
+ get dragging() { return Boolean(this._drag); }
363
+ armDrag(col) {
364
+ const index = this._pointerIndex(col);
365
+ if (index !== null) this._drag = { anchor: index, col, now: Date.now(), moved: false, opening: true };
366
+ }
367
+
368
+ // Map the scrolled terminal cell to a Unicode code-point boundary.
369
+ _pointerIndex(col) {
370
+ const total = this.prompt + this.text;
371
+ const { start, lead } = this._editView(this.columns ?? 80);
372
+ const index = visualColToCharIdx(total, start, Math.max(0, col - 1 - lead));
373
+ return index < this.prompt.length ? null : [...total.slice(this.prompt.length, index)].length;
374
+ }
375
+
376
+ pointerDown(col, now = Date.now()) {
377
+ const index = this._pointerIndex(col);
378
+ if (index === null) { this.handleClick(col, now); return; }
379
+ // Delay double-click actions until release so a drag cannot submit a URL
380
+ // or change history before we know whether it was a click.
381
+ this._drag = { anchor: index, col, now, moved: false };
382
+ this.moveCursor(index);
383
+ }
384
+ pointerMove(col) {
385
+ if (!this._drag) return;
386
+ const index = this._pointerIndex(col) ?? 0;
387
+ if (index !== this._drag.anchor || col !== this._drag.col) this._drag.moved = true;
388
+ this.selectionAnchor = this._drag.anchor;
389
+ this.cursor = index;
390
+ this.render();
391
+ }
392
+ pointerUp() {
393
+ const drag = this._drag; this._drag = null;
394
+ if (!drag) return;
395
+ if (drag.moved) this._lastClick = { time: 0, zone: null };
396
+ else if (!drag.opening) this.handleClick(drag.col, drag.now);
397
+ }
398
+
399
+ // Clear selection, or replace only the selected range on input/delete.
400
+ _deselect() { this.selectionAnchor = null; }
401
+ _clearIfSelected() {
402
+ const range = this.selectionRange;
403
+ if (!range) { this._deselect(); return false; }
404
+ const chars = [...this.text];
405
+ chars.splice(range[0], range[1] - range[0]);
406
+ this.text = chars.join(''); this.cursor = range[0]; this._deselect();
407
+ return true;
408
+ }
409
+
410
+ // Ctrl+U / Ctrl+K: cut or restore the text on one side of the cursor.
411
+ // The two sides deliberately keep independent buffers, like bunmsh.
412
+ _toggleSavedText(side) {
413
+ const chars = [...this.text];
414
+ const savedKey = side === 'head' ? '_savedHead' : '_savedTail';
415
+
416
+ if (side === 'head' && this.cursor > 0) {
417
+ this[savedKey] = chars.slice(0, this.cursor).join('');
418
+ this.text = chars.slice(this.cursor).join('');
419
+ this.cursor = 0;
420
+ } else if (side === 'head' && this[savedKey]) {
421
+ const saved = this[savedKey];
422
+ this.text = saved + this.text;
423
+ this.cursor = [...saved].length;
424
+ this[savedKey] = '';
425
+ } else if (side === 'tail' && this.cursor < chars.length) {
426
+ this[savedKey] = chars.slice(this.cursor).join('');
427
+ this.text = chars.slice(0, this.cursor).join('');
428
+ } else if (side === 'tail' && this[savedKey]) {
429
+ this.text += this[savedKey];
430
+ this[savedKey] = '';
431
+ }
432
+
433
+ this._deselect();
434
+ this.render();
435
+ }
436
+
437
+ // Insert text (for paste)
438
+ insertText(str) {
439
+ if (!this.editing) return;
440
+ this._clearIfSelected();
441
+ const chars = [...this.text];
442
+ const input = [...str];
443
+ this.text = chars.slice(0, this.cursor).join('') + str + chars.slice(this.cursor).join('');
444
+ this.cursor += input.length;
445
+ this.render();
446
+ }
447
+
448
+ // Handle key input during editing (returns true if consumed)
449
+ handleInput(str) {
450
+ if (!this.editing) return false;
451
+
452
+ // Tab switches prompts without changing the text or cursor.
453
+ if (str === '\t') {
454
+ this.toggleMode();
455
+ return true;
456
+ }
457
+
458
+ // Enter → confirm
459
+ if (str === '\r' || str === '\n') {
460
+ this._finishEditing(this.text.trim() || null);
461
+ return true;
462
+ }
463
+
464
+ // Escape → cancel
465
+ if (str === '\x1b') {
466
+ this._finishEditing(null);
467
+ return true;
468
+ }
469
+
470
+ // Clipboard shortcuts are handled by the input layer.
471
+ if (str === '\x03') return true;
472
+
473
+ // Ctrl+U → toggle text before the cursor
474
+ if (str === '\x15') {
475
+ this._toggleSavedText('head');
476
+ return true;
477
+ }
478
+
479
+ // Ctrl+K → toggle text after the cursor
480
+ if (str === '\x0b') {
481
+ this._toggleSavedText('tail');
482
+ return true;
483
+ }
484
+
485
+ // Shift+arrows extend selection; vertical movement selects to the ends
486
+ // of this single-line editor. Kitty events use these same local forms.
487
+ const shift = /^\x1b\[1;([2468])([ABCDHF])$/.exec(str);
488
+ if (shift) {
489
+ const positions = { A: 0, B: [...this.text].length, C: this.cursor + 1,
490
+ D: this.cursor - 1, H: 0, F: [...this.text].length };
491
+ this.moveCursor(positions[shift[2]], true);
492
+ return true;
493
+ }
494
+
495
+ // Up / Down → history of this mode
496
+ if (str === '\x1b[A' || str === '\x1bOA') {
497
+ this.historyUp();
498
+ return true;
499
+ }
500
+ if (str === '\x1b[B' || str === '\x1bOB') {
501
+ this.historyDown();
502
+ return true;
503
+ }
504
+
505
+ // Ctrl+A → select all
506
+ if (str === '\x01') {
507
+ this.selectAll = true;
508
+ this.cursor = [...this.text].length;
509
+ this.render();
510
+ return true;
511
+ }
512
+
513
+ // Ctrl+E → move to end
514
+ if (str === '\x05') {
515
+ this._deselect();
516
+ this.cursor = [...this.text].length;
517
+ this.render();
518
+ return true;
519
+ }
520
+
521
+ // Ctrl+W → delete selected text or the preceding word
522
+ if (str === '\x17') {
523
+ if (this._clearIfSelected()) { this.render(); return true; }
524
+ const chars = [...this.text];
525
+ const before = chars.slice(0, this.cursor).join('');
526
+ const after = chars.slice(this.cursor).join('');
527
+ const trimmed = before.replace(/\S+\s*$/, '');
528
+ this.text = trimmed + after;
529
+ this.cursor = [...trimmed].length;
530
+ this.render();
531
+ return true;
532
+ }
533
+
534
+ // Backspace
535
+ if (str === '\x7f' || str === '\x08') {
536
+ if (this.selectionRange) {
537
+ this._clearIfSelected();
538
+ } else if (this.cursor > 0) {
539
+ const chars = [...this.text];
540
+ chars.splice(this.cursor - 1, 1);
541
+ this.text = chars.join('');
542
+ this.cursor--;
543
+ }
544
+ this.render();
545
+ return true;
546
+ }
547
+
548
+ // Delete
549
+ if (str === '\x1b[3~') {
550
+ if (this.selectionRange) {
551
+ this._clearIfSelected();
552
+ } else if (this.cursor < [...this.text].length) {
553
+ const chars = [...this.text];
554
+ chars.splice(this.cursor, 1);
555
+ this.text = chars.join('');
556
+ }
557
+ this.render();
558
+ return true;
559
+ }
560
+
561
+ // Left arrow (Ctrl+Left too, as in jsmdcui) → deselect and move to start
562
+ if (LEFT_KEYS.has(str)) {
563
+ if (this.selectionRange) { this.cursor = this.selectionRange[0]; this._deselect(); }
564
+ else if (this.cursor > 0) this.cursor--;
565
+ this.render();
566
+ return true;
567
+ }
568
+
569
+ // Right arrow (Ctrl+Right too) → deselect and move to end
570
+ if (RIGHT_KEYS.has(str)) {
571
+ if (this.selectionRange) { this.cursor = this.selectionRange[1]; this._deselect(); }
572
+ else if (this.cursor < [...this.text].length) this.cursor++;
573
+ this.render();
574
+ return true;
575
+ }
576
+
577
+ // Home
578
+ if (HOME_KEYS.has(str)) {
579
+ this._deselect();
580
+ this.cursor = 0;
581
+ this.render();
582
+ return true;
583
+ }
584
+
585
+ // End
586
+ if (END_KEYS.has(str)) {
587
+ this._deselect();
588
+ this.cursor = [...this.text].length;
589
+ this.render();
590
+ return true;
591
+ }
592
+
593
+ // Normal character input → replace all if selected
594
+ if (!str.startsWith('\x1b') && str.charCodeAt(0) >= 32) {
595
+ this._clearIfSelected();
596
+ const chars = [...this.text];
597
+ const input = [...str];
598
+ this.text = chars.slice(0, this.cursor).join('') + str + chars.slice(this.cursor).join('');
599
+ this.cursor += input.length;
600
+ this.render();
601
+ return true;
602
+ }
603
+
604
+ return true; // Consume all input while editing
605
+ }
606
+ }
@@ -0,0 +1,88 @@
1
+ import {join,isAbsolute} from 'node:path';
2
+ import {tmpdir} from 'node:os';
3
+ import {openSync,closeSync,ftruncateSync,unlinkSync,writeSync} from 'node:fs';
4
+ import {connectUnix} from './transport.js';
5
+ import {interfaces,decode,encodeRequest} from './protocol.js';
6
+
7
+ export class WaylandClient {
8
+ constructor(path) {
9
+ const display=process.env.WAYLAND_DISPLAY || 'wayland-0';
10
+ this.connection=connectUnix(path || (isAbsolute(display)?display:join(process.env.XDG_RUNTIME_DIR||tmpdir(),display)));
11
+ this.objects=new Map();this.nextId=2;this.globals=new Map();this.waiters=new Set();
12
+ this.objects.set(1,{iface:'wl_display',event:(name,args)=>{
13
+ if(name==='error')throw Error(`Wayland error ${args.join(': ')}`);
14
+ if(name==='delete_id')this.objects.delete(args[0]);
15
+ }});
16
+ this.connection.onData=()=>this.receive();
17
+ this.connection.onClose=()=>{clearInterval(this.timer);for(const reject of this.waiters)reject(Error('Wayland disconnected'));this.waiters.clear();this.onClose?.();};
18
+ this.timer=setInterval(()=>this.connection.poll(),4);
19
+ }
20
+ object(iface,event=()=>{}) {const id=this.nextId++;this.objects.set(id,{iface,event});return id;}
21
+ request(id,name,...args) {const object=this.objects.get(id);if(!object)throw Error(`Unknown object ${id}`);const packet=encodeRequest(id,object.iface,name,args);this.connection.send(packet.data,packet.fds);this.connection.flush();}
22
+ receive() {
23
+ const c=this.connection;
24
+ while(!c.closed&&c.input.length>=8){
25
+ const id=c.input.readUInt32LE(0),header=c.input.readUInt32LE(4),size=header>>>16,opcode=header&65535;
26
+ if(size<8||size%4)throw Error('Invalid event size');if(c.input.length<size)return;
27
+ const object=this.objects.get(id),spec=interfaces.get(object?.iface)?.events[opcode];
28
+ if(!spec)throw Error(`Unknown event ${id}:${opcode}`);
29
+ const args=decode(spec,c.input.subarray(0,size),c.fds);if(args===null)return;
30
+ c.input=c.input.subarray(size);object.event(spec.name,args);
31
+ }
32
+ }
33
+ sync() {return new Promise((resolve,reject)=>{
34
+ const timer=setTimeout(()=>{this.waiters.delete(fail);reject(Error('Wayland roundtrip timeout'));},5000);
35
+ const fail=e=>{clearTimeout(timer);reject(e);};this.waiters.add(fail);
36
+ const id=this.object('wl_callback',()=>{clearTimeout(timer);this.waiters.delete(fail);resolve();});this.request(1,'sync',id);
37
+ });}
38
+ async initialize() {
39
+ this.registry=this.object('wl_registry',(event,[name,iface,version])=>{
40
+ if(event==='global')this.globals.set(iface,{name,version});
41
+ else for(const [key,value]of this.globals)if(value.name===name)this.globals.delete(key);
42
+ });this.request(1,'get_registry',this.registry);await this.sync();
43
+ this.compositor=this.bind('wl_compositor',4);this.shm=this.bind('wl_shm',1);
44
+ this.shell=this.bind('xdg_wm_base',1,(event,[serial])=>{if(event==='ping')this.request(this.shell,'pong',serial);});
45
+ await this.sync();return this;
46
+ }
47
+ bind(iface,version,event) {const global=this.globals.get(iface);if(!global)throw Error(`Compositor lacks ${iface}`);const id=this.object(iface,event);this.request(this.registry,'bind',global.name,iface,Math.min(version,global.version),id);return id;}
48
+ close(){this.connection.close();}
49
+ }
50
+
51
+ export class WaylandWindow {
52
+ constructor(client,{width=800,height=600,title='Buninu Browser'}={}) {
53
+ this.client=client;this.width=width;this.height=height;this.buffers=[];this.configured=false;this.framePending=false;
54
+ const c=client;
55
+ this.surface=c.object('wl_surface');c.request(c.compositor,'create_surface',this.surface);
56
+ this.xdg=c.object('xdg_surface',(event,[serial])=>{
57
+ if(event!=='configure')return;c.request(this.xdg,'ack_configure',serial);this.configured=true;
58
+ if(this.pendingSize){[this.width,this.height]=this.pendingSize;this.pendingSize=null;}
59
+ this.onConfigure?.(this.width,this.height);
60
+ });c.request(c.shell,'get_xdg_surface',this.xdg,this.surface);
61
+ this.toplevel=c.object('xdg_toplevel',(event,args)=>{
62
+ if(event==='configure')this.pendingSize=[args[0]||this.width,args[1]||this.height];
63
+ if(event==='close')this.onClose?.();
64
+ });c.request(this.xdg,'get_toplevel',this.toplevel);c.request(this.toplevel,'set_title',title);c.request(this.toplevel,'set_app_id','org.buninu.browser');c.request(this.surface,'commit');
65
+ }
66
+ get available(){return this.configured&&!this.framePending&&this.buffers.filter(b=>b.busy).length<2;}
67
+ present(bytes,width,height) {
68
+ if(!this.available||width!==this.width||height!==this.height)return false;
69
+ const c=this.client,size=width*height*4,parts=Array.isArray(bytes)?bytes:[bytes];if(parts.reduce((sum,part)=>sum+part.length,0)!==size)throw Error('Invalid raw pixel length');
70
+ for(const b of [...this.buffers])if(!b.busy&&(b.width!==width||b.height!==height))this.disposeBuffer(b);
71
+ let b=this.buffers.find(b=>!b.busy);
72
+ if(!b){
73
+ const path=join(process.env.XDG_RUNTIME_DIR||tmpdir(),`buninu-shm-${process.pid}-${crypto.randomUUID()}`);
74
+ const fd=openSync(path,'wx+',0o600);unlinkSync(path);
75
+ try{ftruncateSync(fd,size);
76
+ b={fd,width,height,busy:false};const pool=c.object('wl_shm_pool');c.request(c.shm,'create_pool',pool,fd,size);
77
+ b.id=c.object('wl_buffer',(event)=>{if(event==='release'){b.busy=false;this.onReady?.();}});
78
+ c.request(pool,'create_buffer',b.id,0,width,height,width*4,1);c.request(pool,'destroy');this.buffers.push(b);
79
+ }catch(e){closeSync(fd);throw e;}
80
+ }
81
+ let position=0;for(const part of parts){let written=0;while(written<part.length){const n=writeSync(b.fd,part,written,part.length-written,position);if(!n)throw Error('Short SHM write');written+=n;position+=n;}}
82
+ b.busy=true;this.framePending=true;
83
+ const callback=c.object('wl_callback',()=>{this.framePending=false;this.onReady?.();});
84
+ c.request(this.surface,'attach',b.id,0,0);c.request(this.surface,'damage',0,0,width,height);c.request(this.surface,'frame',callback);c.request(this.surface,'commit');return true;
85
+ }
86
+ disposeBuffer(b){this.client.request(b.id,'destroy');closeSync(b.fd);this.buffers.splice(this.buffers.indexOf(b),1);}
87
+ close(){if(this.closed)return;this.closed=true;for(const b of [...this.buffers])this.disposeBuffer(b);this.client.request(this.toplevel,'destroy');this.client.request(this.xdg,'destroy');this.client.request(this.surface,'destroy');}
88
+ }