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,305 @@
1
+ .pragma library
2
+
3
+ // SI, because a file's size on disk has no power-of-two basis; GLib's own rule and the whole GUI group.
4
+ var BYTES_PER_UNIT = 1000
5
+ var UNITS = ["B", "kB", "MB", "GB", "TB"]
6
+ // GNOME renders a narrow no-break space before the unit, and Flea's neighbours are GLib-formatted.
7
+ var UNIT_SPACE = " "
8
+
9
+ // Below one kilobyte a fraction is noise, so bytes print whole.
10
+ // Counts reach six figures on a real directory, so they are grouped the way the canvas draws them.
11
+ function count(n) {
12
+ var digits = String(n)
13
+ var out = ""
14
+ for (var i = 0; i < digits.length; i++) {
15
+ if (i > 0 && (digits.length - i) % 3 === 0)
16
+ out += ","
17
+ out += digits.charAt(i)
18
+ }
19
+ return out
20
+ }
21
+
22
+ function size(bytes) {
23
+ if (bytes < BYTES_PER_UNIT) {
24
+ return bytes + UNIT_SPACE + UNITS[0]
25
+ }
26
+ var value = bytes
27
+ var unit = 0
28
+ while (value >= BYTES_PER_UNIT && unit < UNITS.length - 1) {
29
+ value = value / BYTES_PER_UNIT
30
+ unit += 1
31
+ }
32
+ return value.toFixed(1) + UNIT_SPACE + UNITS[unit]
33
+ }
34
+
35
+ // Fills %1 to %9 in one pass: a value prints as given, a dollar sign or placeholder in it included; an unfilled one stays.
36
+ function fill(template) {
37
+ var values = arguments
38
+ return String(template).replace(/%([1-9])/g, function (token, at) { return at < values.length ? String(values[at]) : token })
39
+ }
40
+
41
+ function pad(n) {
42
+ return n < 10 ? "0" + n : "" + n
43
+ }
44
+
45
+ // "2026-09-12 15:29", the one form every surface prints, in the machine's local wall clock.
46
+ function stamp(d) {
47
+ return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
48
+ + " " + pad(d.getHours()) + ":" + pad(d.getMinutes())
49
+ }
50
+
51
+ // One form, sortable and unambiguous, so no surface has to invent a relative word for a time.
52
+ function date(mtime) {
53
+ return stamp(new Date(mtime * 1000))
54
+ }
55
+
56
+ // The send picker's column is SendPicker.html's 80 and not the window's 125, which holds about ten
57
+ // characters: the one place the full stamp does not fit. It drops the time and keeps the date, so it
58
+ // is still sortable and still unambiguous. Preview board, "One function, four surfaces".
59
+ function compactDate(mtime) {
60
+ var d = new Date(mtime * 1000)
61
+ return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
62
+ }
63
+
64
+ // The low nine bits of st_mode, read three at a time.
65
+ var PERMISSION_BITS = 9
66
+ var TRIAD = "rwx"
67
+
68
+ function permissions(mode) {
69
+ var out = ""
70
+ for (var i = 0; i < PERMISSION_BITS; i++) {
71
+ var bit = 1 << (PERMISSION_BITS - 1 - i)
72
+ out += (mode & bit) ? TRIAD[i % TRIAD.length] : "-"
73
+ }
74
+ return out
75
+ }
76
+
77
+ // The file type lives in the top four bits of st_mode, as S_IFMT masks it.
78
+ var S_IFMT = 0o170000
79
+ var S_IFREG = 0o100000
80
+ var S_IFDIR = 0o040000
81
+ // The owner execute bit, the one Make executable adds.
82
+ var S_IXUSR = 0o100
83
+ var S_IFLNK = 0o120000
84
+ var ANY_EXECUTE_BIT = 0o111
85
+
86
+ function isSymlink(mode) {
87
+ return (mode & S_IFMT) === S_IFLNK
88
+ }
89
+
90
+ function isExecutable(mode) {
91
+ return (mode & ANY_EXECUTE_BIT) !== 0
92
+ }
93
+
94
+ // Shared by Row.qml's iconSource and PreviewMedia.qml's player source: encodeURI leaves # and ?
95
+ // literal, which Qt then reads as a URL fragment or query rather than path bytes.
96
+ function fileUri(path) {
97
+ return "file://" + encodeURI(path).replace(/#/g, "%23").replace(/\?/g, "%3F")
98
+ }
99
+
100
+ // "3:05", or "1:03:05" once an hour is on the clock; mm/ss are always two digits, matching a media player's own clock rather than Format.date's prose.
101
+ function duration(ms) {
102
+ var totalSeconds = Math.max(0, Math.floor(ms / 1000))
103
+ var hours = Math.floor(totalSeconds / 3600)
104
+ var minutes = Math.floor((totalSeconds % 3600) / 60)
105
+ var seconds = totalSeconds % 60
106
+ if (hours > 0) {
107
+ return hours + ":" + pad(minutes) + ":" + pad(seconds)
108
+ }
109
+ return minutes + ":" + pad(seconds)
110
+ }
111
+
112
+ // The scope reads as the user writes it, so the home prefix comes back as a tilde. Both the search
113
+ // strip and the window chrome draw a path through this, so the rule has one definition.
114
+ // Issue 95, nixfred: a bare prefix made /home/gmx into "~x", a sibling wearing home's name. The test
115
+ // is home itself or home and a separator, the one ui/js/Nav.js crumbs and Search.scopeRoot both make.
116
+ function tilde(path, home) {
117
+ var text = String(path)
118
+ if (home.length > 0 && (text === home || text.indexOf(home + "/") === 0)) {
119
+ return "~" + text.substring(home.length)
120
+ }
121
+ return text
122
+ }
123
+
124
+ // A tab is named after the directory it is standing in, so the label is the path's last segment.
125
+ function leafPart(display) {
126
+ var text = String(display)
127
+ var cut = text.lastIndexOf("/")
128
+ if (cut < 0) {
129
+ return text
130
+ }
131
+ // "/" itself has no leaf, and its own separator is the whole label.
132
+ return cut === text.length - 1 ? text : text.substring(cut + 1)
133
+ }
134
+
135
+ // "44.1 kHz", the way the canvas writes an audio row's Rate; a zero is not a rate and reads empty.
136
+ function sampleRate(hz) {
137
+ var n = Number(hz)
138
+ if (!n || n <= 0) {
139
+ return ""
140
+ }
141
+ var khz = n / 1000
142
+ // A whole number of kilohertz reads without a decimal, so 48000 is "48 kHz" and not "48.0 kHz".
143
+ return (khz === Math.round(khz) ? khz : khz.toFixed(1)) + " kHz"
144
+ }
145
+
146
+ // Issue 67, jesedv: a yanked path is quoted unless a shell reads every character of it as itself.
147
+ function shellQuoted(path) {
148
+ var text = String(path)
149
+ if (/^[A-Za-z0-9_@%+=:,.\/-]+$/.test(text)) {
150
+ return text
151
+ }
152
+ return "'" + text.split("'").join("'\\''") + "'"
153
+ }
154
+
155
+ // One window-level day boundary, so no Date is built and no timer ticks per row.
156
+
157
+ // Sample input: Date.now() on the board date, 2026-09-23 11:40 local.
158
+ var DAY_MS = 24 * 60 * 60 * 1000
159
+ // The midnight timer's minute step, so a suspend still lands the day within a minute of resume.
160
+ var MINUTE_MS = 60 * 1000
161
+
162
+ // Local midnight starting the day nowMs falls in, in ms since the epoch.
163
+ function dayStart(nowMs) {
164
+ var now = new Date(nowMs)
165
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
166
+ }
167
+
168
+ // True when the stamp is on or after today's local midnight.
169
+ function isRecent(mtimeSec, todayStartMs) {
170
+ if (typeof mtimeSec !== "number" || !isFinite(mtimeSec))
171
+ return false
172
+ return mtimeSec * 1000 >= todayStartMs
173
+ }
174
+
175
+ // Built from components, so a daylight-saving night still lands on midnight.
176
+ function msUntilMidnight(nowMs) {
177
+ var now = new Date(nowMs)
178
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getTime() - nowMs
179
+ }
180
+
181
+ // Elides in the middle so the extension stays visible; truncation costs a slice, never a relayout.
182
+ // Sample input: "screenshot-2026-08-30-final-review-for-gm-after-the-bench-v3.png", 49.
183
+
184
+ // The walk pairs surrogates by hand, so a cut never splits a code point.
185
+ function charsOf(text) {
186
+ var out = []
187
+ for (var i = 0; i < text.length; i++) {
188
+ var lead = text.charCodeAt(i)
189
+ if (lead >= 0xD800 && lead <= 0xDBFF && i + 1 < text.length) {
190
+ var trail = text.charCodeAt(i + 1)
191
+ if (trail >= 0xDC00 && trail <= 0xDFFF) {
192
+ out.push(text.substring(i, i + 2))
193
+ i += 1
194
+ continue
195
+ }
196
+ }
197
+ // A combining mark rides with the char before it, so an NFD accent never orphans.
198
+ if (lead >= 0x0300 && lead <= 0x036F && out.length > 0) {
199
+ out[out.length - 1] += text.charAt(i)
200
+ continue
201
+ }
202
+ out.push(text.charAt(i))
203
+ }
204
+ return out
205
+ }
206
+
207
+ // Every fast unit sits below U+0300, so a fast name holds no surrogate, no combining mark and no wide glyph.
208
+ var FAST_LIMIT = 0x0300
209
+
210
+ // True when every UTF-16 unit is fast: each char paints one cell and slices never split a point.
211
+ function isFastText(text) {
212
+ for (var i = 0; i < text.length; i++)
213
+ if (text.charCodeAt(i) >= FAST_LIMIT) return false
214
+ return true
215
+ }
216
+
217
+ function middleElide(name, maxCells) {
218
+ var text = String(name), max = Math.max(0, Math.floor(maxCells))
219
+ // A non-finite budget names no width, so the name goes through untouched.
220
+ if (!isFinite(max)) return text
221
+ // One scan serves the fit check and the store below, so a fitting fast name builds nothing.
222
+ var fast = isFastText(text)
223
+ // A fast name counts one cell a char, so a short one already fits untouched.
224
+ if (fast && text.length <= max) return text
225
+ var st = fast ? fastStoreOf(text) : storeOf(charsOf(text))
226
+ if (rangeCells(st, 0, st.n) <= max) return text
227
+ // The head takes the odd cell, so a 49-wide board keeps 24 and 24.
228
+ var h = spanFrom(st, 0, Math.ceil((max - 1) / 2)), t = tailSpan(st, Math.floor((max - 1) / 2))
229
+ return pieceOf(st, 0, h) + "…" + pieceOf(st, st.n - t, st.n)
230
+ }
231
+
232
+ // Display cells: East Asian Wide and Fullwidth paint two columns, emoji paint two, all else one.
233
+ // Sample input: cellWidthOf("🎉") is 2, cellWidthOf("A") is 1.
234
+ var WIDE_RANGES = [[0x1100, 0x115F], [0x2E80, 0x303E], [0x3041, 0x33FF], [0x3400, 0x4DBF], [0x4E00, 0xA4CF], [0xAC00, 0xD7A3], [0xF900, 0xFAFF], [0xFE10, 0xFE19], [0xFE30, 0xFE4F], [0xFF00, 0xFF60], [0xFFE0, 0xFFE6], [0x20000, 0x3FFFD]]
235
+ var EMOJI_RANGES = [[0x2600, 0x26FF], [0x2700, 0x27BF], [0x2B00, 0x2BFF], [0x1F000, 0x1FAFF]]
236
+
237
+ // True when a code point paints two columns rather than one.
238
+ function isWideCode(cp) {
239
+ // Every wide range starts at U+1100, so a smaller code point never scans.
240
+ if (cp < 0x1100) return false
241
+ for (var i = 0; i < WIDE_RANGES.length; i++)
242
+ if (cp >= WIDE_RANGES[i][0] && cp <= WIDE_RANGES[i][1]) return true
243
+ for (var j = 0; j < EMOJI_RANGES.length; j++)
244
+ if (cp >= EMOJI_RANGES[j][0] && cp <= EMOJI_RANGES[j][1]) return true
245
+ return false
246
+ }
247
+
248
+ // The code point one charsOf element starts with, read without wrapping it in a string.
249
+ // Sample input: codeOf("🎉") is 0x1F389, codeOf("A") is 0x41.
250
+ function codeOf(ch) {
251
+ var s = String(ch), lead = s.charCodeAt(0)
252
+ if (lead >= 0xD800 && lead <= 0xDBFF && s.length > 1) {
253
+ var trail = s.charCodeAt(1)
254
+ if (trail >= 0xDC00 && trail <= 0xDFFF) return 0x10000 + ((lead - 0xD800) << 10) + (trail - 0xDC00)
255
+ }
256
+ return lead
257
+ }
258
+
259
+ // Cells one char paints: 2 for a wide code point, 1 for all else.
260
+ function cellWidthOf(ch) {
261
+ return isWideCode(codeOf(ch)) ? 2 : 1
262
+ }
263
+
264
+ // One layout per call: widths plus prefix sums, so every range below is one subtraction.
265
+ // Sample input: storeOf(["a", "🎉"]) carries widths [1, 2].
266
+ function storeOf(chars) {
267
+ var widths = new Array(chars.length), prefix = new Array(chars.length + 1)
268
+ prefix[0] = 0
269
+ for (var i = 0; i < chars.length; i++) {
270
+ widths[i] = isWideCode(codeOf(chars[i])) ? 2 : 1
271
+ prefix[i + 1] = prefix[i] + widths[i]
272
+ }
273
+ return { seq: chars, widths: widths, prefix: prefix, n: chars.length }
274
+ }
275
+
276
+ // The same layout over a fast string: no arrays, every range counts chars as cells.
277
+ function fastStoreOf(text) {
278
+ return { seq: text, widths: null, prefix: null, n: text.length }
279
+ }
280
+
281
+ // Cells of elements [a..b).
282
+ function rangeCells(st, a, b) {
283
+ return st.widths === null ? b - a : st.prefix[b] - st.prefix[a]
284
+ }
285
+
286
+ // Elements from pos fitting in this many cells.
287
+ function spanFrom(st, pos, budget) {
288
+ if (st.widths === null) return Math.min(Math.max(budget, 0), st.n - pos)
289
+ var used = 0, i = pos
290
+ while (i < st.widths.length && used + st.widths[i] <= budget) { used += st.widths[i]; i += 1 }
291
+ return i - pos
292
+ }
293
+
294
+ // Trailing elements fitting in this many cells.
295
+ function tailSpan(st, budget) {
296
+ if (st.widths === null) return Math.min(Math.max(budget, 0), st.n)
297
+ var used = 0, i = st.widths.length
298
+ while (i > 0 && used + st.widths[i - 1] <= budget) { used += st.widths[i - 1]; i -= 1 }
299
+ return st.widths.length - i
300
+ }
301
+
302
+ // Text of elements [a..b): a slice for a fast string, a join for a char array.
303
+ function pieceOf(st, a, b) {
304
+ return st.widths === null ? st.seq.slice(a, b) : st.seq.slice(a, b).join("")
305
+ }
@@ -0,0 +1,206 @@
1
+ .pragma library
2
+
3
+ .import "Format.js" as Format
4
+
5
+ // The backend sends a freedesktop icon name per row; this is the only place Flea maps one to a mark.
6
+ // Counts re-derived 2026-08-31 from /usr/share/mime/generic-icons on this box, see AGENTS.md "Icons in the row":
7
+ // x-office-document 106, package-x-generic 89, application-x-executable 57, text-x-generic 46,
8
+ // image-x-generic 40, x-office-spreadsheet 38, text-x-script 36, font-x-generic 23, x-office-presentation 21,
9
+ // video-x-generic 15, text-html 11, then a tail of five or fewer each.
10
+ var GLYPHS = {
11
+ "folder": "folder",
12
+ "inode-directory": "folder",
13
+ "text-x-generic": "file-text",
14
+ "text-plain": "file-text",
15
+ // text-x-script, text-html and application-xml are 49 types a generic file mark would hide.
16
+ "text-x-script": "code",
17
+ "text-html": "code",
18
+ "application-xml": "code",
19
+ "image-x-generic": "image",
20
+ "video-x-generic": "film",
21
+ "audio-x-generic": "music",
22
+ "package-x-generic": "archive",
23
+ "font-x-generic": "type",
24
+ "application-x-executable": "terminal",
25
+ "application-x-generic": "file",
26
+ "x-office-document": "file-text",
27
+ "x-office-spreadsheet": "table",
28
+ "x-office-presentation": "presentation"
29
+ }
30
+
31
+ var FALLBACK = "file"
32
+
33
+ function glyphFor(iconName) {
34
+ var g = GLYPHS[String(iconName)]
35
+ return g ? g : FALLBACK
36
+ }
37
+
38
+ // A row's mark. The backend resolves a symlink's icon to its target's, so a link to a directory
39
+ // arrives as "folder"; every board draws such a row with the link mark instead, so the mode decides
40
+ // here and the icon name only answers for what the row really is.
41
+ function glyphForRow(iconName, mode) {
42
+ if (Format.isSymlink(mode))
43
+ return "symlink"
44
+ return glyphFor(iconName)
45
+ }
46
+
47
+ // The sidebar's own set, keyed on the favourite's label (Places.js leaf() or bookmark label),
48
+ // the operator's overnight ask; any bookmark outside this set draws the plain folder mark.
49
+ var SIDEBAR_GLYPHS = {
50
+ "Home": "house",
51
+ "Downloads": "download",
52
+ "Documents": "file-text",
53
+ "Pictures": "image",
54
+ "Videos": "film",
55
+ "Music": "music",
56
+ "Projects": "folder-git-2"
57
+ }
58
+
59
+ var SIDEBAR_FALLBACK = "folder"
60
+
61
+ function sidebarGlyphFor(label) {
62
+ var g = SIDEBAR_GLYPHS[String(label)]
63
+ return g ? g : SIDEBAR_FALLBACK
64
+ }
65
+
66
+ // Lucide-static 1.38.0 geometry, ISC licence, https://github.com/lucide-icons/lucide, one "d"
67
+ // per mark on its native 24 unit grid, recut to the Omarchy edge: baked rounded corners became
68
+ // hard corners, real curves stayed; see AGENTS.md "Lucide path data", "The Omarchy cut".
69
+ var PATHS = {
70
+ "info": "M12 11v6 M12 7h.01 M22 12a10 10 0 1 1-20 0a10 10 0 1 1 20 0",
71
+ "shelf": "M21 21H3V3h18v14H7V7h10v6h-6",
72
+ "pin": "M9 3h6v6l3 3v2H6v-2l3-3z M12 14v7",
73
+ "star": "M12 2l2.9 6.6 7.1.6-5.4 4.7 1.6 7L12 17.3l-6.2 3.6 1.6-7L2 9.2l7.1-.6z",
74
+ "globe": "M2 12h20 M12 2a15 15 0 0 1 0 20 M12 2a15 15 0 0 0 0 20 M22 12a10 10 0 1 1-20 0a10 10 0 1 1 20 0",
75
+
76
+ "file": "M4 22V2h10l6 6v14H4z M14 2v6h6",
77
+ "folder": "M2 20V3h6l2 3h12v14H2z",
78
+ "folders-first": "M2 12V4h6l2 2h12v6H2z M2 17h20 M2 21h20",
79
+ "file-text": "M4 22V2h10l6 6v14H4z M14 2v6h6 M8 9h2 M8 13h8 M8 17h8",
80
+ // LanguageMarks' Markdown mark, Flea's cut of dcurtis/markdown-mark (CC0): the frame, the M and the down arrow in one stroke.
81
+ "markdown": "M2 5h20v14H2z M5.5 15V9l3 3.5 3-3.5v6 M17 9v6 M14.5 12.5 17 15l2.5-2.5",
82
+ "html": "M4 2h16l-1.5 17L12 22l-6.5-3z M15.5 6.5h-7V11h7v5L12 17.5 8.5 16",
83
+ "code": "M16 18l6-6-6-6 M8 6l-6 6 6 6",
84
+ "image": "M3 3h18v18H3z M7 9A2 2 0 1 0 11 9A2 2 0 1 0 7 9Z M21 15l-3-3L6 21",
85
+ "film": "M3 3h18v18H3z M7 3v18 M3 7.5h4 M3 12h18 M3 16.5h4 M17 3v18 M17 7.5h4 M17 16.5h4",
86
+ // Note heads squared instead of lucide's circles: the set's brand tell.
87
+ "music": "M9 18V5l12-2v13 M3 15h6v6H3z M15 13h6v6h-6z",
88
+ "archive": "M2 3h20v5H2z M4 8v13h16V8 M10 12h4",
89
+ "type": "M12 4v16 M4 7V4h16v3 M9 20h6",
90
+ "terminal": "M12 19h8 M4 17l6-6-6-6",
91
+ "table": "M3 3h18v18H3z M12 3v18 M3 9h18 M3 15h18",
92
+ "presentation": "M2 3h20 M3 3v13h18V3 M7 21l5-5 5 5",
93
+ "house": "M3 21V10l9-7 9 7v11h-6v-8H9v8H3z",
94
+ "download": "M12 15V3 M7 10l5 5 5-5 M3 15v6h18v-6",
95
+ "folder-git-2": "M18 19a5 5 0 0 1-5-5v8 M9 20H2V3h6l2 2h12v5 M11 12A2 2 0 1 0 15 12A2 2 0 1 0 11 12Z M18 19A2 2 0 1 0 22 19A2 2 0 1 0 18 19Z",
96
+ // The two near-zero-length lines are lucide's own technique for the rack unit's LED dots; SquareCap draws them as square dots, matching the cut.
97
+ "server": "M2 2h20v8H2z M2 14h20v8H2z M6 6L6.01 6 M6 18L6.01 18",
98
+ // The Network group's add mark, replacing a Text "+" the operator read as a Christian cross.
99
+ "plus": "M5 12h14 M12 5v14",
100
+ // The picker rail's Recent mark, lucide rotate-ccw, the one SendPicker.html itself draws; the
101
+ // circle is a real curve and stays, and the arrow head is already square on lucide's own grid.
102
+ "history": "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5",
103
+ // Lucide's hard-drive with its four baked 2 unit corner arcs cut square, which lands the body on
104
+ // (6,4) (18,4) (22,12) (22,20) (2,20) (2,12); the divider and the two LED dots are lucide's own.
105
+ "drive": "M6 4h12l4 8v8H2v-8z M2 12h20 M6 16L6.01 16 M10 16L10.01 16",
106
+ // The DEVICES rail's phone mark, lucide's smartphone with its rx=2 body rect cut square the way
107
+ // drive's arcs were; the home-button dot is lucide's own near-zero line, drawn as server's are.
108
+ "smartphone": "M5 2h14v20H5z M12 18L12.01 18",
109
+ // Its GPhoto2 sibling, per the PhoneMark board: lucide's camera with the four a2 2 body arcs cut
110
+ // to (2,7) (22,7) (22,20) (2,20), the pentaprism kept, and the lens a genuine circle, cut rule 4.
111
+ "camera": "M14.5 4h-5L7 7H2v13h20V7h-5z M15 13a3 3 0 1 1-6 0a3 3 0 1 1 6 0",
112
+ // The rail menu's release mark, the shelf's "for: unmount": lucide's triangle corner arcs
113
+ // extend to (12, 2.09) (22.32, 13) (1.68, 13) and snap to grid, its rx=1 bar rect squares off.
114
+ "eject": "M12 2 22 13H2z M3 17h18v4H3z",
115
+ // Task 22's transport strip, recut sharp like the rest of the set.
116
+ "play": "M6 4l14 8-14 8z",
117
+ "pause": "M14 3h5v18h-5z M5 3h5v18H5z",
118
+ // The search strip's mark; the lens is a real curve, so it stays a circle under the cut.
119
+ "search": "M16.7 16.7 21 21 M3 11A8 8 0 1 0 19 11A8 8 0 1 0 3 11Z",
120
+ // The filter strip's mark, the funnel the icon spec already shelves and the Main board draws.
121
+ "filter": "M3 4h18l-7 8v7l-4 2v-9z",
122
+ // The context menu's own seven marks, added with the operations surface that draws them.
123
+ // The canvas's own open folder, which keeps the closed folder's back panel byte for byte.
124
+ "folder-open": "M2 20V3h6l2 3h12v3 M22 11l-2.5 9H2l2.5-9z",
125
+ // OpenWith.html draws this window for the Open with row, its tail row and an app the theme cannot name.
126
+ "app-window": "M3 4h18v16H3z M3 9h18 M6 6.5h.01 M9 6.5h.01",
127
+ "undo": "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5",
128
+ "rename": "M3 21l1-4L17 4l3 3L7 20l-4 1z M14 7l3 3",
129
+ "file-plus": "M6 22V2h8l4 4v16H6z M14 2v4h4 M12 11v6 M9 14h6",
130
+ // lucide's own folder-plus is the folder body byte for byte plus these two strokes, so the recut
131
+ // body is reused verbatim and only the plus is new; the specimen sheet's 5 unit plus is not it.
132
+ "folder-plus": "M2 20V3h6l2 3h12v14H2z M12 10v6 M9 13h6",
133
+ "trash": "M3 6h18 M8 6V3h8v3 M6 6l1.2 15h9.6L18 6 M10 10v7 M14 10v7",
134
+ // A diamond eye with a square pupil: lucide's own eye is two arcs meeting at points, which the cut squares off.
135
+ "eye": "M12 5 22 12 12 19 2 12z M10 10h4v4h-4z",
136
+ "eye-off": "M12 5 22 12 12 19 2 12z M10 10h4v4h-4z M4 4l16 16",
137
+ // Replaces the "▸" Text the menu used to draw, so it stops mixing a font dingbat into a path language.
138
+ "chevron-right": "M9 18l6-6-6-6",
139
+ "chevron-down": "M6 9l6 6 6-6",
140
+ "chevron-left": "M15 18l-6-6 6-6",
141
+ // The window chrome's own five marks, added with the bar that draws them.
142
+ "arrow-left": "M19 12H5 M12 19l-7-7 7-7",
143
+ "arrow-up": "M12 19V5 M5 12l7-7 7 7",
144
+ // The bullets are lucide's own zero-length-line technique, which SquareCap draws as square dots.
145
+ "list": "M8 6h13 M8 12h13 M8 18h13 M3 6L3.01 6 M3 12L3.01 12 M3 18L3.01 18",
146
+ "columns": "M3 3h18v18H3z M9 3v18 M15 3v18",
147
+ "preview": "M3 3h18v18H3z M15 3v18",
148
+ "dual": "M3 4h8v16H3z M13 4h8v16h-8z",
149
+ "grid": "M3 3h8v8H3z M13 3h8v8h-8z M3 13h8v8H3z M13 13h8v8h-8z",
150
+ // The preview column's own two, taken from the canvas's icon table rather than recut from lucide.
151
+ "symlink": "M7 17L17 7 M8 7h9v9",
152
+ // The dot is the zero-length-line technique again, which SquareCap draws square.
153
+ "alert": "M12 2 23 21H1z M12 10v4 M12 18L12.01 18",
154
+ // The Locked pane state draws this beside it: the shackle is a real curve so it stays an arc, and the keyhole is the same dot technique.
155
+ "lock": "M4 11h16v10H4z M8 11V7a4 4 0 0 1 8 0v4 M12 15h.01",
156
+ // The compress submenu's With-password row: a bow ring, a shaft and two teeth; the bow is a true circle under the cut.
157
+ "key": "M3 7a4 4 0 1 0 8 0a4 4 0 1 0-8 0 M10.5 10.5l8 8 M16 16l2.5-2.5 M18.5 18.5l1.5-1.5",
158
+ // The operations rows the archive and convert subsystems draw, from the spec's own glyph shelf.
159
+ "archive-out": "M2 3h20v5H2z M4 8v13h16V8 M12 18v-6 M9 15l3-3 3 3",
160
+ // The Move to Dropbox row, which the canvas draws with the network mark.
161
+ "network": "M9 2h6v6H9z M2 16h6v6H2z M16 16h6v6h-6z M12 8v4 M5 16v-4h14v4",
162
+ // The convert popup's checkbox mark.
163
+ "check": "M4 12l6 6L20 6",
164
+ // The Tags row's mark: a square-cornered price tag with its hole as a SquareCap dot.
165
+ "tag": "M3 3h7l11 11-7 7L3 10z M7.5 7.5L7.51 7.5",
166
+ // The PDF viewer's own three, recut sharp like the rest of the set; "maximize" is lucide's
167
+ // name for the mark the canvas calls expand.
168
+ "minus": "M5 12h14",
169
+ "maximize": "M8 3H3v5 M16 3h5v5 M8 21H3v-5 M16 21h5v-5",
170
+ "x": "M6 6l12 12 M18 6 6 18",
171
+ "sliders": "M4 21v-7 M4 10V3 M12 21v-9 M12 8V3 M20 21v-5 M20 12V3 M2 14h4 M10 8h4 M18 16h4",
172
+ // Grid zoom's own mark, the SettingsGrammar board's arrows, lucide's move-horizontal recut sharp.
173
+ "move-horizontal": "M4 12h16 M8 8l-4 4 4 4 M16 8l4 4-4 4",
174
+ // The background menu's Sort by row, Menus.html's own geometry: three rules shortening to the
175
+ // right of a bare down arrow. IconSets.html names this mark when it caps the corner radius.
176
+ "sort": "M11 5h10 M11 9h7 M11 13h4 M7 5v14 M7 19l-3-3 M7 19l3-3",
177
+ // MediaMute rule 2: lucide's speaker polygon is already hard-cornered, and the two waves are
178
+ // genuine arcs that stay arcs under cut rule 4; the cross is two lines.
179
+ "volume": "M11 5L6 9H2v6h4l5 4z M15.5 8.5a5 5 0 0 1 0 7 M19 5a10 10 0 0 1 0 14",
180
+ "volume-x": "M11 5L6 9H2v6h4l5 4z M22 9l-6 6 M16 9l6 6",
181
+ // The three basic clipboard rows the context menu grew with its visibility settings. Lucide's
182
+ // scissors and clipboard both carry arcs; these are the cut's own square handles and square tray.
183
+ "scissors": "M5 3l14 14 M19 3L5 17 M2 17h5v5H2z M17 17h5v5h-5z",
184
+ "copy": "M9 8h12v13H9z M4 16V3h13",
185
+ "clipboard": "M9 2h6v4H9z M6 4H3v18h18V4h-3 M8 12h8 M8 16h5",
186
+ // The Keys section's rail mark: the key caps are the same zero-length-line dots the list mark uses.
187
+ "keyboard": "M2 6h20v12H2z M6 10L6.01 10 M10 10L10.01 10 M14 10L14.01 10 M18 10L18.01 10 M8 14h8",
188
+ // Invert selection's mark, lucide's contrast, its outer ring kept a true curve.
189
+ "contrast": "M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0-20 M12 18a6 6 0 0 0 0-12v12z",
190
+ // Settings > Click and refresh's Open items with mark: the arrow head and its tail, as Icons040 cuts it.
191
+ "pointer": "M4 4l16 6.5-7 2-2 7z M13 13l6 6"
192
+ }
193
+
194
+ // The sub-paths a mark fills as well as strokes, by name; ui/Glyph.qml draws them in the mark's own ink.
195
+ var FILLED = {
196
+ // MenuAdditions040: Invert selection's right half disc is solid, lucide's contrast.
197
+ "contrast": "M12 18a6 6 0 0 0 0-12v12z"
198
+ }
199
+
200
+ function filledPathFor(name) {
201
+ return FILLED[name] || ""
202
+ }
203
+
204
+ function pathFor(name) {
205
+ return PATHS[name] || PATHS[FALLBACK]
206
+ }
@@ -0,0 +1,39 @@
1
+ .pragma library
2
+
3
+ // Delete carries DEL through libxkbcommon; retain SheetQuery's single-character semantics.
4
+ function isPrintable(text) {
5
+ var s = String(text || "")
6
+ return s.length === 1 && s >= " " && s !== "\u007f"
7
+ }
8
+
9
+ // Issue 287's paste chords, matched on the four real modifiers the way ui/js/Keymap.js matches keys.
10
+ function isPasteKey(event) {
11
+ var mods = event.modifiers & (Qt.ControlModifier | Qt.ShiftModifier | Qt.AltModifier | Qt.MetaModifier)
12
+ return (event.key === Qt.Key_V && mods === Qt.ControlModifier)
13
+ || (event.key === Qt.Key_Insert && mods === Qt.ShiftModifier)
14
+ }
15
+
16
+ // The clipboard's first line, keeping printable characters and whole surrogate pairs only.
17
+ function cleanPaste(text) {
18
+ var line = String(text || "").split(/\r\n|[\r\n]/)[0]
19
+ var out = ""
20
+ for (var i = 0; i < line.length; i++) {
21
+ var code = line.charCodeAt(i)
22
+ var low = i + 1 < line.length ? line.charCodeAt(i + 1) : 0
23
+ if (code >= 0xd800 && code <= 0xdbff && low >= 0xdc00 && low <= 0xdfff) {
24
+ out += line.substr(i, 2)
25
+ i++
26
+ } else if ((code < 0xd800 || code > 0xdfff) && isPrintable(line.charAt(i))) {
27
+ out += line.charAt(i)
28
+ }
29
+ }
30
+ return out
31
+ }
32
+
33
+ // Hands a query line's paste to its own typing route; the chord is consumed even when nothing is left to type.
34
+ function pasteInto(host, typeText) {
35
+ var pasted = cleanPaste(host.clipboardText())
36
+ if (pasted.length > 0)
37
+ typeText(pasted)
38
+ return true
39
+ }
@@ -0,0 +1,64 @@
1
+ .pragma library
2
+
3
+ .import "Keymap.js" as Keymap
4
+ .import "Format.js" as Format
5
+
6
+ // A key the way the running preset binds it, spelled for a sentence; the sentences stay in their own modules with a "%1" for the key.
7
+
8
+ // The names keys.toml gives a key that a person would write as a mark.
9
+ var MARKS = { plus: "+", minus: "-", equal: "=", underscore: "_", greater: ">", less: "<", slash: "/", period: ".", comma: "," }
10
+ var MODIFIERS = { ctrl: "Ctrl", shift: "Shift", alt: "Alt", super: "Super" }
11
+
12
+ // Sample input: "ctrl-shift-n" answers "Ctrl+Shift+N"; "delete" answers "Delete"; "ctrl-shift-plus" answers "Ctrl+Shift++".
13
+ function spelled(keys) {
14
+ var parts = String(keys).split("-")
15
+ var out = []
16
+ for (var i = 0; i < parts.length; i++) {
17
+ var part = parts[i]
18
+ if (i < parts.length - 1 && MODIFIERS[part] !== undefined) { out.push(MODIFIERS[part]); continue }
19
+ if (MARKS[part] !== undefined) out.push(MARKS[part])
20
+ else if (part.length === 1) out.push(out.length > 0 ? part.toUpperCase() : part)
21
+ else out.push(part.charAt(0).toUpperCase() + part.substring(1))
22
+ }
23
+ return out.join("+")
24
+ }
25
+
26
+ // The row the running preset binds to an action in the listing, the first of the given modifier kind when one is asked for.
27
+ function rowFor(action, mods) {
28
+ var rows = Keymap.bindingRows(Keymap.preset, "gui", Keymap.actionGroup(action))
29
+ for (var i = 0; i < rows.length; i++) {
30
+ if (rows[i].action === action && (mods === undefined || rows[i].mods === mods)) return rows[i]
31
+ }
32
+ return null
33
+ }
34
+
35
+ // The key to press for an action: the plain key the menus draw, else the first chord. "" when the preset binds none.
36
+ function keyFor(action) {
37
+ var plain = Keymap.hintFor(action)
38
+ if (plain.length > 0) return plain
39
+ var row = rowFor(action)
40
+ return row === null ? "" : spelled(row.keys)
41
+ }
42
+
43
+ // The key of one modifier kind ("none" for a key pressed alone), "" when the preset binds none.
44
+ function keyOf(action, mods) {
45
+ var row = rowFor(action, mods)
46
+ return row === null ? "" : spelled(row.keys)
47
+ }
48
+
49
+ // A whole sentence with "%1" for the key bound to an action, "" when the preset binds none so no hint names a missing key.
50
+ function hinted(template, action) {
51
+ var key = keyFor(action)
52
+ return key.length > 0 ? Format.fill(template, key) : ""
53
+ }
54
+
55
+ // The same with "%1", "%2" and on for the keys bound to several actions, "" when the preset leaves any one of them unbound.
56
+ function hintedAll(template, actions) {
57
+ var keys = [template]
58
+ for (var i = 0; i < actions.length; i++) {
59
+ var key = keyFor(actions[i])
60
+ if (key.length === 0) return ""
61
+ keys.push(key)
62
+ }
63
+ return Format.fill.apply(null, keys)
64
+ }