pinokiod 3.270.0 → 3.272.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 (56) hide show
  1. package/kernel/ansi_stream_tracker.js +115 -0
  2. package/kernel/api/app/index.js +422 -0
  3. package/kernel/api/htmlmodal/index.js +94 -0
  4. package/kernel/app_launcher/index.js +115 -0
  5. package/kernel/app_launcher/platform/base.js +276 -0
  6. package/kernel/app_launcher/platform/linux.js +229 -0
  7. package/kernel/app_launcher/platform/macos.js +163 -0
  8. package/kernel/app_launcher/platform/unsupported.js +34 -0
  9. package/kernel/app_launcher/platform/windows.js +247 -0
  10. package/kernel/bin/conda-meta.js +93 -0
  11. package/kernel/bin/conda.js +2 -4
  12. package/kernel/bin/index.js +2 -4
  13. package/kernel/index.js +9 -2
  14. package/kernel/peer.js +186 -19
  15. package/kernel/shell.js +212 -1
  16. package/package.json +1 -1
  17. package/server/index.js +491 -6
  18. package/server/public/common.js +224 -741
  19. package/server/public/create-launcher.js +754 -0
  20. package/server/public/htmlmodal.js +292 -0
  21. package/server/public/logs.js +715 -0
  22. package/server/public/resizeSync.js +117 -0
  23. package/server/public/style.css +651 -6
  24. package/server/public/tab-idle-notifier.js +34 -59
  25. package/server/public/tab-link-popover.js +7 -10
  26. package/server/public/terminal-settings.js +723 -9
  27. package/server/public/terminal_input_utils.js +72 -0
  28. package/server/public/terminal_key_caption.js +187 -0
  29. package/server/public/urldropdown.css +120 -3
  30. package/server/public/xterm-inline-bridge.js +116 -0
  31. package/server/socket.js +29 -0
  32. package/server/views/agents.ejs +1 -2
  33. package/server/views/app.ejs +55 -28
  34. package/server/views/bookmarklet.ejs +1 -1
  35. package/server/views/bootstrap.ejs +1 -0
  36. package/server/views/connect.ejs +1 -2
  37. package/server/views/create.ejs +63 -0
  38. package/server/views/editor.ejs +36 -4
  39. package/server/views/index.ejs +1 -2
  40. package/server/views/index2.ejs +1 -2
  41. package/server/views/init/index.ejs +36 -28
  42. package/server/views/install.ejs +20 -22
  43. package/server/views/layout.ejs +2 -8
  44. package/server/views/logs.ejs +155 -0
  45. package/server/views/mini.ejs +0 -18
  46. package/server/views/net.ejs +2 -2
  47. package/server/views/network.ejs +1 -2
  48. package/server/views/network2.ejs +1 -2
  49. package/server/views/old_network.ejs +1 -2
  50. package/server/views/pro.ejs +26 -23
  51. package/server/views/prototype/index.ejs +30 -34
  52. package/server/views/screenshots.ejs +1 -2
  53. package/server/views/settings.ejs +1 -20
  54. package/server/views/shell.ejs +59 -66
  55. package/server/views/terminal.ejs +118 -73
  56. package/server/views/tools.ejs +1 -2
@@ -59,17 +59,639 @@
59
59
  const THEME_KEY_ALIASES = {
60
60
  selection: 'selectionBackground'
61
61
  };
62
+ const DIRECT_TYPING_PREF_KEY = 'pinokio.terminal.directTyping';
62
63
 
63
64
  function isFiniteNumber(value) {
64
65
  return typeof value === 'number' && Number.isFinite(value);
65
66
  }
66
67
 
68
+ class TerminalMobileInput {
69
+ constructor(settings) {
70
+ this.settings = settings;
71
+ this.runnerButtons = new WeakMap();
72
+ this.termRecords = new Map();
73
+ this.modal = null;
74
+ this.backdrop = null;
75
+ this.textarea = null;
76
+ this.newlineCheckbox = null;
77
+ this.statusElement = null;
78
+ this.statusTimer = null;
79
+ this.modalOpen = false;
80
+ this.lastTrigger = null;
81
+ this.tapTrackers = new WeakMap();
82
+ this.supportsPointer = typeof window !== 'undefined' && 'PointerEvent' in window;
83
+ this.escapeHandler = (event) => {
84
+ if (!event || event.key !== 'Escape' || !this.modalOpen) {
85
+ return;
86
+ }
87
+ event.preventDefault();
88
+ this.closeModal();
89
+ };
90
+ const stored = this.loadDirectTypingPreference();
91
+ if (stored === null) {
92
+ this.directTypingEnabled = !this.shouldPreferModalInput();
93
+ } else {
94
+ this.directTypingEnabled = stored;
95
+ }
96
+ }
97
+
98
+ shouldPreferModalInput() {
99
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
100
+ try {
101
+ if (window.matchMedia('(pointer: coarse)').matches) {
102
+ return true;
103
+ }
104
+ } catch (_) {}
105
+ try {
106
+ if (window.matchMedia('(max-width: 768px)').matches) {
107
+ return true;
108
+ }
109
+ } catch (_) {}
110
+ }
111
+ if (typeof navigator !== 'undefined') {
112
+ const ua = navigator.userAgent || '';
113
+ if (/Mobi|Android|iPhone|iPad|Tablet/i.test(ua)) {
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+
120
+ loadDirectTypingPreference() {
121
+ if (typeof window === 'undefined' || !window.localStorage) {
122
+ return null;
123
+ }
124
+ try {
125
+ const stored = window.localStorage.getItem(DIRECT_TYPING_PREF_KEY);
126
+ if (stored === '1') {
127
+ return true;
128
+ }
129
+ if (stored === '0') {
130
+ return false;
131
+ }
132
+ } catch (_) {}
133
+ return null;
134
+ }
135
+
136
+ saveDirectTypingPreference(value) {
137
+ if (typeof window === 'undefined' || !window.localStorage) {
138
+ return;
139
+ }
140
+ try {
141
+ if (typeof value === 'boolean') {
142
+ window.localStorage.setItem(DIRECT_TYPING_PREF_KEY, value ? '1' : '0');
143
+ } else {
144
+ window.localStorage.removeItem(DIRECT_TYPING_PREF_KEY);
145
+ }
146
+ } catch (_) {}
147
+ }
148
+
149
+ registerTerminal(term) {
150
+ if (!term || this.termRecords.has(term)) {
151
+ return;
152
+ }
153
+ const record = {
154
+ term,
155
+ textarea: null,
156
+ renderDisposable: null
157
+ };
158
+ this.termRecords.set(term, record);
159
+ if (!term._pinokioMobileInputPatchedOpen && typeof term.open === 'function') {
160
+ const originalOpen = term.open;
161
+ const mobileInput = this;
162
+ term.open = function patchedOpen(...args) {
163
+ const result = originalOpen.apply(this, args);
164
+ try {
165
+ mobileInput.configureTapListener(term);
166
+ } catch (_) {}
167
+ return result;
168
+ };
169
+ term._pinokioMobileInputPatchedOpen = true;
170
+ }
171
+ const capture = () => {
172
+ if (!this.termRecords.has(term)) {
173
+ return true;
174
+ }
175
+ const textarea = this.getTermTextarea(term);
176
+ if (!textarea) {
177
+ return false;
178
+ }
179
+ record.textarea = textarea;
180
+ this.applyInputPolicy(textarea);
181
+ return true;
182
+ };
183
+ if (!capture()) {
184
+ if (typeof term.onRender === 'function') {
185
+ record.renderDisposable = term.onRender(() => {
186
+ if (capture() && record.renderDisposable && typeof record.renderDisposable.dispose === 'function') {
187
+ record.renderDisposable.dispose();
188
+ record.renderDisposable = null;
189
+ }
190
+ });
191
+ }
192
+ if (!record.renderDisposable) {
193
+ let attempts = 0;
194
+ const poll = () => {
195
+ if (!this.termRecords.has(term) || record.textarea) {
196
+ return;
197
+ }
198
+ attempts += 1;
199
+ if (capture()) {
200
+ return;
201
+ }
202
+ if (attempts < 60) {
203
+ setTimeout(poll, 100);
204
+ }
205
+ };
206
+ setTimeout(poll, 50);
207
+ }
208
+ }
209
+ this.configureTapListener(term);
210
+ }
211
+
212
+ unregisterTerminal(term) {
213
+ const record = this.termRecords.get(term);
214
+ if (!record) {
215
+ return;
216
+ }
217
+ if (record.renderDisposable && typeof record.renderDisposable.dispose === 'function') {
218
+ record.renderDisposable.dispose();
219
+ }
220
+ this.termRecords.delete(term);
221
+ this.removeTapListener(term);
222
+ }
223
+
224
+ getTermTextarea(term) {
225
+ if (!term) {
226
+ return null;
227
+ }
228
+ if (term.textarea && typeof term.textarea.focus === 'function') {
229
+ return term.textarea;
230
+ }
231
+ if (term._core && term._core._textarea && typeof term._core._textarea.focus === 'function') {
232
+ return term._core._textarea;
233
+ }
234
+ return null;
235
+ }
236
+
237
+ applyInputPolicy(textarea) {
238
+ if (!textarea) {
239
+ return;
240
+ }
241
+ if (this.directTypingEnabled) {
242
+ textarea.removeAttribute('inputmode');
243
+ textarea.removeAttribute('readonly');
244
+ textarea.removeAttribute('aria-readonly');
245
+ } else {
246
+ textarea.setAttribute('inputmode', 'none');
247
+ textarea.setAttribute('readonly', 'readonly');
248
+ textarea.setAttribute('aria-readonly', 'true');
249
+ }
250
+ }
251
+
252
+ applyPolicyToAll() {
253
+ this.termRecords.forEach((record) => {
254
+ if (record && record.textarea) {
255
+ this.applyInputPolicy(record.textarea);
256
+ }
257
+ if (record && record.term) {
258
+ this.configureTapListener(record.term);
259
+ }
260
+ });
261
+ }
262
+
263
+ setDirectTypingEnabled(enabled) {
264
+ const next = Boolean(enabled);
265
+ if (next === this.directTypingEnabled) {
266
+ return;
267
+ }
268
+ this.directTypingEnabled = next;
269
+ this.saveDirectTypingPreference(next);
270
+ this.applyPolicyToAll();
271
+ }
272
+
273
+ attachKeyboardButton(runner, host) {
274
+ if (!runner || this.runnerButtons.has(runner)) {
275
+ return;
276
+ }
277
+ const button = document.createElement('button');
278
+ button.type = 'button';
279
+ button.className = 'btn terminal-keyboard-button';
280
+ button.innerHTML = '<i class="fa-solid fa-keyboard"></i> Input';
281
+ button.addEventListener('click', (event) => {
282
+ event.preventDefault();
283
+ this.lastTrigger = button;
284
+ this.openModal();
285
+ });
286
+ if (host && host.firstChild) {
287
+ host.insertBefore(button, host.firstChild);
288
+ } else if (host) {
289
+ host.appendChild(button);
290
+ } else {
291
+ runner.appendChild(button);
292
+ }
293
+ this.runnerButtons.set(runner, { button });
294
+ }
295
+
296
+ ensureModalElements() {
297
+ if (this.modal || typeof document === 'undefined' || !document.body) {
298
+ return;
299
+ }
300
+ const backdrop = document.createElement('div');
301
+ backdrop.className = 'terminal-keyboard-backdrop';
302
+ backdrop.hidden = true;
303
+
304
+ const modal = document.createElement('div');
305
+ modal.className = 'terminal-keyboard-modal';
306
+ modal.setAttribute('role', 'dialog');
307
+ modal.setAttribute('aria-modal', 'true');
308
+ modal.setAttribute('aria-labelledby', 'terminal-keyboard-title');
309
+ modal.hidden = true;
310
+
311
+
312
+
313
+ const form = document.createElement('form');
314
+ form.className = 'terminal-keyboard-form';
315
+ form.addEventListener('submit', (event) => {
316
+ event.preventDefault();
317
+ this.submitInput();
318
+ });
319
+
320
+ const textarea = document.createElement('textarea');
321
+ textarea.className = 'terminal-keyboard-textarea';
322
+ textarea.placeholder = 'Enter command';
323
+ textarea.rows = 4;
324
+ textarea.autocapitalize = 'off';
325
+ textarea.autocomplete = 'off';
326
+ textarea.spellcheck = false;
327
+ textarea.addEventListener('keydown', (event) => {
328
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
329
+ event.preventDefault();
330
+ this.submitInput();
331
+ }
332
+ });
333
+
334
+ const options = document.createElement('div');
335
+ options.className = 'terminal-keyboard-options';
336
+
337
+ const newlineOption = document.createElement('label');
338
+ newlineOption.className = 'terminal-keyboard-option';
339
+ const newlineCheckbox = document.createElement('input');
340
+ newlineCheckbox.type = 'checkbox';
341
+ newlineCheckbox.className = 'terminal-keyboard-checkbox';
342
+ newlineCheckbox.checked = true;
343
+ const newlineText = document.createElement('span');
344
+ newlineText.textContent = 'Append newline on send';
345
+ newlineOption.appendChild(newlineCheckbox);
346
+ newlineOption.appendChild(newlineText);
347
+
348
+ options.appendChild(newlineOption);
349
+
350
+ const status = document.createElement('div');
351
+ status.className = 'terminal-keyboard-status';
352
+ status.setAttribute('role', 'status');
353
+ status.setAttribute('aria-live', 'polite');
354
+
355
+ const actions = document.createElement('div');
356
+ actions.className = 'terminal-keyboard-actions';
357
+
358
+ const actionsLeft = document.createElement('div');
359
+ actionsLeft.className = 'terminal-keyboard-actions-left';
360
+
361
+ const directTypingButton = document.createElement('button');
362
+ directTypingButton.type = 'button';
363
+ directTypingButton.className = 'btn terminal-keyboard-direct-button';
364
+ directTypingButton.innerHTML = '<i class="fa-solid fa-keyboard"></i> Use Terminal';
365
+ directTypingButton.addEventListener('click', () => this.enableDirectTypingFromModal());
366
+ actionsLeft.appendChild(directTypingButton);
367
+
368
+ const actionsRight = document.createElement('div');
369
+ actionsRight.className = 'terminal-keyboard-actions-right';
370
+
371
+ const cancelButton = document.createElement('button');
372
+ cancelButton.type = 'button';
373
+ cancelButton.className = 'btn2';
374
+ cancelButton.textContent = 'Cancel';
375
+ cancelButton.addEventListener('click', () => this.closeModal());
376
+
377
+ const sendButton = document.createElement('button');
378
+ sendButton.type = 'submit';
379
+ sendButton.className = 'btn terminal-keyboard-send';
380
+ sendButton.innerHTML = '<i class="fa-solid fa-paper-plane"></i> Send';
381
+
382
+ actionsRight.appendChild(cancelButton);
383
+ actionsRight.appendChild(sendButton);
384
+
385
+ actions.appendChild(actionsLeft);
386
+ actions.appendChild(actionsRight);
387
+
388
+ form.appendChild(textarea);
389
+ form.appendChild(options);
390
+ form.appendChild(status);
391
+ form.appendChild(actions);
392
+
393
+ modal.appendChild(form);
394
+
395
+ backdrop.addEventListener('click', () => this.closeModal());
396
+
397
+ document.body.appendChild(backdrop);
398
+ document.body.appendChild(modal);
399
+
400
+ this.modal = modal;
401
+ this.backdrop = backdrop;
402
+ this.textarea = textarea;
403
+ this.newlineCheckbox = newlineCheckbox;
404
+ this.statusElement = status;
405
+ }
406
+
407
+ openModal() {
408
+ if (typeof document === 'undefined') {
409
+ return;
410
+ }
411
+ if (!this.modal) {
412
+ this.ensureModalElements();
413
+ }
414
+ if (!this.modal || !this.backdrop) {
415
+ return;
416
+ }
417
+ this.setDirectTypingEnabled(false);
418
+ if (this.modalOpen) {
419
+ if (this.textarea) {
420
+ this.textarea.focus();
421
+ }
422
+ return;
423
+ }
424
+ this.modal.hidden = false;
425
+ this.backdrop.hidden = false;
426
+ this.modalOpen = true;
427
+ document.body.classList.add('terminal-keyboard-open');
428
+ document.addEventListener('keydown', this.escapeHandler, true);
429
+ this.setStatus('');
430
+ if (this.textarea) {
431
+ this.textarea.value = '';
432
+ this.textarea.focus();
433
+ }
434
+ }
435
+
436
+ closeModal(options) {
437
+ const opts = options || {};
438
+ if (!this.modalOpen) {
439
+ return;
440
+ }
441
+ this.modalOpen = false;
442
+ if (this.modal) {
443
+ this.modal.hidden = true;
444
+ }
445
+ if (this.backdrop) {
446
+ this.backdrop.hidden = true;
447
+ }
448
+ if (typeof document !== 'undefined') {
449
+ document.body.classList.remove('terminal-keyboard-open');
450
+ document.removeEventListener('keydown', this.escapeHandler, true);
451
+ }
452
+ if (this.textarea) {
453
+ this.textarea.blur();
454
+ this.textarea.value = '';
455
+ }
456
+ this.setStatus('');
457
+ if (opts.focusTrigger !== false && this.lastTrigger && typeof document !== 'undefined' && document.body && document.body.contains(this.lastTrigger)) {
458
+ try {
459
+ this.lastTrigger.focus();
460
+ } catch (_) {}
461
+ }
462
+ this.lastTrigger = null;
463
+ this.configureTapListenerForAll();
464
+ }
465
+
466
+ setStatus(message, tone) {
467
+ if (!this.statusElement) {
468
+ return;
469
+ }
470
+ if (this.statusTimer) {
471
+ clearTimeout(this.statusTimer);
472
+ this.statusTimer = null;
473
+ }
474
+ const text = typeof message === 'string' ? message : '';
475
+ this.statusElement.textContent = text;
476
+ if (tone) {
477
+ this.statusElement.dataset.tone = tone;
478
+ } else {
479
+ delete this.statusElement.dataset.tone;
480
+ }
481
+ if (text) {
482
+ this.statusElement.classList.add('visible');
483
+ this.statusTimer = setTimeout(() => {
484
+ if (this.statusElement) {
485
+ this.statusElement.classList.remove('visible');
486
+ this.statusElement.textContent = '';
487
+ delete this.statusElement.dataset.tone;
488
+ }
489
+ this.statusTimer = null;
490
+ }, tone === 'error' ? 5000 : 2000);
491
+ } else {
492
+ this.statusElement.classList.remove('visible');
493
+ }
494
+ }
495
+
496
+ focusPrimaryTerminalInput() {
497
+ const term = this.settings && typeof this.settings.getPrimaryTerminal === 'function'
498
+ ? this.settings.getPrimaryTerminal()
499
+ : null;
500
+ if (!term) {
501
+ return false;
502
+ }
503
+ const textarea = this.getTermTextarea(term);
504
+ if (textarea) {
505
+ try {
506
+ textarea.focus();
507
+ return true;
508
+ } catch (_) {}
509
+ }
510
+ if (typeof term.focus === 'function') {
511
+ try {
512
+ term.focus();
513
+ return true;
514
+ } catch (_) {}
515
+ }
516
+ return false;
517
+ }
518
+
519
+ enableDirectTypingFromModal() {
520
+ this.setDirectTypingEnabled(true);
521
+ this.closeModal({ focusTrigger: false });
522
+ this.focusPrimaryTerminalInput();
523
+ }
524
+
525
+ submitInput() {
526
+ if (!this.textarea) {
527
+ return;
528
+ }
529
+ const value = this.textarea.value || '';
530
+ const appendNewline = this.newlineCheckbox ? this.newlineCheckbox.checked : true;
531
+ if (!value && !appendNewline) {
532
+ this.setStatus('Enter text or enable newline.', 'error');
533
+ return;
534
+ }
535
+ const success = this.dispatchToTerminal(value, appendNewline);
536
+ if (!success) {
537
+ this.setStatus('Terminal is not ready yet.', 'error');
538
+ return;
539
+ }
540
+ this.textarea.value = '';
541
+ this.setStatus('Sent to terminal.', 'success');
542
+ this.closeModal();
543
+ }
544
+
545
+ dispatchToTerminal(value, appendNewline) {
546
+ const term = this.settings && typeof this.settings.getPrimaryTerminal === 'function'
547
+ ? this.settings.getPrimaryTerminal()
548
+ : null;
549
+ if (!term) {
550
+ return false;
551
+ }
552
+ let payload = typeof value === 'string' ? value : '';
553
+ if (payload) {
554
+ payload = payload.replace(/\r\n/g, '\r').replace(/\n/g, '\r');
555
+ }
556
+ const wantsNewline = Boolean(appendNewline);
557
+ if (wantsNewline) {
558
+ payload = payload.replace(/\r+$/, '');
559
+ }
560
+ const hasText = Boolean(payload);
561
+ if (!hasText && !wantsNewline) {
562
+ return false;
563
+ }
564
+ if (hasText && !this.injectIntoTerminal(term, payload)) {
565
+ return false;
566
+ }
567
+ if (wantsNewline) {
568
+ setTimeout(() => this.injectIntoTerminal(term, '\r'), 100);
569
+ }
570
+ return true;
571
+ }
572
+
573
+ injectIntoTerminal(term, payload) {
574
+ if (!term) {
575
+ return false;
576
+ }
577
+ let dispatched = false;
578
+ const coreService = term.coreService
579
+ || (term._core && (term._core.coreService || term._core._coreService))
580
+ || null;
581
+ if (coreService && typeof coreService.triggerDataEvent === 'function') {
582
+ coreService.triggerDataEvent(payload, true);
583
+ dispatched = true;
584
+ } else if (term._core && term._core._onData && typeof term._core._onData.fire === 'function') {
585
+ term._core._onData.fire(payload);
586
+ dispatched = true;
587
+ } else if (term._onData && typeof term._onData.fire === 'function') {
588
+ term._onData.fire(payload);
589
+ dispatched = true;
590
+ }
591
+ if (!dispatched) {
592
+ return false;
593
+ }
594
+ if (typeof term.focus === 'function') {
595
+ term.focus();
596
+ }
597
+ return true;
598
+ }
599
+
600
+ configureTapListener(term) {
601
+ if (!term) {
602
+ return;
603
+ }
604
+ const node = term.element || term._core && term._core._terminalDiv || term._core && term._core.element || null;
605
+ if (!node) {
606
+ return;
607
+ }
608
+ if (this.directTypingEnabled) {
609
+ this.removeTapListener(term);
610
+ return;
611
+ }
612
+ if (this.tapTrackers.has(term)) {
613
+ return;
614
+ }
615
+ const tracker = {
616
+ lastTime: 0,
617
+ lastX: 0,
618
+ lastY: 0,
619
+ handler: null,
620
+ eventName: this.supportsPointer ? 'pointerdown' : 'touchstart'
621
+ };
622
+ const handlePointerDown = (event) => {
623
+ if (!event) {
624
+ return;
625
+ }
626
+ if (this.supportsPointer) {
627
+ const pointerType = event.pointerType;
628
+ if (pointerType && pointerType !== 'touch' && pointerType !== 'pen') {
629
+ return;
630
+ }
631
+ } else if (event.touches && event.touches.length !== 1) {
632
+ return;
633
+ }
634
+ const pointSource = this.supportsPointer ? event : (event.touches && event.touches[0]);
635
+ const pointX = pointSource && typeof pointSource.clientX === 'number' ? pointSource.clientX : 0;
636
+ const pointY = pointSource && typeof pointSource.clientY === 'number' ? pointSource.clientY : 0;
637
+ const now = Date.now();
638
+ const delta = now - tracker.lastTime;
639
+ const distance = Math.hypot(pointX - tracker.lastX, pointY - tracker.lastY);
640
+ if (delta < 320 && distance < 40) {
641
+ event.preventDefault();
642
+ event.stopPropagation();
643
+ this.openModalFromGesture();
644
+ }
645
+ tracker.lastTime = now;
646
+ tracker.lastX = pointX;
647
+ tracker.lastY = pointY;
648
+ };
649
+ node.addEventListener(tracker.eventName, handlePointerDown, { passive: false });
650
+ tracker.handler = handlePointerDown;
651
+ tracker.node = node;
652
+ this.tapTrackers.set(term, tracker);
653
+ }
654
+
655
+ configureTapListenerForAll() {
656
+ this.termRecords.forEach((record) => {
657
+ if (record && record.term) {
658
+ this.configureTapListener(record.term);
659
+ }
660
+ });
661
+ }
662
+
663
+ removeTapListener(term) {
664
+ if (!term) {
665
+ return;
666
+ }
667
+ const tracker = this.tapTrackers.get(term);
668
+ if (!tracker) {
669
+ return;
670
+ }
671
+ if (tracker.node && tracker.handler) {
672
+ const eventName = tracker.eventName || (this.supportsPointer ? 'pointerdown' : 'touchstart');
673
+ tracker.node.removeEventListener(eventName, tracker.handler, { passive: false });
674
+ }
675
+ this.tapTrackers.delete(term);
676
+ }
677
+
678
+ openModalFromGesture() {
679
+ this.lastTrigger = null;
680
+ this.openModal();
681
+ }
682
+ }
683
+
67
684
  class TerminalSettings {
68
685
  constructor() {
69
686
  this.preferences = this.loadPreferences();
70
687
  this.terminals = new Set();
71
688
  this.menus = new Set();
72
689
  this.styleElement = null;
690
+ this.mobileInput = typeof TerminalMobileInput === 'function'
691
+ ? new TerminalMobileInput(this)
692
+ : null;
693
+ this.forceResizeButtons = new WeakMap();
694
+ this.forceResizeHandler = null;
73
695
  this.currentFontFamily = typeof this.preferences.fontFamily === 'string' ? this.preferences.fontFamily.trim() : '';
74
696
  if (typeof document !== 'undefined') {
75
697
  const ready = document.readyState;
@@ -191,10 +813,16 @@
191
813
  }
192
814
  this.terminals.add(term);
193
815
  this.applyPreferences(term);
816
+ if (this.mobileInput) {
817
+ this.mobileInput.registerTerminal(term);
818
+ }
194
819
  if (!term._pinokioPatchedDispose && typeof term.dispose === 'function') {
195
820
  const dispose = term.dispose.bind(term);
196
821
  term.dispose = (...args) => {
197
822
  this.terminals.delete(term);
823
+ if (this.mobileInput) {
824
+ this.mobileInput.unregisterTerminal(term);
825
+ }
198
826
  return dispose(...args);
199
827
  };
200
828
  term._pinokioPatchedDispose = true;
@@ -396,10 +1024,6 @@
396
1024
  }
397
1025
  const family = typeof this.preferences.fontFamily === 'string' ? this.preferences.fontFamily.trim() : '';
398
1026
  const size = isFiniteNumber(this.preferences.fontSize) ? this.preferences.fontSize : null;
399
- if (!family && !size) {
400
- this.removeStyleElement();
401
- return;
402
- }
403
1027
  const style = this.ensureStyleElement();
404
1028
  if (!style) {
405
1029
  return;
@@ -413,14 +1037,17 @@
413
1037
  '.xterm .xterm-cursor-layer',
414
1038
  '.xterm .xterm-char-measure-element'
415
1039
  ];
416
- const rules = [];
1040
+ const declarations = [
1041
+ 'font-variant-ligatures: none !important',
1042
+ 'font-feature-settings: "liga" 0, "clig" 0, "calt" 0, "rlig" 0 !important'
1043
+ ];
417
1044
  if (family) {
418
- rules.push(`${selectors.join(', ')} { font-family: ${family} !important; }`);
1045
+ declarations.push(`font-family: ${family} !important`);
419
1046
  }
420
1047
  if (size) {
421
- rules.push(`${selectors.join(', ')} { font-size: ${size}px !important; }`);
1048
+ declarations.push(`font-size: ${size}px !important`);
422
1049
  }
423
- style.textContent = rules.join('\n');
1050
+ style.textContent = `${selectors.join(', ')} { ${declarations.join('; ')}; }`;
424
1051
  }
425
1052
 
426
1053
  sanitizeTheme(raw, allowUnknown) {
@@ -592,6 +1219,82 @@
592
1219
 
593
1220
  warnNonMonospace() {}
594
1221
 
1222
+ ensureRunnerUtilities(runner) {
1223
+ if (typeof document === 'undefined' || !runner) {
1224
+ return null;
1225
+ }
1226
+ let container = runner.querySelector('.terminal-runner-utilities');
1227
+ if (container) {
1228
+ return container;
1229
+ }
1230
+ container = document.createElement('div');
1231
+ container.className = 'terminal-runner-utilities';
1232
+ container.setAttribute('role', 'group');
1233
+ container.setAttribute('aria-label', 'Terminal controls');
1234
+ runner.appendChild(container);
1235
+ return container;
1236
+ }
1237
+
1238
+ setForceResizeHandler(handler) {
1239
+ if (typeof handler === 'function') {
1240
+ this.forceResizeHandler = handler;
1241
+ } else {
1242
+ this.forceResizeHandler = null;
1243
+ }
1244
+ }
1245
+
1246
+ requestForceResize(context) {
1247
+ if (typeof this.forceResizeHandler === 'function') {
1248
+ try {
1249
+ this.forceResizeHandler(context || null);
1250
+ return true;
1251
+ } catch (error) {
1252
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
1253
+ console.warn('Pinokio: force resize handler failed', error);
1254
+ }
1255
+ }
1256
+ }
1257
+ return false;
1258
+ }
1259
+
1260
+ attachForceResizeButton(runner, host) {
1261
+ if (typeof document === 'undefined' || !runner || !host || this.forceResizeButtons.has(runner)) {
1262
+ return;
1263
+ }
1264
+ const button = document.createElement('button');
1265
+ button.type = 'button';
1266
+ button.className = 'btn terminal-resize-button';
1267
+ button.innerHTML = '<i class="fa-solid fa-expand"></i> Resize';
1268
+ button.title = 'Resize to this window';
1269
+ button.addEventListener('click', (event) => {
1270
+ if (event) {
1271
+ event.preventDefault();
1272
+ event.stopPropagation();
1273
+ }
1274
+ const handled = this.requestForceResize({ runner });
1275
+ if (!handled && typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
1276
+ try {
1277
+ if (typeof window.CustomEvent === 'function') {
1278
+ window.dispatchEvent(new window.CustomEvent('pinokio-terminal-force-resize', {
1279
+ detail: { runner }
1280
+ }));
1281
+ } else if (typeof document !== 'undefined' && typeof document.createEvent === 'function') {
1282
+ const legacyEvent = document.createEvent('CustomEvent');
1283
+ legacyEvent.initCustomEvent('pinokio-terminal-force-resize', true, true, { runner });
1284
+ window.dispatchEvent(legacyEvent);
1285
+ }
1286
+ } catch (_) {}
1287
+ }
1288
+ });
1289
+ const configBlock = host.querySelector('.terminal-config');
1290
+ if (configBlock && configBlock.parentNode === host) {
1291
+ host.insertBefore(button, configBlock);
1292
+ } else {
1293
+ host.appendChild(button);
1294
+ }
1295
+ this.forceResizeButtons.set(runner, { button });
1296
+ }
1297
+
595
1298
  initRunnerMenus() {
596
1299
  if (typeof document === 'undefined') {
597
1300
  return;
@@ -609,10 +1312,18 @@
609
1312
  return;
610
1313
  }
611
1314
  runner.dataset.terminalConfigAttached = 'true';
1315
+ const utilities = this.ensureRunnerUtilities(runner);
612
1316
  const menu = this.createMenu(runner);
1317
+ if (utilities && menu && menu.wrapper) {
1318
+ utilities.appendChild(menu.wrapper);
1319
+ }
613
1320
  if (menu) {
614
1321
  this.menus.add(menu);
615
1322
  }
1323
+ if (this.mobileInput) {
1324
+ this.mobileInput.attachKeyboardButton(runner, utilities);
1325
+ }
1326
+ this.attachForceResizeButton(runner, utilities);
616
1327
  });
617
1328
  }
618
1329
 
@@ -626,7 +1337,7 @@
626
1337
  const button = document.createElement('button');
627
1338
  button.type = 'button';
628
1339
  button.className = 'btn terminal-config-button';
629
- button.innerHTML = '<i class="fa-solid fa-sliders"></i> Configure';
1340
+ button.innerHTML = '<i class="fa-solid fa-sliders"></i>';
630
1341
  button.setAttribute('aria-haspopup', 'true');
631
1342
  button.setAttribute('aria-expanded', 'false');
632
1343
 
@@ -1119,6 +1830,9 @@
1119
1830
 
1120
1831
  const settings = new TerminalSettings();
1121
1832
  window.PinokioTerminalSettings = settings;
1833
+ if (typeof window !== 'undefined') {
1834
+ window.PinokioTerminalKeyboard = settings && settings.mobileInput ? settings.mobileInput : null;
1835
+ }
1122
1836
 
1123
1837
  if (typeof document !== 'undefined') {
1124
1838
  const readyState = document.readyState;