xterm-input-panel 1.0.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.
- package/.storybook/main.ts +11 -0
- package/.storybook/preview.ts +15 -0
- package/.storybook/vitest.setup.ts +8 -0
- package/CHANGELOG.md +7 -0
- package/package.json +37 -0
- package/src/brand-icons/claude.png +0 -0
- package/src/brand-icons/codex.png +0 -0
- package/src/brand-icons/gemini.png +0 -0
- package/src/icons.ts +77 -0
- package/src/index.ts +16 -0
- package/src/input-method-tab.stories.ts +131 -0
- package/src/input-method-tab.ts +142 -0
- package/src/input-panel-settings.stories.ts +73 -0
- package/src/input-panel-settings.ts +245 -0
- package/src/input-panel.stories.ts +241 -0
- package/src/input-panel.ts +815 -0
- package/src/pixi-theme.test.ts +58 -0
- package/src/pixi-theme.ts +179 -0
- package/src/platform.ts +14 -0
- package/src/shortcut-pages.ts +204 -0
- package/src/shortcut-tab.ts +543 -0
- package/src/virtual-keyboard-layouts.ts +150 -0
- package/src/virtual-keyboard-tab.stories.ts +390 -0
- package/src/virtual-keyboard-tab.ts +642 -0
- package/src/virtual-trackpad-tab.stories.ts +476 -0
- package/src/virtual-trackpad-tab.ts +556 -0
- package/src/xterm-addon.stories.ts +413 -0
- package/src/xterm-addon.ts +947 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +13 -0
- package/vitest.storybook.config.ts +23 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit'
|
|
2
|
+
import { Application, CanvasTextMetrics, Container, FederatedPointerEvent, Graphics, Text, TextStyle } from 'pixi.js'
|
|
3
|
+
import { createElement, SquareSlash, SquareTerminal } from 'lucide'
|
|
4
|
+
import { resolvePixiTheme, onThemeChange, type PixiTheme } from './pixi-theme.js'
|
|
5
|
+
import { buildShortcutPages, type ShortcutAction, type ShortcutItem, type ShortcutPage } from './shortcut-pages.js'
|
|
6
|
+
import { detectHostPlatform, type PlatformMode } from './platform.js'
|
|
7
|
+
|
|
8
|
+
const GRID_GAP = 8
|
|
9
|
+
const GRID_PADDING = 10
|
|
10
|
+
const CARD_RADIUS = 8
|
|
11
|
+
|
|
12
|
+
const CLAUDE_ICON_URL = new URL('./brand-icons/claude.png', import.meta.url).href
|
|
13
|
+
const CODEX_ICON_URL = new URL('./brand-icons/codex.png', import.meta.url).href
|
|
14
|
+
const GEMINI_ICON_URL = new URL('./brand-icons/gemini.png', import.meta.url).href
|
|
15
|
+
|
|
16
|
+
interface RenderedShortcut {
|
|
17
|
+
item: ShortcutItem
|
|
18
|
+
container: Container
|
|
19
|
+
gfx: Graphics
|
|
20
|
+
width: number
|
|
21
|
+
height: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ShortcutTab extends LitElement {
|
|
25
|
+
static get properties() {
|
|
26
|
+
return {
|
|
27
|
+
platform: { type: String, reflect: true },
|
|
28
|
+
activePageId: { type: String, attribute: 'active-page' },
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static styles = css`
|
|
33
|
+
:host {
|
|
34
|
+
display: block;
|
|
35
|
+
height: 100%;
|
|
36
|
+
min-height: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.layout {
|
|
40
|
+
height: 100%;
|
|
41
|
+
min-height: 0;
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: 56px minmax(0, 1fr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.pages {
|
|
47
|
+
border-right: 1px solid var(--border, #333);
|
|
48
|
+
padding: 8px 6px;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 8px;
|
|
52
|
+
overflow-y: auto;
|
|
53
|
+
min-height: 0;
|
|
54
|
+
align-items: center;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.page-btn {
|
|
58
|
+
width: 40px;
|
|
59
|
+
height: 40px;
|
|
60
|
+
border: 1px solid var(--border, #333);
|
|
61
|
+
border-radius: 6px;
|
|
62
|
+
background: var(--muted, #2a2a2a);
|
|
63
|
+
color: var(--muted-foreground, #aaa);
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
padding: 0;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.page-btn:hover {
|
|
73
|
+
border-color: var(--primary, #e04a2f);
|
|
74
|
+
color: var(--foreground, #fff);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.page-btn[data-active] {
|
|
78
|
+
border-color: var(--primary, #e04a2f);
|
|
79
|
+
background: color-mix(in srgb, var(--primary, #e04a2f), transparent 85%);
|
|
80
|
+
color: var(--foreground, #fff);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.canvas-wrap {
|
|
84
|
+
position: relative;
|
|
85
|
+
min-height: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.pixi-host {
|
|
89
|
+
width: 100%;
|
|
90
|
+
height: 100%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.page-logo {
|
|
94
|
+
width: 20px;
|
|
95
|
+
height: 20px;
|
|
96
|
+
object-fit: contain;
|
|
97
|
+
display: block;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.page-vector {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
width: 20px;
|
|
103
|
+
height: 20px;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
color: currentColor;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.page-vector svg {
|
|
110
|
+
width: 18px;
|
|
111
|
+
height: 18px;
|
|
112
|
+
stroke-width: 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.sr-only {
|
|
116
|
+
position: absolute;
|
|
117
|
+
width: 1px;
|
|
118
|
+
height: 1px;
|
|
119
|
+
padding: 0;
|
|
120
|
+
margin: -1px;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
clip: rect(0, 0, 0, 0);
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
border: 0;
|
|
125
|
+
}
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
declare platform: PlatformMode
|
|
129
|
+
declare activePageId: string
|
|
130
|
+
|
|
131
|
+
private _app: Application | null = null
|
|
132
|
+
private _container: Container | null = null
|
|
133
|
+
private _resizeObserver: ResizeObserver | null = null
|
|
134
|
+
private _theme: PixiTheme = resolvePixiTheme(this)
|
|
135
|
+
private _unsubTheme: (() => void) | null = null
|
|
136
|
+
private _renderedItems: RenderedShortcut[] = []
|
|
137
|
+
|
|
138
|
+
constructor() {
|
|
139
|
+
super()
|
|
140
|
+
this.platform = 'auto'
|
|
141
|
+
this.activePageId = ''
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async connectedCallback() {
|
|
145
|
+
super.connectedCallback()
|
|
146
|
+
this._theme = resolvePixiTheme(this)
|
|
147
|
+
this._unsubTheme = onThemeChange((theme) => {
|
|
148
|
+
this._theme = theme
|
|
149
|
+
this._layoutPage()
|
|
150
|
+
}, this)
|
|
151
|
+
this._ensureActivePage()
|
|
152
|
+
await this.updateComplete
|
|
153
|
+
await this._initPixi()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
disconnectedCallback() {
|
|
157
|
+
super.disconnectedCallback()
|
|
158
|
+
this._resizeObserver?.disconnect()
|
|
159
|
+
this._resizeObserver = null
|
|
160
|
+
this._unsubTheme?.()
|
|
161
|
+
this._unsubTheme = null
|
|
162
|
+
this._app?.destroy()
|
|
163
|
+
this._app = null
|
|
164
|
+
this._container = null
|
|
165
|
+
this._renderedItems = []
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected updated(changed: Map<string, unknown>) {
|
|
169
|
+
super.updated(changed)
|
|
170
|
+
if (changed.has('platform')) {
|
|
171
|
+
this._ensureActivePage()
|
|
172
|
+
this._layoutPage()
|
|
173
|
+
}
|
|
174
|
+
if (changed.has('activePageId')) {
|
|
175
|
+
this._layoutPage()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private _hostPlatform() {
|
|
180
|
+
if (this.platform === 'windows' || this.platform === 'macos' || this.platform === 'common') {
|
|
181
|
+
return this.platform
|
|
182
|
+
}
|
|
183
|
+
return detectHostPlatform()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private _pages(): ShortcutPage[] {
|
|
187
|
+
return buildShortcutPages(this._hostPlatform())
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private _activePage(): ShortcutPage {
|
|
191
|
+
const pages = this._pages()
|
|
192
|
+
return pages.find((page) => page.id === this.activePageId) ?? pages[0]!
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private _ensureActivePage() {
|
|
196
|
+
const pages = this._pages()
|
|
197
|
+
if (!pages.length) return
|
|
198
|
+
const exists = pages.some((page) => page.id === this.activePageId)
|
|
199
|
+
if (!exists) {
|
|
200
|
+
this.activePageId = pages[0]!.id
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async _initPixi() {
|
|
205
|
+
const host = this.shadowRoot?.querySelector('.pixi-host') as HTMLElement
|
|
206
|
+
if (!host) return
|
|
207
|
+
|
|
208
|
+
const app = new Application()
|
|
209
|
+
await app.init({
|
|
210
|
+
background: this._theme.background,
|
|
211
|
+
antialias: true,
|
|
212
|
+
resolution: window.devicePixelRatio || 1,
|
|
213
|
+
autoDensity: false,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
host.appendChild(app.canvas as HTMLCanvasElement)
|
|
217
|
+
this._app = app
|
|
218
|
+
|
|
219
|
+
const canvas = app.canvas as HTMLCanvasElement
|
|
220
|
+
canvas.style.width = '100%'
|
|
221
|
+
canvas.style.height = '100%'
|
|
222
|
+
canvas.style.display = 'block'
|
|
223
|
+
canvas.addEventListener('contextmenu', (event) => event.preventDefault())
|
|
224
|
+
|
|
225
|
+
const width = host.clientWidth
|
|
226
|
+
const height = host.clientHeight
|
|
227
|
+
if (width > 0 && height > 0) {
|
|
228
|
+
app.renderer.resize(width, height)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this._container = new Container()
|
|
232
|
+
app.stage.addChild(this._container)
|
|
233
|
+
this._layoutPage()
|
|
234
|
+
|
|
235
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
236
|
+
const nextWidth = host.clientWidth
|
|
237
|
+
const nextHeight = host.clientHeight
|
|
238
|
+
if (nextWidth > 0 && nextHeight > 0) {
|
|
239
|
+
app.renderer.resize(nextWidth, nextHeight)
|
|
240
|
+
this._layoutPage()
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
this._resizeObserver.observe(host)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private _drawCard(gfx: Graphics, width: number, height: number, pressed: boolean) {
|
|
247
|
+
gfx.clear()
|
|
248
|
+
gfx.roundRect(0, 0, width, height, CARD_RADIUS)
|
|
249
|
+
gfx.fill({
|
|
250
|
+
color: pressed
|
|
251
|
+
? this._theme.keyPressed
|
|
252
|
+
: this._theme.keyNormal,
|
|
253
|
+
})
|
|
254
|
+
gfx.stroke({
|
|
255
|
+
color: pressed ? this._theme.accent : this._theme.surfaceBorder,
|
|
256
|
+
width: pressed ? 1.5 : 1,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private _drawDpad(gfx: Graphics, width: number, height: number, pressed: boolean) {
|
|
261
|
+
const radius = Math.min(width, height) * 0.42
|
|
262
|
+
const cx = width / 2
|
|
263
|
+
const cy = height / 2
|
|
264
|
+
|
|
265
|
+
gfx.clear()
|
|
266
|
+
gfx.roundRect(0, 0, width, height, CARD_RADIUS)
|
|
267
|
+
gfx.fill({ color: this._theme.keyNormal })
|
|
268
|
+
gfx.stroke({ color: this._theme.surfaceBorder, width: 1 })
|
|
269
|
+
|
|
270
|
+
gfx.circle(cx, cy, radius)
|
|
271
|
+
gfx.fill({ color: pressed ? this._theme.keyPressed : this._theme.keyModifier })
|
|
272
|
+
gfx.stroke({ color: this._theme.surfaceBorder, width: 1 })
|
|
273
|
+
|
|
274
|
+
gfx.moveTo(cx - radius, cy)
|
|
275
|
+
gfx.lineTo(cx + radius, cy)
|
|
276
|
+
gfx.moveTo(cx, cy - radius)
|
|
277
|
+
gfx.lineTo(cx, cy + radius)
|
|
278
|
+
gfx.stroke({ color: this._theme.surfaceBorder, width: 1 })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private _measureTextWidth(text: Text): number {
|
|
282
|
+
const content = typeof text.text === 'string' ? text.text : String(text.text ?? '')
|
|
283
|
+
if (!content) return 0
|
|
284
|
+
try {
|
|
285
|
+
return CanvasTextMetrics.measureText(content, text.style).width
|
|
286
|
+
} catch {
|
|
287
|
+
return text.width / Math.max(text.scale.x, 0.0001)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private _fitText(text: Text, maxWidth: number, minScale = 0.5) {
|
|
292
|
+
if (maxWidth <= 0) return
|
|
293
|
+
text.scale.set(1)
|
|
294
|
+
const width = this._measureTextWidth(text)
|
|
295
|
+
if (width <= maxWidth) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
const scale = Math.max(minScale, maxWidth / width)
|
|
299
|
+
text.scale.set(scale)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private _layoutPage() {
|
|
303
|
+
const app = this._app
|
|
304
|
+
const container = this._container
|
|
305
|
+
if (!app || !container) return
|
|
306
|
+
|
|
307
|
+
container.removeChildren()
|
|
308
|
+
this._renderedItems = []
|
|
309
|
+
|
|
310
|
+
const page = this._activePage()
|
|
311
|
+
const width = app.screen.width
|
|
312
|
+
const height = app.screen.height
|
|
313
|
+
|
|
314
|
+
const cellWidth = (width - GRID_PADDING * 2 - GRID_GAP * (page.cols - 1)) / page.cols
|
|
315
|
+
const cellHeight = (height - GRID_PADDING * 2 - GRID_GAP * (page.rows - 1)) / page.rows
|
|
316
|
+
|
|
317
|
+
for (const item of page.items) {
|
|
318
|
+
const cols = item.cols ?? 1
|
|
319
|
+
const rows = item.rows ?? 1
|
|
320
|
+
const x = GRID_PADDING + item.col * (cellWidth + GRID_GAP)
|
|
321
|
+
const y = GRID_PADDING + item.row * (cellHeight + GRID_GAP)
|
|
322
|
+
const cardWidth = cellWidth * cols + GRID_GAP * (cols - 1)
|
|
323
|
+
const cardHeight = cellHeight * rows + GRID_GAP * (rows - 1)
|
|
324
|
+
|
|
325
|
+
const itemContainer = new Container()
|
|
326
|
+
itemContainer.x = x
|
|
327
|
+
itemContainer.y = y
|
|
328
|
+
itemContainer.eventMode = 'static'
|
|
329
|
+
itemContainer.cursor = 'pointer'
|
|
330
|
+
|
|
331
|
+
const gfx = new Graphics()
|
|
332
|
+
itemContainer.addChild(gfx)
|
|
333
|
+
|
|
334
|
+
if (item.kind === 'dpad') {
|
|
335
|
+
this._drawDpad(gfx, cardWidth, cardHeight, false)
|
|
336
|
+
} else {
|
|
337
|
+
this._drawCard(gfx, cardWidth, cardHeight, false)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const label = new Text({
|
|
341
|
+
text: item.label,
|
|
342
|
+
style: new TextStyle({
|
|
343
|
+
fontFamily: 'ui-monospace, SFMono-Regular, monospace',
|
|
344
|
+
fontSize: cardHeight > 48 ? 12 : 11,
|
|
345
|
+
fill: this._theme.text,
|
|
346
|
+
align: 'center',
|
|
347
|
+
}),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
if (item.kind === 'dpad') {
|
|
351
|
+
label.anchor.set(0.5)
|
|
352
|
+
label.x = cardWidth / 2
|
|
353
|
+
label.y = cardHeight / 2
|
|
354
|
+
} else {
|
|
355
|
+
label.anchor.set(0.5)
|
|
356
|
+
label.x = cardWidth / 2
|
|
357
|
+
label.y = cardHeight / 2
|
|
358
|
+
}
|
|
359
|
+
this._fitText(label, cardWidth - 12)
|
|
360
|
+
itemContainer.addChild(label)
|
|
361
|
+
|
|
362
|
+
itemContainer.on('pointerdown', () => {
|
|
363
|
+
if (item.kind === 'dpad') {
|
|
364
|
+
this._drawDpad(gfx, cardWidth, cardHeight, true)
|
|
365
|
+
} else {
|
|
366
|
+
this._drawCard(gfx, cardWidth, cardHeight, true)
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
itemContainer.on('pointerleave', () => {
|
|
371
|
+
if (item.kind === 'dpad') {
|
|
372
|
+
this._drawDpad(gfx, cardWidth, cardHeight, false)
|
|
373
|
+
} else {
|
|
374
|
+
this._drawCard(gfx, cardWidth, cardHeight, false)
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
itemContainer.on('pointerupoutside', () => {
|
|
379
|
+
if (item.kind === 'dpad') {
|
|
380
|
+
this._drawDpad(gfx, cardWidth, cardHeight, false)
|
|
381
|
+
} else {
|
|
382
|
+
this._drawCard(gfx, cardWidth, cardHeight, false)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
itemContainer.on('pointerup', (event: FederatedPointerEvent) => {
|
|
387
|
+
if (item.kind === 'dpad') {
|
|
388
|
+
this._drawDpad(gfx, cardWidth, cardHeight, false)
|
|
389
|
+
} else {
|
|
390
|
+
this._drawCard(gfx, cardWidth, cardHeight, false)
|
|
391
|
+
}
|
|
392
|
+
void this._activateShortcut(item, event, cardWidth, cardHeight, itemContainer)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
container.addChild(itemContainer)
|
|
396
|
+
this._renderedItems.push({
|
|
397
|
+
item,
|
|
398
|
+
container: itemContainer,
|
|
399
|
+
gfx,
|
|
400
|
+
width: cardWidth,
|
|
401
|
+
height: cardHeight,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private _send(data: string) {
|
|
407
|
+
this.dispatchEvent(
|
|
408
|
+
new CustomEvent('input-panel:send', {
|
|
409
|
+
detail: { data },
|
|
410
|
+
bubbles: true,
|
|
411
|
+
composed: true,
|
|
412
|
+
}),
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private async _handleCommand(command: 'copy' | 'paste' | 'select-all') {
|
|
417
|
+
if (command === 'copy') {
|
|
418
|
+
const selection = window.getSelection()?.toString() ?? ''
|
|
419
|
+
if (!selection) return
|
|
420
|
+
try {
|
|
421
|
+
await navigator.clipboard.writeText(selection)
|
|
422
|
+
} catch {
|
|
423
|
+
document.execCommand('copy')
|
|
424
|
+
}
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (command === 'paste') {
|
|
429
|
+
try {
|
|
430
|
+
const text = await navigator.clipboard.readText()
|
|
431
|
+
if (text) this._send(text)
|
|
432
|
+
} catch {
|
|
433
|
+
// ignore clipboard permission failures
|
|
434
|
+
}
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const active = document.activeElement
|
|
439
|
+
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
|
|
440
|
+
active.select()
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
document.execCommand('selectAll')
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private _dpadData(event: FederatedPointerEvent, width: number, height: number, container: Container): string {
|
|
447
|
+
const local = container.toLocal(event.global)
|
|
448
|
+
const dx = local.x - width / 2
|
|
449
|
+
const dy = local.y - height / 2
|
|
450
|
+
|
|
451
|
+
if (Math.abs(dx) <= 8 && Math.abs(dy) <= 8) {
|
|
452
|
+
return '\r'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
456
|
+
return dx < 0 ? '\x1b[D' : '\x1b[C'
|
|
457
|
+
}
|
|
458
|
+
return dy < 0 ? '\x1b[A' : '\x1b[B'
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async _activateShortcut(
|
|
462
|
+
item: ShortcutItem,
|
|
463
|
+
event: FederatedPointerEvent,
|
|
464
|
+
width: number,
|
|
465
|
+
height: number,
|
|
466
|
+
container: Container,
|
|
467
|
+
) {
|
|
468
|
+
if (item.kind === 'dpad') {
|
|
469
|
+
this._send(this._dpadData(event, width, height, container))
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const action: ShortcutAction | undefined = item.action
|
|
474
|
+
if (!action) return
|
|
475
|
+
|
|
476
|
+
if (action.type === 'send') {
|
|
477
|
+
this._send(action.data)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (action.type === 'text') {
|
|
482
|
+
this._send(action.text)
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await this._handleCommand(action.command)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private _setActivePage(pageId: string) {
|
|
490
|
+
if (this.activePageId === pageId) return
|
|
491
|
+
this.activePageId = pageId
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private _renderPageIcon(pageId: string) {
|
|
495
|
+
if (pageId === 'system') {
|
|
496
|
+
return html`<span class="page-vector">${createElement(SquareTerminal)}</span>`
|
|
497
|
+
}
|
|
498
|
+
if (pageId === 'terminal') {
|
|
499
|
+
return html`<span class="page-vector">${createElement(SquareSlash)}</span>`
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (pageId === 'claude') {
|
|
503
|
+
return html`<img class="page-logo" src=${CLAUDE_ICON_URL} alt="" />`
|
|
504
|
+
}
|
|
505
|
+
if (pageId === 'codex') {
|
|
506
|
+
return html`<img class="page-logo" src=${CODEX_ICON_URL} alt="" />`
|
|
507
|
+
}
|
|
508
|
+
if (pageId === 'gemini') {
|
|
509
|
+
return html`<img class="page-logo" src=${GEMINI_ICON_URL} alt="" />`
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return html`<span class="page-vector">${createElement(SquareSlash)}</span>`
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
render() {
|
|
516
|
+
const pages = this._pages()
|
|
517
|
+
const activePage = this._activePage()
|
|
518
|
+
return html`
|
|
519
|
+
<div class="layout">
|
|
520
|
+
<div class="pages">
|
|
521
|
+
${pages.map((page) => html`
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
class="page-btn"
|
|
525
|
+
title=${page.title}
|
|
526
|
+
aria-label=${page.title}
|
|
527
|
+
?data-active=${page.id === activePage.id}
|
|
528
|
+
@click=${() => this._setActivePage(page.id)}
|
|
529
|
+
>
|
|
530
|
+
${this._renderPageIcon(page.id)}
|
|
531
|
+
<span class="sr-only">${page.title}</span>
|
|
532
|
+
</button>
|
|
533
|
+
`)}
|
|
534
|
+
</div>
|
|
535
|
+
<div class="canvas-wrap">
|
|
536
|
+
<div class="pixi-host"></div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
`
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
customElements.define('shortcut-tab', ShortcutTab)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { HostPlatform } from './platform.js'
|
|
2
|
+
|
|
3
|
+
export type ModifierKey = 'ctrl' | 'alt' | 'meta' | 'shift' | 'caps'
|
|
4
|
+
|
|
5
|
+
export interface KeyDef {
|
|
6
|
+
label: string
|
|
7
|
+
data: string
|
|
8
|
+
w?: number
|
|
9
|
+
modifier?: ModifierKey
|
|
10
|
+
special?: 'chord'
|
|
11
|
+
shift?: { label: string; data: string }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const letter = (char: string): KeyDef => ({
|
|
15
|
+
label: char,
|
|
16
|
+
data: char,
|
|
17
|
+
shift: { label: char.toUpperCase(), data: char.toUpperCase() },
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const NAV_ROW: KeyDef[] = [
|
|
21
|
+
{ label: 'Esc', data: '\x1b', w: 1.1 },
|
|
22
|
+
{ label: 'Home', data: '\x1b[H', w: 1.2 },
|
|
23
|
+
{ label: 'End', data: '\x1b[F', w: 1.2 },
|
|
24
|
+
{ label: 'PgUp', data: '\x1b[5~', w: 1.2 },
|
|
25
|
+
{ label: 'PgDn', data: '\x1b[6~', w: 1.2 },
|
|
26
|
+
{ label: '⬅', data: '\x1b[D', w: 1 },
|
|
27
|
+
{ label: '⬆', data: '\x1b[A', w: 1 },
|
|
28
|
+
{ label: '⬇', data: '\x1b[B', w: 1 },
|
|
29
|
+
{ label: '⮕', data: '\x1b[C', w: 1 },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const NUMBER_KEYS: KeyDef[] = [
|
|
33
|
+
{ label: '`', data: '`', shift: { label: '~', data: '~' } },
|
|
34
|
+
{ label: '1', data: '1', shift: { label: '!', data: '!' } },
|
|
35
|
+
{ label: '2', data: '2', shift: { label: '@', data: '@' } },
|
|
36
|
+
{ label: '3', data: '3', shift: { label: '#', data: '#' } },
|
|
37
|
+
{ label: '4', data: '4', shift: { label: '$', data: '$' } },
|
|
38
|
+
{ label: '5', data: '5', shift: { label: '%', data: '%' } },
|
|
39
|
+
{ label: '6', data: '6', shift: { label: '^', data: '^' } },
|
|
40
|
+
{ label: '7', data: '7', shift: { label: '&', data: '&' } },
|
|
41
|
+
{ label: '8', data: '8', shift: { label: '*', data: '*' } },
|
|
42
|
+
{ label: '9', data: '9', shift: { label: '(', data: '(' } },
|
|
43
|
+
{ label: '0', data: '0', shift: { label: ')', data: ')' } },
|
|
44
|
+
{ label: '-', data: '-', shift: { label: '_', data: '_' } },
|
|
45
|
+
{ label: '=', data: '=', shift: { label: '+', data: '+' } },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const LETTERS_Q_ROW: KeyDef[] = 'qwertyuiop'.split('').map(letter)
|
|
49
|
+
const LETTERS_A_ROW: KeyDef[] = 'asdfghjkl'.split('').map(letter)
|
|
50
|
+
const LETTERS_Z_ROW: KeyDef[] = 'zxcvbnm'.split('').map(letter)
|
|
51
|
+
|
|
52
|
+
const Q_ROW: KeyDef[] = [
|
|
53
|
+
{ label: 'Tab', data: '\t', w: 1.5 },
|
|
54
|
+
...LETTERS_Q_ROW,
|
|
55
|
+
{
|
|
56
|
+
label: '[',
|
|
57
|
+
data: '[',
|
|
58
|
+
shift: { label: '{', data: '{' },
|
|
59
|
+
w: 1,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: ']',
|
|
63
|
+
data: ']',
|
|
64
|
+
shift: { label: '}', data: '}' },
|
|
65
|
+
w: 1,
|
|
66
|
+
},
|
|
67
|
+
{ label: '\\', data: '\\', shift: { label: '|', data: '|' }, w: 1.3 },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const CAPS_ROW_COMMON: KeyDef[] = [
|
|
71
|
+
{ label: 'Caps', data: '', modifier: 'caps', w: 1.8 },
|
|
72
|
+
...LETTERS_A_ROW,
|
|
73
|
+
{ label: ';', data: ';', shift: { label: ':', data: ':' } },
|
|
74
|
+
{ label: "'", data: "'", shift: { label: '"', data: '"' } },
|
|
75
|
+
{ label: 'Enter', data: '\r', w: 2.1 },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const CAPS_ROW_MACOS: KeyDef[] = [
|
|
79
|
+
{ label: 'Caps', data: '', modifier: 'caps', w: 1.8 },
|
|
80
|
+
...LETTERS_A_ROW,
|
|
81
|
+
{ label: ';', data: ';', shift: { label: ':', data: ':' } },
|
|
82
|
+
{ label: "'", data: "'", shift: { label: '"', data: '"' } },
|
|
83
|
+
{ label: 'Return', data: '\r', w: 2.1 },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const SHIFT_ROW: KeyDef[] = [
|
|
87
|
+
{ label: 'Shift', data: '', modifier: 'shift', w: 2.2 },
|
|
88
|
+
...LETTERS_Z_ROW,
|
|
89
|
+
{ label: ',', data: ',', shift: { label: '<', data: '<' } },
|
|
90
|
+
{ label: '.', data: '.', shift: { label: '>', data: '>' } },
|
|
91
|
+
{ label: '/', data: '/', shift: { label: '?', data: '?' } },
|
|
92
|
+
{ label: 'Shift', data: '', modifier: 'shift', w: 2.2 },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const COMMON_BOTTOM_ROW: KeyDef[] = [
|
|
96
|
+
{ label: 'Ctrl', data: '', modifier: 'ctrl', w: 1.4 },
|
|
97
|
+
{ label: 'Alt', data: '', modifier: 'alt', w: 1.4 },
|
|
98
|
+
{ label: 'Meta', data: '', modifier: 'meta', w: 1.6 },
|
|
99
|
+
{ label: 'Space', data: ' ', w: 6.1 },
|
|
100
|
+
{ label: '⌬', data: '', special: 'chord', w: 1.9 },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
const WINDOWS_BOTTOM_ROW: KeyDef[] = [
|
|
104
|
+
{ label: 'Ctrl', data: '', modifier: 'ctrl', w: 1.4 },
|
|
105
|
+
{ label: 'Win', data: '', modifier: 'meta', w: 1.6 },
|
|
106
|
+
{ label: 'Alt', data: '', modifier: 'alt', w: 1.4 },
|
|
107
|
+
{ label: 'Space', data: ' ', w: 6.1 },
|
|
108
|
+
{ label: '⌬', data: '', special: 'chord', w: 1.9 },
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
const MACOS_BOTTOM_ROW: KeyDef[] = [
|
|
112
|
+
{ label: 'Control', data: '', modifier: 'ctrl', w: 1.4 },
|
|
113
|
+
{ label: 'Option', data: '', modifier: 'alt', w: 1.5 },
|
|
114
|
+
{ label: 'Command', data: '', modifier: 'meta', w: 1.8 },
|
|
115
|
+
{ label: 'Space', data: ' ', w: 5.8 },
|
|
116
|
+
{ label: '⌬', data: '', special: 'chord', w: 1.9 },
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
const COMMON_ROWS: KeyDef[][] = [
|
|
120
|
+
NAV_ROW,
|
|
121
|
+
[...NUMBER_KEYS, { label: 'Bksp', data: '\x7f', w: 1.8 }],
|
|
122
|
+
Q_ROW,
|
|
123
|
+
CAPS_ROW_COMMON,
|
|
124
|
+
SHIFT_ROW,
|
|
125
|
+
COMMON_BOTTOM_ROW,
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
const WINDOWS_ROWS: KeyDef[][] = [
|
|
129
|
+
NAV_ROW,
|
|
130
|
+
[...NUMBER_KEYS, { label: 'Bksp', data: '\x7f', w: 1.8 }],
|
|
131
|
+
Q_ROW,
|
|
132
|
+
CAPS_ROW_COMMON,
|
|
133
|
+
SHIFT_ROW,
|
|
134
|
+
WINDOWS_BOTTOM_ROW,
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const MACOS_ROWS: KeyDef[][] = [
|
|
138
|
+
NAV_ROW,
|
|
139
|
+
[...NUMBER_KEYS, { label: 'Delete', data: '\x7f', w: 1.8 }],
|
|
140
|
+
Q_ROW,
|
|
141
|
+
CAPS_ROW_MACOS,
|
|
142
|
+
SHIFT_ROW,
|
|
143
|
+
MACOS_BOTTOM_ROW,
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
export const LAYOUTS: Record<HostPlatform, KeyDef[][]> = {
|
|
147
|
+
windows: WINDOWS_ROWS,
|
|
148
|
+
macos: MACOS_ROWS,
|
|
149
|
+
common: COMMON_ROWS,
|
|
150
|
+
}
|