termbeam 1.3.0 → 1.5.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.
@@ -63,6 +63,216 @@
63
63
  --key-special-bg: #adb5bd;
64
64
  --overlay-bg: rgba(0, 0, 0, 0.5);
65
65
  }
66
+ [data-theme='monokai'] {
67
+ --bg: #272822;
68
+ --surface: #1e1f1c;
69
+ --border: #49483e;
70
+ --border-subtle: #5c5c4f;
71
+ --text: #f8f8f2;
72
+ --text-secondary: #a59f85;
73
+ --text-dim: #75715e;
74
+ --text-muted: #5a5854;
75
+ --accent: #a6e22e;
76
+ --accent-hover: #b8f53c;
77
+ --accent-active: #8acc16;
78
+ --danger: #f92672;
79
+ --danger-hover: #e0155d;
80
+ --success: #a6e22e;
81
+ --key-bg: #49483e;
82
+ --key-border: #5c5c4f;
83
+ --key-shadow: rgba(0, 0, 0, 0.4);
84
+ --key-special-bg: #3e3d32;
85
+ --overlay-bg: rgba(0, 0, 0, 0.75);
86
+ }
87
+ [data-theme='solarized-dark'] {
88
+ --bg: #002b36;
89
+ --surface: #073642;
90
+ --border: #586e75;
91
+ --border-subtle: #657b83;
92
+ --text: #839496;
93
+ --text-secondary: #657b83;
94
+ --text-dim: #586e75;
95
+ --text-muted: #4a5a62;
96
+ --accent: #268bd2;
97
+ --accent-hover: #379ce3;
98
+ --accent-active: #1a7abf;
99
+ --danger: #dc322f;
100
+ --danger-hover: #c8221f;
101
+ --success: #859900;
102
+ --key-bg: #073642;
103
+ --key-border: #586e75;
104
+ --key-shadow: rgba(0, 0, 0, 0.3);
105
+ --key-special-bg: #002b36;
106
+ --overlay-bg: rgba(0, 0, 0, 0.75);
107
+ }
108
+ [data-theme='solarized-light'] {
109
+ --bg: #fdf6e3;
110
+ --surface: #eee8d5;
111
+ --border: #93a1a1;
112
+ --border-subtle: #839496;
113
+ --text: #657b83;
114
+ --text-secondary: #93a1a1;
115
+ --text-dim: #a0a0a0;
116
+ --text-muted: #b0b0b0;
117
+ --accent: #268bd2;
118
+ --accent-hover: #379ce3;
119
+ --accent-active: #1a7abf;
120
+ --danger: #dc322f;
121
+ --danger-hover: #c8221f;
122
+ --success: #859900;
123
+ --key-bg: #ffffff;
124
+ --key-border: #b5b5b5;
125
+ --key-shadow: rgba(0, 0, 0, 0.12);
126
+ --key-special-bg: #adb5bd;
127
+ --overlay-bg: rgba(0, 0, 0, 0.4);
128
+ }
129
+ [data-theme='nord'] {
130
+ --bg: #2e3440;
131
+ --surface: #3b4252;
132
+ --border: #434c5e;
133
+ --border-subtle: #4c566a;
134
+ --text: #d8dee9;
135
+ --text-secondary: #b0bac9;
136
+ --text-dim: #7b88a1;
137
+ --text-muted: #5c6a85;
138
+ --accent: #88c0d0;
139
+ --accent-hover: #9fd4e4;
140
+ --accent-active: #6aafbf;
141
+ --danger: #bf616a;
142
+ --danger-hover: #a84d57;
143
+ --success: #a3be8c;
144
+ --key-bg: #434c5e;
145
+ --key-border: #4c566a;
146
+ --key-shadow: rgba(0, 0, 0, 0.3);
147
+ --key-special-bg: #3b4252;
148
+ --overlay-bg: rgba(0, 0, 0, 0.75);
149
+ }
150
+ [data-theme='dracula'] {
151
+ --bg: #282a36;
152
+ --surface: #343746;
153
+ --border: #44475a;
154
+ --border-subtle: #525568;
155
+ --text: #f8f8f2;
156
+ --text-secondary: #c1c4d2;
157
+ --text-dim: #8e92a4;
158
+ --text-muted: #6272a4;
159
+ --accent: #bd93f9;
160
+ --accent-hover: #d0b0ff;
161
+ --accent-active: #a77de7;
162
+ --danger: #ff5555;
163
+ --danger-hover: #e03d3d;
164
+ --success: #50fa7b;
165
+ --key-bg: #44475a;
166
+ --key-border: #525568;
167
+ --key-shadow: rgba(0, 0, 0, 0.4);
168
+ --key-special-bg: #343746;
169
+ --overlay-bg: rgba(0, 0, 0, 0.75);
170
+ }
171
+ [data-theme='github-dark'] {
172
+ --bg: #0d1117;
173
+ --surface: #161b22;
174
+ --border: #30363d;
175
+ --border-subtle: #3d444d;
176
+ --text: #c9d1d9;
177
+ --text-secondary: #8b949e;
178
+ --text-dim: #6e7681;
179
+ --text-muted: #484f58;
180
+ --accent: #58a6ff;
181
+ --accent-hover: #79b8ff;
182
+ --accent-active: #388bfd;
183
+ --danger: #f85149;
184
+ --danger-hover: #da3633;
185
+ --success: #3fb950;
186
+ --key-bg: #161b22;
187
+ --key-border: #30363d;
188
+ --key-shadow: rgba(0, 0, 0, 0.4);
189
+ --key-special-bg: #0d1117;
190
+ --overlay-bg: rgba(0, 0, 0, 0.75);
191
+ }
192
+ [data-theme='one-dark'] {
193
+ --bg: #282c34;
194
+ --surface: #21252b;
195
+ --border: #3e4452;
196
+ --border-subtle: #4b5263;
197
+ --text: #abb2bf;
198
+ --text-secondary: #7f848e;
199
+ --text-dim: #5c6370;
200
+ --text-muted: #4b5263;
201
+ --accent: #61afef;
202
+ --accent-hover: #7dc0ff;
203
+ --accent-active: #4d9ede;
204
+ --danger: #e06c75;
205
+ --danger-hover: #c95c67;
206
+ --success: #98c379;
207
+ --key-bg: #3e4452;
208
+ --key-border: #4b5263;
209
+ --key-shadow: rgba(0, 0, 0, 0.3);
210
+ --key-special-bg: #21252b;
211
+ --overlay-bg: rgba(0, 0, 0, 0.75);
212
+ }
213
+ [data-theme='catppuccin'] {
214
+ --bg: #1e1e2e;
215
+ --surface: #313244;
216
+ --border: #45475a;
217
+ --border-subtle: #585b70;
218
+ --text: #cdd6f4;
219
+ --text-secondary: #a6adc8;
220
+ --text-dim: #7f849c;
221
+ --text-muted: #585b70;
222
+ --accent: #89b4fa;
223
+ --accent-hover: #b4d0ff;
224
+ --accent-active: #5c9de3;
225
+ --danger: #f38ba8;
226
+ --danger-hover: #eb7c9d;
227
+ --success: #a6e3a1;
228
+ --key-bg: #45475a;
229
+ --key-border: #585b70;
230
+ --key-shadow: rgba(0, 0, 0, 0.3);
231
+ --key-special-bg: #313244;
232
+ --overlay-bg: rgba(0, 0, 0, 0.75);
233
+ }
234
+ [data-theme='gruvbox'] {
235
+ --bg: #282828;
236
+ --surface: #3c3836;
237
+ --border: #504945;
238
+ --border-subtle: #665c54;
239
+ --text: #ebdbb2;
240
+ --text-secondary: #d5c4a1;
241
+ --text-dim: #a89984;
242
+ --text-muted: #7c6f64;
243
+ --accent: #83a598;
244
+ --accent-hover: #9dbfb4;
245
+ --accent-active: #6a8f8a;
246
+ --danger: #fb4934;
247
+ --danger-hover: #e33826;
248
+ --success: #b8bb26;
249
+ --key-bg: #504945;
250
+ --key-border: #665c54;
251
+ --key-shadow: rgba(0, 0, 0, 0.4);
252
+ --key-special-bg: #3c3836;
253
+ --overlay-bg: rgba(0, 0, 0, 0.75);
254
+ }
255
+ [data-theme='night-owl'] {
256
+ --bg: #011627;
257
+ --surface: #0d2a45;
258
+ --border: #1d3b53;
259
+ --border-subtle: #264863;
260
+ --text: #d6deeb;
261
+ --text-secondary: #8badc1;
262
+ --text-dim: #5f7e97;
263
+ --text-muted: #3f5f7d;
264
+ --accent: #7fdbca;
265
+ --accent-hover: #9ff0e0;
266
+ --accent-active: #62c5b5;
267
+ --danger: #ef5350;
268
+ --danger-hover: #d83130;
269
+ --success: #addb67;
270
+ --key-bg: #1d3b53;
271
+ --key-border: #264863;
272
+ --key-shadow: rgba(0, 0, 0, 0.4);
273
+ --key-special-bg: #0d2a45;
274
+ --overlay-bg: rgba(0, 0, 0, 0.75);
275
+ }
66
276
  @font-face {
67
277
  font-family: 'NerdFont';
68
278
  src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
@@ -248,6 +458,23 @@
248
458
  .tab-close:hover {
249
459
  color: var(--danger);
250
460
  }
461
+ .tab-unread {
462
+ width: 7px;
463
+ height: 7px;
464
+ border-radius: 50%;
465
+ background: var(--accent);
466
+ flex-shrink: 0;
467
+ animation: tab-pulse 1.5s ease-in-out infinite;
468
+ }
469
+ @keyframes tab-pulse {
470
+ 0%,
471
+ 100% {
472
+ opacity: 1;
473
+ }
474
+ 50% {
475
+ opacity: 0.4;
476
+ }
477
+ }
251
478
  /* ===== Tab Preview ===== */
252
479
  #tab-preview {
253
480
  display: none;
@@ -382,6 +609,52 @@
382
609
  font-size: 14px;
383
610
  font-weight: 600;
384
611
  }
612
+ .theme-wrap {
613
+ position: relative;
614
+ display: none;
615
+ align-items: center;
616
+ }
617
+ .theme-picker {
618
+ display: none;
619
+ position: absolute;
620
+ top: calc(100% + 4px);
621
+ right: 0;
622
+ background: var(--surface);
623
+ border: 1px solid var(--border);
624
+ border-radius: 8px;
625
+ min-width: 160px;
626
+ padding: 4px 0;
627
+ z-index: 200;
628
+ box-shadow: 0 4px 12px var(--shadow);
629
+ }
630
+ .theme-picker.open {
631
+ display: block;
632
+ }
633
+ .theme-option {
634
+ display: flex;
635
+ align-items: center;
636
+ gap: 8px;
637
+ padding: 7px 12px;
638
+ cursor: pointer;
639
+ font-size: 13px;
640
+ color: var(--text);
641
+ transition: background 0.1s;
642
+ white-space: nowrap;
643
+ }
644
+ .theme-option:hover {
645
+ background: var(--border);
646
+ }
647
+ .theme-option.active {
648
+ color: var(--accent);
649
+ }
650
+ .theme-swatch {
651
+ width: 14px;
652
+ height: 14px;
653
+ border-radius: 50%;
654
+ display: inline-block;
655
+ flex-shrink: 0;
656
+ border: 1px solid rgba(128, 128, 128, 0.3);
657
+ }
385
658
  #stop-btn {
386
659
  background: none;
387
660
  border: none;
@@ -389,7 +662,7 @@
389
662
  height: 30px;
390
663
  border-radius: 8px;
391
664
  cursor: pointer;
392
- display: flex;
665
+ display: none;
393
666
  align-items: center;
394
667
  justify-content: center;
395
668
  gap: 4px;
@@ -412,6 +685,75 @@
412
685
  transform: scale(0.9);
413
686
  }
414
687
 
688
+ /* ===== Notification Toggle ===== */
689
+ .notify-btn.active {
690
+ color: var(--accent) !important;
691
+ }
692
+
693
+ /* ===== Search Bar ===== */
694
+ .search-bar {
695
+ display: none;
696
+ position: absolute;
697
+ top: 4px;
698
+ right: 12px;
699
+ z-index: 100;
700
+ background: var(--surface);
701
+ border: 1px solid var(--border);
702
+ border-radius: 8px;
703
+ padding: 4px 6px;
704
+ gap: 4px;
705
+ align-items: center;
706
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
707
+ font-size: 13px;
708
+ color: var(--text);
709
+ }
710
+ .search-bar.visible {
711
+ display: flex;
712
+ }
713
+ .search-bar input {
714
+ background: var(--bg);
715
+ border: 1px solid var(--border);
716
+ border-radius: 4px;
717
+ color: var(--text);
718
+ padding: 3px 6px;
719
+ font-size: 13px;
720
+ font-family: inherit;
721
+ width: 160px;
722
+ outline: none;
723
+ }
724
+ .search-bar input:focus {
725
+ border-color: var(--accent);
726
+ }
727
+ .search-bar .search-count {
728
+ color: var(--text-secondary);
729
+ font-size: 11px;
730
+ min-width: 40px;
731
+ text-align: center;
732
+ white-space: nowrap;
733
+ }
734
+ .search-bar button {
735
+ background: none;
736
+ border: 1px solid transparent;
737
+ color: var(--text-dim);
738
+ width: 24px;
739
+ height: 24px;
740
+ border-radius: 4px;
741
+ cursor: pointer;
742
+ display: flex;
743
+ align-items: center;
744
+ justify-content: center;
745
+ font-size: 13px;
746
+ padding: 0;
747
+ }
748
+ .search-bar button:hover {
749
+ background: var(--border);
750
+ color: var(--text);
751
+ }
752
+ .search-bar button.active {
753
+ color: var(--accent);
754
+ border-color: var(--accent);
755
+ }
756
+
415
757
  /* ===== Terminals Wrapper ===== */
416
758
  #terminals-wrapper {
417
759
  position: absolute;
@@ -467,6 +809,9 @@
467
809
  [data-theme='light'] #key-bar {
468
810
  background: #d1d3d9;
469
811
  }
812
+ [data-theme='solarized-light'] #key-bar {
813
+ background: #d1d3d9;
814
+ }
470
815
  .key-row {
471
816
  display: flex;
472
817
  align-items: center;
@@ -502,6 +847,10 @@
502
847
  color: #000;
503
848
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
504
849
  }
850
+ [data-theme='solarized-light'] .key-btn {
851
+ color: #000;
852
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
853
+ }
505
854
  .key-btn:active {
506
855
  background: #6e6e72;
507
856
  transform: scale(0.95);
@@ -510,6 +859,9 @@
510
859
  [data-theme='light'] .key-btn:active {
511
860
  background: #c8c8cc;
512
861
  }
862
+ [data-theme='solarized-light'] .key-btn:active {
863
+ background: #c8c8cc;
864
+ }
513
865
  .key-btn.flash {
514
866
  background: #fff !important;
515
867
  color: #000 !important;
@@ -519,6 +871,10 @@
519
871
  background: #333 !important;
520
872
  color: #fff !important;
521
873
  }
874
+ [data-theme='solarized-light'] .key-btn.flash {
875
+ background: #333 !important;
876
+ color: #fff !important;
877
+ }
522
878
  .key-btn.modifier,
523
879
  .key-btn.special {
524
880
  background: var(--key-special-bg);
@@ -530,6 +886,11 @@
530
886
  background: var(--key-special-bg);
531
887
  color: #000;
532
888
  }
889
+ [data-theme='solarized-light'] .key-btn.modifier,
890
+ [data-theme='solarized-light'] .key-btn.special {
891
+ background: var(--key-special-bg);
892
+ color: #000;
893
+ }
533
894
  .key-btn.modifier.active {
534
895
  background: var(--accent);
535
896
  color: #fff;
@@ -563,6 +924,10 @@
563
924
  background: #fee2e2;
564
925
  color: #dc2626;
565
926
  }
927
+ [data-theme='solarized-light'] .key-btn.key-danger {
928
+ background: #fee2e2;
929
+ color: #dc2626;
930
+ }
566
931
  .key-btn.key-danger:active {
567
932
  background: var(--danger);
568
933
  color: #fff;
@@ -1372,17 +1737,12 @@
1372
1737
  #tab-list {
1373
1738
  display: none;
1374
1739
  }
1375
- #split-toggle {
1376
- display: none;
1377
- }
1378
- #version-text {
1379
- display: none;
1380
- }
1740
+
1381
1741
  #back-btn {
1382
1742
  display: none;
1383
1743
  }
1384
- #theme-toggle {
1385
- display: flex;
1744
+ #theme-wrap {
1745
+ display: none;
1386
1746
  }
1387
1747
  #stop-btn {
1388
1748
  padding: 0 8px;
@@ -1399,6 +1759,195 @@
1399
1759
  width: auto;
1400
1760
  }
1401
1761
  }
1762
+
1763
+ /* ===== Command Palette / Tool Panel ===== */
1764
+ .palette-backdrop {
1765
+ position: fixed;
1766
+ inset: 0;
1767
+ background: rgba(0, 0, 0, 0.4);
1768
+ z-index: 250;
1769
+ opacity: 0;
1770
+ pointer-events: none;
1771
+ transition: opacity 0.3s;
1772
+ }
1773
+ .palette-backdrop.open {
1774
+ opacity: 1;
1775
+ pointer-events: auto;
1776
+ }
1777
+ .palette-panel {
1778
+ position: fixed;
1779
+ top: 0;
1780
+ right: 0;
1781
+ width: 280px;
1782
+ max-width: 85vw;
1783
+ height: 100%;
1784
+ background: var(--surface);
1785
+ border-left: 1px solid var(--border);
1786
+ z-index: 260;
1787
+ transform: translateX(100%);
1788
+ transition: transform 0.3s ease;
1789
+ display: flex;
1790
+ flex-direction: column;
1791
+ overflow-y: auto;
1792
+ -webkit-overflow-scrolling: touch;
1793
+ }
1794
+ .palette-panel.open {
1795
+ transform: translateX(0);
1796
+ }
1797
+ .palette-header {
1798
+ display: flex;
1799
+ align-items: center;
1800
+ justify-content: space-between;
1801
+ padding: 14px 16px;
1802
+ border-bottom: 1px solid var(--border);
1803
+ font-weight: 600;
1804
+ font-size: 15px;
1805
+ color: var(--text);
1806
+ }
1807
+ .palette-close {
1808
+ background: none;
1809
+ border: none;
1810
+ color: var(--text-secondary);
1811
+ font-size: 18px;
1812
+ cursor: pointer;
1813
+ padding: 4px 8px;
1814
+ border-radius: 6px;
1815
+ }
1816
+ .palette-close:hover {
1817
+ background: var(--hover-bg, rgba(255, 255, 255, 0.08));
1818
+ color: var(--text);
1819
+ }
1820
+ .palette-body {
1821
+ padding: 8px 0;
1822
+ flex: 1;
1823
+ }
1824
+ .palette-category {
1825
+ padding: 8px 16px 4px;
1826
+ font-size: 11px;
1827
+ font-weight: 600;
1828
+ text-transform: uppercase;
1829
+ letter-spacing: 0.5px;
1830
+ color: var(--text-muted, var(--text-secondary));
1831
+ }
1832
+ .palette-action {
1833
+ display: flex;
1834
+ align-items: center;
1835
+ gap: 10px;
1836
+ width: 100%;
1837
+ padding: 10px 16px;
1838
+ background: none;
1839
+ border: none;
1840
+ color: var(--text);
1841
+ font-size: 13px;
1842
+ cursor: pointer;
1843
+ text-align: left;
1844
+ transition: background 0.15s;
1845
+ }
1846
+ .palette-action:hover {
1847
+ background: rgba(255, 255, 255, 0.06);
1848
+ }
1849
+ .palette-action:active {
1850
+ background: rgba(255, 255, 255, 0.1);
1851
+ }
1852
+ [data-theme='light'] .palette-action:hover,
1853
+ [data-theme='solarized-light'] .palette-action:hover {
1854
+ background: rgba(0, 0, 0, 0.06);
1855
+ }
1856
+ [data-theme='light'] .palette-action:active,
1857
+ [data-theme='solarized-light'] .palette-action:active {
1858
+ background: rgba(0, 0, 0, 0.1);
1859
+ }
1860
+ .palette-action-icon {
1861
+ width: 20px;
1862
+ height: 20px;
1863
+ display: flex;
1864
+ align-items: center;
1865
+ justify-content: center;
1866
+ flex-shrink: 0;
1867
+ color: var(--text-secondary);
1868
+ }
1869
+ .palette-action-icon svg {
1870
+ width: 16px;
1871
+ }
1872
+ /* ===== Theme Sub-Panel ===== */
1873
+ .theme-subpanel {
1874
+ display: none;
1875
+ position: fixed;
1876
+ top: 50%;
1877
+ left: 50%;
1878
+ transform: translate(-50%, -50%);
1879
+ width: 240px;
1880
+ max-height: 70vh;
1881
+ background: var(--surface);
1882
+ border: 1px solid var(--border);
1883
+ border-radius: 12px;
1884
+ z-index: 270;
1885
+ overflow-y: auto;
1886
+ -webkit-overflow-scrolling: touch;
1887
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
1888
+ }
1889
+ .theme-subpanel.open {
1890
+ display: block;
1891
+ }
1892
+ .theme-subpanel-header {
1893
+ display: flex;
1894
+ align-items: center;
1895
+ justify-content: space-between;
1896
+ padding: 12px 14px;
1897
+ border-bottom: 1px solid var(--border);
1898
+ font-weight: 600;
1899
+ font-size: 13px;
1900
+ color: var(--text);
1901
+ }
1902
+ .theme-subpanel-close {
1903
+ background: none;
1904
+ border: none;
1905
+ color: var(--text-secondary);
1906
+ font-size: 16px;
1907
+ cursor: pointer;
1908
+ padding: 2px 6px;
1909
+ border-radius: 6px;
1910
+ }
1911
+ .theme-subpanel-close:hover {
1912
+ background: var(--hover-bg, rgba(255, 255, 255, 0.08));
1913
+ color: var(--text);
1914
+ }
1915
+ .theme-subpanel-list {
1916
+ padding: 6px 0;
1917
+ }
1918
+ .theme-subpanel-item {
1919
+ display: flex;
1920
+ align-items: center;
1921
+ gap: 10px;
1922
+ width: 100%;
1923
+ padding: 9px 14px;
1924
+ background: none;
1925
+ border: none;
1926
+ color: var(--text);
1927
+ font-size: 13px;
1928
+ cursor: pointer;
1929
+ text-align: left;
1930
+ transition: background 0.15s;
1931
+ }
1932
+ .theme-subpanel-item:hover {
1933
+ background: rgba(255, 255, 255, 0.06);
1934
+ }
1935
+ [data-theme='light'] .theme-subpanel-item:hover,
1936
+ [data-theme='solarized-light'] .theme-subpanel-item:hover {
1937
+ background: rgba(0, 0, 0, 0.06);
1938
+ }
1939
+ .theme-subpanel-item.active {
1940
+ color: var(--accent);
1941
+ }
1942
+ .theme-subpanel-swatch {
1943
+ width: 14px;
1944
+ height: 14px;
1945
+ border-radius: 50%;
1946
+ flex-shrink: 0;
1947
+ border: 1px solid rgba(128, 128, 128, 0.3);
1948
+ }
1949
+ height: 16px;
1950
+ }
1402
1951
  </style>
1403
1952
  </head>
1404
1953
  <body>
@@ -1496,7 +2045,6 @@
1496
2045
  </div>
1497
2046
  <div id="tab-list"></div>
1498
2047
  <div class="right">
1499
- <span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
1500
2048
  <button class="tab-bar-btn" id="tab-new-btn" title="New session">
1501
2049
  <svg
1502
2050
  width="14"
@@ -1511,34 +2059,67 @@
1511
2059
  <line x1="5" y1="12" x2="19" y2="12" /></svg
1512
2060
  ><span class="new-btn-label">New</span>
1513
2061
  </button>
1514
- <button class="tab-bar-btn" id="split-toggle" title="Split view">
1515
- <svg
1516
- width="16"
1517
- height="16"
1518
- viewBox="0 0 24 24"
1519
- fill="none"
1520
- stroke="currentColor"
1521
- stroke-width="2"
1522
- stroke-linecap="round"
1523
- stroke-linejoin="round"
1524
- >
1525
- <rect x="3" y="3" width="18" height="18" rx="2" />
1526
- <line x1="12" y1="3" x2="12" y2="21" />
1527
- </svg>
1528
- </button>
1529
- <div class="bar-group">
1530
- <button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
1531
- <button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
2062
+ <div class="theme-wrap" id="theme-wrap">
2063
+ <button class="bar-btn" id="theme-toggle" title="Switch theme">
2064
+ <svg
2065
+ width="16"
2066
+ height="16"
2067
+ viewBox="0 0 24 24"
2068
+ fill="none"
2069
+ stroke="currentColor"
2070
+ stroke-width="2"
2071
+ stroke-linecap="round"
2072
+ stroke-linejoin="round"
2073
+ >
2074
+ <circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
2075
+ <circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
2076
+ <circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
2077
+ <circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
2078
+ <path
2079
+ d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
2080
+ />
2081
+ </svg>
2082
+ </button>
2083
+ <div class="theme-picker" id="theme-picker">
2084
+ <div class="theme-option" data-theme-option="dark">
2085
+ <span class="theme-swatch" style="background: #1e1e1e"></span>Dark
2086
+ </div>
2087
+ <div class="theme-option" data-theme-option="light">
2088
+ <span class="theme-swatch" style="background: #ffffff"></span>Light
2089
+ </div>
2090
+ <div class="theme-option" data-theme-option="monokai">
2091
+ <span class="theme-swatch" style="background: #272822"></span>Monokai
2092
+ </div>
2093
+ <div class="theme-option" data-theme-option="solarized-dark">
2094
+ <span class="theme-swatch" style="background: #002b36"></span>Solarized Dark
2095
+ </div>
2096
+ <div class="theme-option" data-theme-option="solarized-light">
2097
+ <span class="theme-swatch" style="background: #fdf6e3"></span>Solarized Light
2098
+ </div>
2099
+ <div class="theme-option" data-theme-option="nord">
2100
+ <span class="theme-swatch" style="background: #2e3440"></span>Nord
2101
+ </div>
2102
+ <div class="theme-option" data-theme-option="dracula">
2103
+ <span class="theme-swatch" style="background: #282a36"></span>Dracula
2104
+ </div>
2105
+ <div class="theme-option" data-theme-option="github-dark">
2106
+ <span class="theme-swatch" style="background: #0d1117"></span>GitHub Dark
2107
+ </div>
2108
+ <div class="theme-option" data-theme-option="one-dark">
2109
+ <span class="theme-swatch" style="background: #282c34"></span>One Dark
2110
+ </div>
2111
+ <div class="theme-option" data-theme-option="catppuccin">
2112
+ <span class="theme-swatch" style="background: #1e1e2e"></span>Catppuccin
2113
+ </div>
2114
+ <div class="theme-option" data-theme-option="gruvbox">
2115
+ <span class="theme-swatch" style="background: #282828"></span>Gruvbox
2116
+ </div>
2117
+ <div class="theme-option" data-theme-option="night-owl">
2118
+ <span class="theme-swatch" style="background: #011627"></span>Night Owl
2119
+ </div>
2120
+ </div>
1532
2121
  </div>
1533
- <button
1534
- class="bar-btn"
1535
- id="preview-btn"
1536
- title="Preview local port"
1537
- onclick="openPreviewModal()"
1538
- >
1539
- 🌐
1540
- </button>
1541
- <button class="bar-btn" id="share-btn" title="Share link">
2122
+ <button class="bar-btn" id="palette-trigger" title="Tools (Ctrl+K)">
1542
2123
  <svg
1543
2124
  width="16"
1544
2125
  height="16"
@@ -1549,49 +2130,10 @@
1549
2130
  stroke-linecap="round"
1550
2131
  stroke-linejoin="round"
1551
2132
  >
1552
- <circle cx="18" cy="5" r="3" />
1553
- <circle cx="6" cy="12" r="3" />
1554
- <circle cx="18" cy="19" r="3" />
1555
- <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
1556
- <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
1557
- </svg>
1558
- </button>
1559
- <button class="bar-btn" id="refresh-btn" title="Refresh app">
1560
- <svg
1561
- width="16"
1562
- height="16"
1563
- viewBox="0 0 24 24"
1564
- fill="none"
1565
- stroke="currentColor"
1566
- stroke-width="2"
1567
- stroke-linecap="round"
1568
- stroke-linejoin="round"
1569
- >
1570
- <polyline points="23 4 23 10 17 10" />
1571
- <polyline points="1 20 1 14 7 14" />
1572
- <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
1573
- </svg>
1574
- </button>
1575
- <button class="bar-btn" id="theme-toggle" title="Toggle theme">
1576
- <svg
1577
- width="16"
1578
- height="16"
1579
- viewBox="0 0 24 24"
1580
- fill="none"
1581
- stroke="currentColor"
1582
- stroke-width="2"
1583
- stroke-linecap="round"
1584
- stroke-linejoin="round"
1585
- >
1586
- <circle cx="12" cy="12" r="5" />
1587
- <line x1="12" y1="1" x2="12" y2="3" />
1588
- <line x1="12" y1="21" x2="12" y2="23" />
1589
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
1590
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
1591
- <line x1="1" y1="12" x2="3" y2="12" />
1592
- <line x1="21" y1="12" x2="23" y2="12" />
1593
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
1594
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
2133
+ <rect x="3" y="3" width="7" height="7" rx="1" />
2134
+ <rect x="14" y="3" width="7" height="7" rx="1" />
2135
+ <rect x="3" y="14" width="7" height="7" rx="1" />
2136
+ <rect x="14" y="14" width="7" height="7" rx="1" />
1595
2137
  </svg>
1596
2138
  </button>
1597
2139
  <button id="stop-btn" title="Stop session">
@@ -1612,7 +2154,16 @@
1612
2154
  </div>
1613
2155
 
1614
2156
  <!-- Terminals Wrapper (panes created dynamically) -->
1615
- <div id="terminals-wrapper"></div>
2157
+ <div id="terminals-wrapper">
2158
+ <div class="search-bar" id="search-bar">
2159
+ <input type="text" id="search-input" placeholder="Search…" autocomplete="off" />
2160
+ <span class="search-count" id="search-count"></span>
2161
+ <button id="search-regex" title="Regex">.*</button>
2162
+ <button id="search-prev" title="Previous">▲</button>
2163
+ <button id="search-next" title="Next">▼</button>
2164
+ <button id="search-close" title="Close">✕</button>
2165
+ </div>
2166
+ </div>
1616
2167
 
1617
2168
  <div id="copy-toast">Copied!</div>
1618
2169
 
@@ -1631,7 +2182,7 @@
1631
2182
  <div class="key-row">
1632
2183
  <button class="key-btn modifier" id="ctrl-btn" title="Toggle Ctrl modifier">Ctrl</button>
1633
2184
  <button class="key-btn modifier" id="shift-btn" title="Toggle Shift modifier">Shift</button>
1634
- <button class="key-btn special" data-key="&#x09;" title="Autocomplete">Tab</button>
2185
+ <button class="key-btn special" data-key="tab" title="Autocomplete">Tab</button>
1635
2186
  <button class="key-btn special key-danger" data-key="&#x03;" title="Interrupt process">
1636
2187
  ^C
1637
2188
  </button>
@@ -1878,9 +2429,29 @@
1878
2429
  </div>
1879
2430
  </div>
1880
2431
 
2432
+ <!-- Command Palette / Tool Panel -->
2433
+ <div id="palette-backdrop" class="palette-backdrop"></div>
2434
+ <div id="palette-panel" class="palette-panel">
2435
+ <div class="palette-header">
2436
+ <span>Tools</span>
2437
+ <button class="palette-close" id="palette-close">✕</button>
2438
+ </div>
2439
+ <div class="palette-body" id="palette-body"></div>
2440
+ </div>
2441
+
2442
+ <!-- Theme Sub-Panel -->
2443
+ <div class="theme-subpanel" id="theme-subpanel">
2444
+ <div class="theme-subpanel-header">
2445
+ <span>Theme</span>
2446
+ <button class="theme-subpanel-close" id="theme-subpanel-close">✕</button>
2447
+ </div>
2448
+ <div class="theme-subpanel-list" id="theme-subpanel-list"></div>
2449
+ </div>
2450
+
1881
2451
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
1882
2452
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
1883
2453
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
2454
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
1884
2455
  <script>
1885
2456
  // ===== Constants =====
1886
2457
  const SESSION_COLORS = [
@@ -1901,6 +2472,33 @@
1901
2472
 
1902
2473
  let splitSecondId = null;
1903
2474
 
2475
+ // ===== Notification State =====
2476
+ let notificationsEnabled = localStorage.getItem('termbeam-notifications') !== 'false';
2477
+
2478
+ function updateNotifyToggle() {
2479
+ // notify-toggle button removed from top bar; function kept for palette use
2480
+ }
2481
+
2482
+ function sendCommandNotification(sessionName) {
2483
+ if (Notification.permission !== 'granted') return;
2484
+ try {
2485
+ new Notification('Command finished in ' + sessionName, {
2486
+ icon: '/icons/icon-192.png',
2487
+ tag: 'termbeam-cmd',
2488
+ });
2489
+ } catch {}
2490
+ }
2491
+
2492
+ function resetSilenceTimer(ms) {
2493
+ if (ms.silenceTimer) clearTimeout(ms.silenceTimer);
2494
+ ms.silenceTimer = setTimeout(() => {
2495
+ ms.silenceTimer = null;
2496
+ if (document.hidden && notificationsEnabled) {
2497
+ sendCommandNotification(ms.name || ms.id);
2498
+ }
2499
+ }, 3000);
2500
+ }
2501
+
1904
2502
  // Clipboard copy fallback for non-secure contexts (HTTP over LAN)
1905
2503
  function copyFallback(text) {
1906
2504
  const ta = document.createElement('textarea');
@@ -1970,29 +2568,384 @@
1970
2568
  brightCyan: '#0598bc',
1971
2569
  brightWhite: '#a5a5a5',
1972
2570
  };
2571
+ const monokaiTermTheme = {
2572
+ background: '#272822',
2573
+ foreground: '#f8f8f2',
2574
+ cursor: '#f8f8f0',
2575
+ cursorAccent: '#272822',
2576
+ selectionBackground: 'rgba(166, 226, 46, 0.3)',
2577
+ black: '#272822',
2578
+ red: '#f92672',
2579
+ green: '#a6e22e',
2580
+ yellow: '#f4bf75',
2581
+ blue: '#66d9e8',
2582
+ magenta: '#ae81ff',
2583
+ cyan: '#a1efe4',
2584
+ white: '#f8f8f2',
2585
+ brightBlack: '#75715e',
2586
+ brightRed: '#f92672',
2587
+ brightGreen: '#a6e22e',
2588
+ brightYellow: '#f4bf75',
2589
+ brightBlue: '#66d9e8',
2590
+ brightMagenta: '#ae81ff',
2591
+ brightCyan: '#a1efe4',
2592
+ brightWhite: '#f9f8f5',
2593
+ };
2594
+ const solarizedDarkTermTheme = {
2595
+ background: '#002b36',
2596
+ foreground: '#839496',
2597
+ cursor: '#839496',
2598
+ cursorAccent: '#002b36',
2599
+ selectionBackground: 'rgba(7, 54, 66, 0.8)',
2600
+ black: '#073642',
2601
+ red: '#dc322f',
2602
+ green: '#859900',
2603
+ yellow: '#b58900',
2604
+ blue: '#268bd2',
2605
+ magenta: '#d33682',
2606
+ cyan: '#2aa198',
2607
+ white: '#eee8d5',
2608
+ brightBlack: '#002b36',
2609
+ brightRed: '#cb4b16',
2610
+ brightGreen: '#586e75',
2611
+ brightYellow: '#657b83',
2612
+ brightBlue: '#839496',
2613
+ brightMagenta: '#6c71c4',
2614
+ brightCyan: '#93a1a1',
2615
+ brightWhite: '#fdf6e3',
2616
+ };
2617
+ const solarizedLightTermTheme = {
2618
+ background: '#fdf6e3',
2619
+ foreground: '#657b83',
2620
+ cursor: '#586e75',
2621
+ cursorAccent: '#fdf6e3',
2622
+ selectionBackground: 'rgba(238, 232, 213, 0.8)',
2623
+ black: '#073642',
2624
+ red: '#dc322f',
2625
+ green: '#859900',
2626
+ yellow: '#b58900',
2627
+ blue: '#268bd2',
2628
+ magenta: '#d33682',
2629
+ cyan: '#2aa198',
2630
+ white: '#eee8d5',
2631
+ brightBlack: '#002b36',
2632
+ brightRed: '#cb4b16',
2633
+ brightGreen: '#586e75',
2634
+ brightYellow: '#657b83',
2635
+ brightBlue: '#839496',
2636
+ brightMagenta: '#6c71c4',
2637
+ brightCyan: '#93a1a1',
2638
+ brightWhite: '#fdf6e3',
2639
+ };
2640
+ const nordTermTheme = {
2641
+ background: '#2e3440',
2642
+ foreground: '#d8dee9',
2643
+ cursor: '#d8dee9',
2644
+ cursorAccent: '#2e3440',
2645
+ selectionBackground: 'rgba(67, 76, 94, 0.5)',
2646
+ black: '#3b4252',
2647
+ red: '#bf616a',
2648
+ green: '#a3be8c',
2649
+ yellow: '#ebcb8b',
2650
+ blue: '#81a1c1',
2651
+ magenta: '#b48ead',
2652
+ cyan: '#88c0d0',
2653
+ white: '#e5e9f0',
2654
+ brightBlack: '#4c566a',
2655
+ brightRed: '#bf616a',
2656
+ brightGreen: '#a3be8c',
2657
+ brightYellow: '#ebcb8b',
2658
+ brightBlue: '#81a1c1',
2659
+ brightMagenta: '#b48ead',
2660
+ brightCyan: '#8fbcbb',
2661
+ brightWhite: '#eceff4',
2662
+ };
2663
+ const draculaTermTheme = {
2664
+ background: '#282a36',
2665
+ foreground: '#f8f8f2',
2666
+ cursor: '#f8f8f2',
2667
+ cursorAccent: '#282a36',
2668
+ selectionBackground: 'rgba(68, 71, 90, 0.7)',
2669
+ black: '#21222c',
2670
+ red: '#ff5555',
2671
+ green: '#50fa7b',
2672
+ yellow: '#f1fa8c',
2673
+ blue: '#bd93f9',
2674
+ magenta: '#ff79c6',
2675
+ cyan: '#8be9fd',
2676
+ white: '#f8f8f2',
2677
+ brightBlack: '#6272a4',
2678
+ brightRed: '#ff6e6e',
2679
+ brightGreen: '#69ff94',
2680
+ brightYellow: '#ffffa5',
2681
+ brightBlue: '#d6acff',
2682
+ brightMagenta: '#ff92df',
2683
+ brightCyan: '#a4ffff',
2684
+ brightWhite: '#ffffff',
2685
+ };
2686
+ const githubDarkTermTheme = {
2687
+ background: '#0d1117',
2688
+ foreground: '#c9d1d9',
2689
+ cursor: '#c9d1d9',
2690
+ cursorAccent: '#0d1117',
2691
+ selectionBackground: 'rgba(56, 139, 253, 0.3)',
2692
+ black: '#484f58',
2693
+ red: '#ff7b72',
2694
+ green: '#3fb950',
2695
+ yellow: '#d29922',
2696
+ blue: '#58a6ff',
2697
+ magenta: '#bc8cff',
2698
+ cyan: '#39c5cf',
2699
+ white: '#c9d1d9',
2700
+ brightBlack: '#6e7681',
2701
+ brightRed: '#ffa198',
2702
+ brightGreen: '#56d364',
2703
+ brightYellow: '#e3b341',
2704
+ brightBlue: '#79c0ff',
2705
+ brightMagenta: '#d2a8ff',
2706
+ brightCyan: '#56d4dd',
2707
+ brightWhite: '#f0f6fc',
2708
+ };
2709
+ const oneDarkTermTheme = {
2710
+ background: '#282c34',
2711
+ foreground: '#abb2bf',
2712
+ cursor: '#528bff',
2713
+ cursorAccent: '#282c34',
2714
+ selectionBackground: 'rgba(62, 68, 82, 0.7)',
2715
+ black: '#3f4451',
2716
+ red: '#e06c75',
2717
+ green: '#98c379',
2718
+ yellow: '#e5c07b',
2719
+ blue: '#61afef',
2720
+ magenta: '#c678dd',
2721
+ cyan: '#56b6c2',
2722
+ white: '#d7dae0',
2723
+ brightBlack: '#4f5666',
2724
+ brightRed: '#e06c75',
2725
+ brightGreen: '#98c379',
2726
+ brightYellow: '#e5c07b',
2727
+ brightBlue: '#61afef',
2728
+ brightMagenta: '#c678dd',
2729
+ brightCyan: '#56b6c2',
2730
+ brightWhite: '#ffffff',
2731
+ };
2732
+ const catppuccinTermTheme = {
2733
+ background: '#1e1e2e',
2734
+ foreground: '#cdd6f4',
2735
+ cursor: '#f5e0dc',
2736
+ cursorAccent: '#1e1e2e',
2737
+ selectionBackground: 'rgba(88, 91, 112, 0.5)',
2738
+ black: '#45475a',
2739
+ red: '#f38ba8',
2740
+ green: '#a6e3a1',
2741
+ yellow: '#f9e2af',
2742
+ blue: '#89b4fa',
2743
+ magenta: '#f5c2e7',
2744
+ cyan: '#94e2d5',
2745
+ white: '#bac2de',
2746
+ brightBlack: '#585b70',
2747
+ brightRed: '#f38ba8',
2748
+ brightGreen: '#a6e3a1',
2749
+ brightYellow: '#f9e2af',
2750
+ brightBlue: '#89b4fa',
2751
+ brightMagenta: '#f5c2e7',
2752
+ brightCyan: '#94e2d5',
2753
+ brightWhite: '#a6adc8',
2754
+ };
2755
+ const gruvboxTermTheme = {
2756
+ background: '#282828',
2757
+ foreground: '#ebdbb2',
2758
+ cursor: '#ebdbb2',
2759
+ cursorAccent: '#282828',
2760
+ selectionBackground: 'rgba(80, 73, 69, 0.7)',
2761
+ black: '#282828',
2762
+ red: '#cc241d',
2763
+ green: '#98971a',
2764
+ yellow: '#d79921',
2765
+ blue: '#458588',
2766
+ magenta: '#b16286',
2767
+ cyan: '#689d6a',
2768
+ white: '#a89984',
2769
+ brightBlack: '#928374',
2770
+ brightRed: '#fb4934',
2771
+ brightGreen: '#b8bb26',
2772
+ brightYellow: '#fabd2f',
2773
+ brightBlue: '#83a598',
2774
+ brightMagenta: '#d3869b',
2775
+ brightCyan: '#8ec07c',
2776
+ brightWhite: '#ebdbb2',
2777
+ };
2778
+ const nightOwlTermTheme = {
2779
+ background: '#011627',
2780
+ foreground: '#d6deeb',
2781
+ cursor: '#80a4c2',
2782
+ cursorAccent: '#011627',
2783
+ selectionBackground: 'rgba(29, 59, 83, 0.7)',
2784
+ black: '#010e1a',
2785
+ red: '#ef5350',
2786
+ green: '#22da6e',
2787
+ yellow: '#addb67',
2788
+ blue: '#82aaff',
2789
+ magenta: '#c792ea',
2790
+ cyan: '#21c7a8',
2791
+ white: '#d6deeb',
2792
+ brightBlack: '#575656',
2793
+ brightRed: '#ef5350',
2794
+ brightGreen: '#22da6e',
2795
+ brightYellow: '#ffeb95',
2796
+ brightBlue: '#82aaff',
2797
+ brightMagenta: '#c792ea',
2798
+ brightCyan: '#7fdbca',
2799
+ brightWhite: '#ffffff',
2800
+ };
2801
+ const TERM_THEMES = {
2802
+ dark: darkTermTheme,
2803
+ light: lightTermTheme,
2804
+ monokai: monokaiTermTheme,
2805
+ 'solarized-dark': solarizedDarkTermTheme,
2806
+ 'solarized-light': solarizedLightTermTheme,
2807
+ nord: nordTermTheme,
2808
+ dracula: draculaTermTheme,
2809
+ 'github-dark': githubDarkTermTheme,
2810
+ 'one-dark': oneDarkTermTheme,
2811
+ catppuccin: catppuccinTermTheme,
2812
+ gruvbox: gruvboxTermTheme,
2813
+ 'night-owl': nightOwlTermTheme,
2814
+ };
1973
2815
 
1974
2816
  // ===== Theme =====
2817
+ const THEMES = [
2818
+ { id: 'dark', name: 'Dark', bg: '#1e1e1e' },
2819
+ { id: 'light', name: 'Light', bg: '#f3f3f3' },
2820
+ { id: 'monokai', name: 'Monokai', bg: '#272822' },
2821
+ { id: 'solarized-dark', name: 'Solarized Dark', bg: '#002b36' },
2822
+ { id: 'solarized-light', name: 'Solarized Light', bg: '#fdf6e3' },
2823
+ { id: 'nord', name: 'Nord', bg: '#2e3440' },
2824
+ { id: 'dracula', name: 'Dracula', bg: '#282a36' },
2825
+ { id: 'github-dark', name: 'GitHub Dark', bg: '#0d1117' },
2826
+ { id: 'one-dark', name: 'One Dark', bg: '#282c34' },
2827
+ { id: 'catppuccin', name: 'Catppuccin', bg: '#1e1e2e' },
2828
+ { id: 'gruvbox', name: 'Gruvbox', bg: '#282828' },
2829
+ { id: 'night-owl', name: 'Night Owl', bg: '#011627' },
2830
+ ];
1975
2831
  function getTheme() {
1976
2832
  return localStorage.getItem('termbeam-theme') || 'dark';
1977
2833
  }
1978
2834
  function applyTheme(theme) {
1979
2835
  document.documentElement.setAttribute('data-theme', theme);
1980
- document.querySelector('meta[name="theme-color"]').content =
1981
- theme === 'light' ? '#f3f3f3' : '#1e1e1e';
1982
- const btn = document.getElementById('theme-toggle');
1983
- if (btn)
1984
- btn.innerHTML =
1985
- theme === 'light'
1986
- ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
1987
- : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
2836
+ const t = THEMES.find((x) => x.id === theme) || THEMES[0];
2837
+ document.querySelector('meta[name="theme-color"]').content = t.bg;
1988
2838
  localStorage.setItem('termbeam-theme', theme);
2839
+ document.querySelectorAll('.theme-option').forEach((el) => {
2840
+ el.classList.toggle('active', el.dataset.themeOption === theme);
2841
+ });
2842
+ const termTheme = TERM_THEMES[theme] || darkTermTheme;
1989
2843
  for (const [, ms] of managed) {
1990
- ms.term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
2844
+ ms.term.options.theme = termTheme;
1991
2845
  }
1992
2846
  }
1993
2847
  applyTheme(getTheme());
1994
- document.getElementById('theme-toggle').addEventListener('click', () => {
1995
- applyTheme(getTheme() === 'light' ? 'dark' : 'light');
2848
+ document.getElementById('theme-toggle').addEventListener('click', (e) => {
2849
+ e.stopPropagation();
2850
+ document.getElementById('theme-picker').classList.toggle('open');
2851
+ });
2852
+ document.addEventListener('click', () => {
2853
+ document.getElementById('theme-picker').classList.remove('open');
2854
+ });
2855
+ document.querySelectorAll('.theme-option').forEach((el) => {
2856
+ el.addEventListener('click', (e) => {
2857
+ e.stopPropagation();
2858
+ applyTheme(el.dataset.themeOption);
2859
+ });
2860
+ });
2861
+
2862
+ // ===== Terminal Search =====
2863
+ const searchBar = document.getElementById('search-bar');
2864
+ const searchInput = document.getElementById('search-input');
2865
+ const searchCount = document.getElementById('search-count');
2866
+ const searchRegexBtn = document.getElementById('search-regex');
2867
+ let searchRegex = false;
2868
+ let searchResultIndex = 0;
2869
+ let searchResultTotal = 0;
2870
+
2871
+ function getActiveSearchAddon() {
2872
+ if (!activeId) return null;
2873
+ const ms = managed.get(activeId);
2874
+ return ms ? ms.searchAddon : null;
2875
+ }
2876
+
2877
+ function updateSearchCount(idx, total) {
2878
+ searchResultIndex = idx;
2879
+ searchResultTotal = total;
2880
+ searchCount.textContent = total > 0 ? idx + 1 + ' of ' + total : 'No results';
2881
+ }
2882
+
2883
+ function doSearch(direction) {
2884
+ const addon = getActiveSearchAddon();
2885
+ if (!addon) return;
2886
+ const query = searchInput.value;
2887
+ if (!query) {
2888
+ searchCount.textContent = '';
2889
+ return;
2890
+ }
2891
+ const opts = {
2892
+ regex: searchRegex,
2893
+ caseSensitive: false,
2894
+ incremental: direction === 'next',
2895
+ };
2896
+ let result;
2897
+ if (direction === 'prev') {
2898
+ result = addon.findPrevious(query, opts);
2899
+ } else {
2900
+ result = addon.findNext(query, opts);
2901
+ }
2902
+ // SearchAddon returns boolean; no match count API in v0.15
2903
+ searchCount.textContent = result ? 'Found' : 'No results';
2904
+ }
2905
+
2906
+ function openSearchBar() {
2907
+ searchBar.classList.add('visible');
2908
+ searchInput.focus();
2909
+ searchInput.select();
2910
+ }
2911
+
2912
+ function closeSearchBar() {
2913
+ searchBar.classList.remove('visible');
2914
+ searchCount.textContent = '';
2915
+ searchInput.value = '';
2916
+ const addon = getActiveSearchAddon();
2917
+ if (addon) addon.clearDecorations();
2918
+ // Re-focus terminal
2919
+ if (activeId) {
2920
+ const ms = managed.get(activeId);
2921
+ if (ms) ms.term.focus();
2922
+ }
2923
+ }
2924
+
2925
+ searchInput.addEventListener('input', () => doSearch('next'));
2926
+ searchInput.addEventListener('keydown', (e) => {
2927
+ if (e.key === 'Escape') {
2928
+ closeSearchBar();
2929
+ e.preventDefault();
2930
+ } else if (e.key === 'Enter') {
2931
+ e.preventDefault();
2932
+ doSearch(e.shiftKey ? 'prev' : 'next');
2933
+ }
2934
+ });
2935
+ document.getElementById('search-next').addEventListener('click', () => doSearch('next'));
2936
+ document.getElementById('search-prev').addEventListener('click', () => doSearch('prev'));
2937
+ document.getElementById('search-close').addEventListener('click', closeSearchBar);
2938
+ document.getElementById('search-regex').addEventListener('click', () => {
2939
+ searchRegex = !searchRegex;
2940
+ searchRegexBtn.classList.toggle('active', searchRegex);
2941
+ if (searchInput.value) doSearch('next');
2942
+ });
2943
+
2944
+ document.addEventListener('keydown', (e) => {
2945
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
2946
+ e.preventDefault();
2947
+ openSearchBar();
2948
+ }
1996
2949
  });
1997
2950
 
1998
2951
  // ===== Font Loading (non-blocking) =====
@@ -2049,7 +3002,7 @@
2049
3002
 
2050
3003
  // ===== Zoom =====
2051
3004
  const MIN_FONT = 2,
2052
- MAX_FONT = 28;
3005
+ MAX_FONT = 32;
2053
3006
  function defaultFontSize() {
2054
3007
  const w = window.innerWidth;
2055
3008
  if (w <= 480) return 12;
@@ -2130,11 +3083,61 @@
2130
3083
  loadShellsForModal();
2131
3084
  startPolling();
2132
3085
 
2133
- // Zoom
2134
- document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
2135
- document
2136
- .getElementById('zoom-out')
2137
- .addEventListener('click', () => applyZoom(fontSize - 2));
3086
+ // Pinch-to-zoom
3087
+ (function setupPinchZoom() {
3088
+ const wrapper = document.getElementById('terminals-wrapper');
3089
+ let pinchStartDist = 0;
3090
+ let pinchStartFont = 0;
3091
+ let pinchActive = false;
3092
+ let pinchZoomTimer = null;
3093
+
3094
+ function touchDist(t) {
3095
+ const dx = t[0].clientX - t[1].clientX;
3096
+ const dy = t[0].clientY - t[1].clientY;
3097
+ return Math.sqrt(dx * dx + dy * dy);
3098
+ }
3099
+
3100
+ wrapper.addEventListener(
3101
+ 'touchstart',
3102
+ function (e) {
3103
+ if (e.touches.length === 2) {
3104
+ pinchActive = true;
3105
+ pinchStartDist = touchDist(e.touches);
3106
+ pinchStartFont = fontSize;
3107
+ wrapper.style.touchAction = 'none';
3108
+ }
3109
+ },
3110
+ { passive: true },
3111
+ );
3112
+
3113
+ wrapper.addEventListener(
3114
+ 'touchmove',
3115
+ function (e) {
3116
+ if (!pinchActive || e.touches.length !== 2) return;
3117
+ e.preventDefault();
3118
+ const dist = touchDist(e.touches);
3119
+ const scale = dist / pinchStartDist;
3120
+ const newSize = Math.round(pinchStartFont * scale);
3121
+ if (newSize !== fontSize) {
3122
+ if (pinchZoomTimer) clearTimeout(pinchZoomTimer);
3123
+ pinchZoomTimer = setTimeout(function () {
3124
+ applyZoom(newSize);
3125
+ }, 50);
3126
+ }
3127
+ },
3128
+ { passive: false },
3129
+ );
3130
+
3131
+ wrapper.addEventListener('touchend', function () {
3132
+ pinchActive = false;
3133
+ wrapper.style.touchAction = '';
3134
+ });
3135
+
3136
+ wrapper.addEventListener('touchcancel', function () {
3137
+ pinchActive = false;
3138
+ wrapper.style.touchAction = '';
3139
+ });
3140
+ })();
2138
3141
 
2139
3142
  // Resize
2140
3143
  function doResize() {
@@ -2207,12 +3210,10 @@
2207
3210
  );
2208
3211
  }
2209
3212
 
2210
- // Split toggle
2211
- document.getElementById('split-toggle').addEventListener('click', toggleSplit);
2212
-
2213
3213
  // Scroll to bottom when returning from idle / tab switch
2214
3214
  document.addEventListener('visibilitychange', () => {
2215
3215
  if (!document.hidden && activeId) {
3216
+ clearUnreadIndicator();
2216
3217
  const ms = managed.get(activeId);
2217
3218
  if (ms) {
2218
3219
  ms.term.scrollToBottom();
@@ -2256,7 +3257,7 @@
2256
3257
  fetch('/api/version')
2257
3258
  .then((r) => r.json())
2258
3259
  .then((d) => {
2259
- document.getElementById('version-text').textContent = 'v' + d.version;
3260
+ window._termbeamVersion = 'v' + d.version;
2260
3261
  document.getElementById('side-panel-version').textContent = 'v' + d.version;
2261
3262
  })
2262
3263
  .catch(() => {});
@@ -2265,6 +3266,7 @@
2265
3266
  // ===== Session Management =====
2266
3267
  function addSession(data) {
2267
3268
  if (managed.has(data.id)) return;
3269
+ managed.set(data.id, null); // reserve slot to prevent race condition
2268
3270
 
2269
3271
  const term = new window.Terminal({
2270
3272
  cursorBlink: true,
@@ -2275,7 +3277,7 @@
2275
3277
  fontWeightBold: 'bold',
2276
3278
  letterSpacing: 0,
2277
3279
  lineHeight: 1.1,
2278
- theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
3280
+ theme: TERM_THEMES[getTheme()] || darkTermTheme,
2279
3281
  allowProposedApi: true,
2280
3282
  scrollback: 10000,
2281
3283
  scrollOnOutput: false,
@@ -2283,8 +3285,10 @@
2283
3285
 
2284
3286
  const fitAddon = new window.FitAddon.FitAddon();
2285
3287
  const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
3288
+ const searchAddon = new window.SearchAddon.SearchAddon();
2286
3289
  term.loadAddon(fitAddon);
2287
3290
  term.loadAddon(webLinksAddon);
3291
+ term.loadAddon(searchAddon);
2288
3292
 
2289
3293
  const container = document.createElement('div');
2290
3294
  container.className = 'terminal-pane';
@@ -2434,6 +3438,7 @@
2434
3438
  pid: data.pid,
2435
3439
  term,
2436
3440
  fitAddon,
3441
+ searchAddon,
2437
3442
  container,
2438
3443
  coalescedWrite,
2439
3444
  scrollBtn,
@@ -2442,6 +3447,7 @@
2442
3447
  reconnectTimer: null,
2443
3448
  reconnectDelay: 3000,
2444
3449
  lastActivity: data.lastActivity || Date.now(),
3450
+ silenceTimer: null,
2445
3451
  };
2446
3452
 
2447
3453
  managed.set(data.id, ms);
@@ -2472,6 +3478,53 @@
2472
3478
  return ms;
2473
3479
  }
2474
3480
 
3481
+ // Tab title activity indicator for unread output
3482
+ const originalTitle = document.title;
3483
+ let hasUnread = false;
3484
+
3485
+ // Shared AudioContext — must be created/resumed on user gesture for mobile
3486
+ let notifAudioCtx = null;
3487
+ function ensureAudioContext() {
3488
+ if (!notifAudioCtx) {
3489
+ notifAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
3490
+ }
3491
+ if (notifAudioCtx.state === 'suspended') notifAudioCtx.resume();
3492
+ return notifAudioCtx;
3493
+ }
3494
+ document.addEventListener('click', ensureAudioContext, { once: true });
3495
+ document.addEventListener('touchstart', ensureAudioContext, { once: true });
3496
+
3497
+ function playNotificationSound() {
3498
+ try {
3499
+ const ctx = ensureAudioContext();
3500
+ const osc = ctx.createOscillator();
3501
+ const gain = ctx.createGain();
3502
+ osc.connect(gain);
3503
+ gain.connect(ctx.destination);
3504
+ osc.frequency.value = 440;
3505
+ osc.type = 'sine';
3506
+ gain.gain.setValueAtTime(0.15, ctx.currentTime);
3507
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
3508
+ osc.start(ctx.currentTime);
3509
+ osc.stop(ctx.currentTime + 0.15);
3510
+ } catch {
3511
+ // Audio not available
3512
+ }
3513
+ }
3514
+
3515
+ function markUnreadOutput() {
3516
+ if (!document.hidden || hasUnread) return;
3517
+ hasUnread = true;
3518
+ playNotificationSound();
3519
+ document.title = '(\u25CF) ' + originalTitle;
3520
+ }
3521
+
3522
+ function clearUnreadIndicator() {
3523
+ if (!hasUnread) return;
3524
+ hasUnread = false;
3525
+ document.title = originalTitle;
3526
+ }
3527
+
2475
3528
  function connectSession(ms) {
2476
3529
  if (ms.reconnectTimer) {
2477
3530
  clearTimeout(ms.reconnectTimer);
@@ -2506,6 +3559,13 @@
2506
3559
  if (msg.type === 'output') {
2507
3560
  ms.coalescedWrite(msg.data);
2508
3561
  ms.lastActivity = Date.now();
3562
+ resetSilenceTimer(ms);
3563
+ markUnreadOutput();
3564
+ if (ms.id !== activeId && !ms.hasUnread) {
3565
+ ms.hasUnread = true;
3566
+ playNotificationSound();
3567
+ renderTabs();
3568
+ }
2509
3569
  } else if (msg.type === 'attached') {
2510
3570
  if (ms.container.classList.contains('visible')) {
2511
3571
  sendResize(ms);
@@ -2567,6 +3627,8 @@
2567
3627
  function activateSession(id) {
2568
3628
  if (!managed.has(id)) return;
2569
3629
  activeId = id;
3630
+ const ms = managed.get(id);
3631
+ if (ms) ms.hasUnread = false;
2570
3632
  reconnectOverlay.classList.remove('visible');
2571
3633
 
2572
3634
  // Show/hide panes
@@ -2618,6 +3680,10 @@
2618
3680
  clearTimeout(ms.reconnectTimer);
2619
3681
  ms.reconnectTimer = null;
2620
3682
  }
3683
+ if (ms.silenceTimer) {
3684
+ clearTimeout(ms.silenceTimer);
3685
+ ms.silenceTimer = null;
3686
+ }
2621
3687
  if (ms.ws)
2622
3688
  try {
2623
3689
  ms.ws.close();
@@ -2681,6 +3747,7 @@
2681
3747
  esc(ms.name) +
2682
3748
  '</span>' +
2683
3749
  (activity ? '<span class="tab-activity">' + activity + '</span>' : '') +
3750
+ (ms.hasUnread && !isActive ? '<span class="tab-unread"></span>' : '') +
2684
3751
  '<span class="tab-status" style="background:' +
2685
3752
  safeColor(statusColor) +
2686
3753
  '"></span>' +
@@ -2770,6 +3837,14 @@
2770
3837
  activateSession(tab.dataset.id);
2771
3838
  });
2772
3839
 
3840
+ // Middle-click to close tab
3841
+ tab.addEventListener('auxclick', (e) => {
3842
+ if (e.button === 1) {
3843
+ e.preventDefault();
3844
+ if (confirm('Close this session?')) removeSession(tab.dataset.id);
3845
+ }
3846
+ });
3847
+
2773
3848
  // Desktop: hover preview (non-active tabs only)
2774
3849
  tab.addEventListener('mouseenter', () => {
2775
3850
  if (tab.dataset.id === activeId) return;
@@ -2832,6 +3907,7 @@
2832
3907
  '<span class="side-panel-card-name">' +
2833
3908
  esc(ms.name) +
2834
3909
  '</span>' +
3910
+ (ms.hasUnread && !isActive ? '<span class="tab-unread"></span>' : '') +
2835
3911
  '<span class="side-panel-card-status" style="background:' +
2836
3912
  safeColor(statusColor) +
2837
3913
  '"></span>' +
@@ -2994,7 +4070,6 @@
2994
4070
  // ===== Split View =====
2995
4071
  function toggleSplit() {
2996
4072
  splitMode = !splitMode;
2997
- document.getElementById('split-toggle').classList.toggle('active', splitMode);
2998
4073
 
2999
4074
  if (splitMode) {
3000
4075
  // Find a second session to show
@@ -3081,7 +4156,12 @@
3081
4156
  function sendKey(btn) {
3082
4157
  if (!btn || !btn.dataset.key) return;
3083
4158
  flashBtn(btn);
3084
- let data = btn.dataset.key === 'enter' ? '\r' : btn.dataset.key;
4159
+ let data =
4160
+ btn.dataset.key === 'enter'
4161
+ ? '\r'
4162
+ : btn.dataset.key === 'tab'
4163
+ ? '\x09'
4164
+ : btn.dataset.key;
3085
4165
  data = applyModifiers(data);
3086
4166
  const ms = managed.get(activeId);
3087
4167
  if (ms && ms.ws && ms.ws.readyState === 1) {
@@ -3614,6 +4694,18 @@
3614
4694
  if (cmd) body.initialCommand = cmd;
3615
4695
  if (color) body.color = color;
3616
4696
 
4697
+ // Include current terminal dimensions so the PTY spawns at the right
4698
+ // size — prevents oh-my-posh and other slow prompts from rendering
4699
+ // at the default 120×30 size and triggering a duplicate on SIGWINCH.
4700
+ const activeMs = managed.get(activeId);
4701
+ if (activeMs && activeMs.fitAddon) {
4702
+ const dims = activeMs.fitAddon.proposeDimensions();
4703
+ if (dims) {
4704
+ body.cols = dims.cols;
4705
+ body.rows = dims.rows;
4706
+ }
4707
+ }
4708
+
3617
4709
  try {
3618
4710
  const res = await fetch('/api/sessions', {
3619
4711
  method: 'POST',
@@ -3668,6 +4760,10 @@
3668
4760
  clearTimeout(ms.reconnectTimer);
3669
4761
  ms.reconnectTimer = null;
3670
4762
  }
4763
+ if (ms.silenceTimer) {
4764
+ clearTimeout(ms.silenceTimer);
4765
+ ms.silenceTimer = null;
4766
+ }
3671
4767
  if (ms.ws)
3672
4768
  try {
3673
4769
  ms.ws.close();
@@ -3801,12 +4897,11 @@
3801
4897
  });
3802
4898
  }
3803
4899
 
3804
- document.getElementById('share-btn').addEventListener('click', async () => {
4900
+ async function shareLink() {
3805
4901
  const urlPromise = fetch('/api/share-token')
3806
4902
  .then((r) => (r.ok ? r.json() : null))
3807
4903
  .then((data) => (data && data.url) || location.href)
3808
4904
  .catch(() => location.href);
3809
- // ClipboardItem with a promise preserves user activation across the fetch
3810
4905
  if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
3811
4906
  try {
3812
4907
  const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
@@ -3815,7 +4910,6 @@
3815
4910
  return;
3816
4911
  } catch {}
3817
4912
  }
3818
- // Fallback: resolve URL first, then try legacy methods
3819
4913
  const url = await urlPromise;
3820
4914
  if (navigator.clipboard && navigator.clipboard.writeText) {
3821
4915
  try {
@@ -3829,10 +4923,9 @@
3829
4923
  } else {
3830
4924
  showShareUrlPrompt(url);
3831
4925
  }
3832
- });
4926
+ }
3833
4927
 
3834
- // ===== Refresh Button =====
3835
- document.getElementById('refresh-btn').addEventListener('click', async () => {
4928
+ async function refreshApp() {
3836
4929
  if ('caches' in window) {
3837
4930
  const keys = await caches.keys();
3838
4931
  await Promise.all(keys.map((k) => caches.delete(k)));
@@ -3842,7 +4935,294 @@
3842
4935
  if (reg) await reg.update();
3843
4936
  }
3844
4937
  location.reload();
3845
- });
4938
+ }
4939
+
4940
+ // ===== Command Palette =====
4941
+ (function setupPalette() {
4942
+ const paletteActions = [
4943
+ {
4944
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
4945
+ label: 'New tab',
4946
+ category: 'Session',
4947
+ action: () => openNewSessionModal(),
4948
+ },
4949
+ {
4950
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
4951
+ label: 'Close tab',
4952
+ category: 'Session',
4953
+ action: () => {
4954
+ if (!activeId) return;
4955
+ const ms = managed.get(activeId);
4956
+ const name = (ms && ms.name) || activeId.slice(0, 8);
4957
+ if (confirm('Close session "' + name + '"?')) {
4958
+ removeSession(activeId);
4959
+ }
4960
+ },
4961
+ },
4962
+ {
4963
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>',
4964
+ label: 'Rename session',
4965
+ category: 'Session',
4966
+ action: () => {
4967
+ if (!activeId) return;
4968
+ const ms = managed.get(activeId);
4969
+ if (!ms) return;
4970
+ const name = prompt('Rename session:', ms.name || '');
4971
+ if (name !== null && name.trim()) {
4972
+ ms.name = name.trim();
4973
+ renderTabs();
4974
+ updateStatusBar();
4975
+ }
4976
+ },
4977
+ },
4978
+ {
4979
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg>',
4980
+ label: 'Split view',
4981
+ category: 'Session',
4982
+ action: () => toggleSplit(),
4983
+ },
4984
+ {
4985
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>',
4986
+ label: 'Stop session',
4987
+ category: 'Session',
4988
+ action: () => {
4989
+ if (!activeId) return;
4990
+ if (!confirm('Stop this session? The process will be killed.')) return;
4991
+ removeSession(activeId);
4992
+ },
4993
+ },
4994
+ {
4995
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
4996
+ label: 'Find in terminal',
4997
+ category: 'Search',
4998
+ action: () => openSearchBar(),
4999
+ },
5000
+ {
5001
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>',
5002
+ label: 'Increase font size',
5003
+ category: 'View',
5004
+ action: () => applyZoom(fontSize + 1),
5005
+ },
5006
+ {
5007
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>',
5008
+ label: 'Decrease font size',
5009
+ category: 'View',
5010
+ action: () => applyZoom(fontSize - 1),
5011
+ },
5012
+ {
5013
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
5014
+ get label() {
5015
+ const current = THEMES.find((x) => x.id === getTheme()) || THEMES[0];
5016
+ return 'Theme (' + current.name + ')';
5017
+ },
5018
+ category: 'View',
5019
+ action: () => {
5020
+ openThemeSubpanel();
5021
+ },
5022
+ },
5023
+ {
5024
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
5025
+ label: 'Preview port',
5026
+ category: 'View',
5027
+ action: () => openPreviewModal(),
5028
+ },
5029
+ {
5030
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
5031
+ label: 'Copy link',
5032
+ category: 'Share',
5033
+ action: () => {
5034
+ const url = location.href;
5035
+ if (navigator.clipboard && navigator.clipboard.writeText) {
5036
+ navigator.clipboard.writeText(url).then(
5037
+ () => showToast('Link copied!'),
5038
+ () => {
5039
+ if (copyToClipboardFallback(url)) showToast('Link copied!');
5040
+ else showShareUrlPrompt(url);
5041
+ },
5042
+ );
5043
+ } else if (copyToClipboardFallback(url)) {
5044
+ showToast('Link copied!');
5045
+ } else {
5046
+ showShareUrlPrompt(url);
5047
+ }
5048
+ },
5049
+ },
5050
+ {
5051
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>',
5052
+ get label() {
5053
+ return notificationsEnabled ? 'Notifications (on)' : 'Notifications (off)';
5054
+ },
5055
+ category: 'Notifications',
5056
+ keepOpen: true,
5057
+ action: () => {
5058
+ notificationsEnabled = !notificationsEnabled;
5059
+ localStorage.setItem('termbeam-notifications', notificationsEnabled);
5060
+ if (
5061
+ notificationsEnabled &&
5062
+ 'Notification' in window &&
5063
+ Notification.permission === 'default'
5064
+ ) {
5065
+ Notification.requestPermission();
5066
+ }
5067
+ showToast(notificationsEnabled ? 'Notifications on' : 'Notifications off');
5068
+ renderPalette();
5069
+ },
5070
+ },
5071
+ {
5072
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>',
5073
+ label: 'Refresh',
5074
+ category: 'System',
5075
+ action: () => refreshApp(),
5076
+ },
5077
+ {
5078
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="12" y1="9" x2="18" y2="15"/></svg>',
5079
+ label: 'Clear terminal',
5080
+ category: 'System',
5081
+ action: () => {
5082
+ if (!activeId) return;
5083
+ const ms = managed.get(activeId);
5084
+ if (ms) ms.term.clear();
5085
+ },
5086
+ },
5087
+ {
5088
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
5089
+ label: 'About',
5090
+ category: 'System',
5091
+ action: () => {
5092
+ const ver = window._termbeamVersion || 'TermBeam';
5093
+ const overlay = document.createElement('div');
5094
+ overlay.style.cssText =
5095
+ 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
5096
+ const box = document.createElement('div');
5097
+ box.style.cssText =
5098
+ 'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:90vw;width:320px;text-align:center;';
5099
+ box.innerHTML =
5100
+ '<div style="font-size:24px;margin-bottom:8px;">⚡</div>' +
5101
+ '<div style="font-size:16px;font-weight:600;color:var(--text);margin-bottom:4px;">TermBeam</div>' +
5102
+ '<div style="font-size:13px;color:var(--text-secondary);margin-bottom:16px;">' +
5103
+ esc(ver) +
5104
+ '</div>' +
5105
+ '<div style="font-size:12px;color:var(--text-secondary);margin-bottom:16px;">Terminal in your browser, optimized for mobile.</div>' +
5106
+ '<div style="display:flex;gap:16px;justify-content:center;margin-bottom:16px;">' +
5107
+ '<a href="https://github.com/dorlugasigal/TermBeam" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;text-decoration:none;">GitHub</a>' +
5108
+ '<a href="https://dorlugasigal.github.io/TermBeam/" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;text-decoration:none;">Docs</a>' +
5109
+ '<a href="https://termbeam.pages.dev" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;text-decoration:none;">Website</a>' +
5110
+ '</div>';
5111
+ const btn = document.createElement('button');
5112
+ btn.textContent = 'Close';
5113
+ btn.style.cssText =
5114
+ 'padding:6px 20px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';
5115
+ btn.onclick = () => overlay.remove();
5116
+ box.appendChild(btn);
5117
+ overlay.appendChild(box);
5118
+ overlay.addEventListener('click', (e) => {
5119
+ if (e.target === overlay) overlay.remove();
5120
+ });
5121
+ document.body.appendChild(overlay);
5122
+ },
5123
+ },
5124
+ ];
5125
+
5126
+ const backdrop = document.getElementById('palette-backdrop');
5127
+ const panel = document.getElementById('palette-panel');
5128
+ const body = document.getElementById('palette-body');
5129
+
5130
+ function renderPalette() {
5131
+ const grouped = {};
5132
+ paletteActions.forEach((a) => {
5133
+ if (!grouped[a.category]) grouped[a.category] = [];
5134
+ grouped[a.category].push(a);
5135
+ });
5136
+ body.innerHTML = '';
5137
+ Object.keys(grouped).forEach((cat) => {
5138
+ const header = document.createElement('div');
5139
+ header.className = 'palette-category';
5140
+ header.textContent = cat;
5141
+ body.appendChild(header);
5142
+ grouped[cat].forEach((a) => {
5143
+ const btn = document.createElement('button');
5144
+ btn.className = 'palette-action';
5145
+ btn.innerHTML =
5146
+ '<span class="palette-action-icon">' + a.icon + '</span>' + esc(a.label);
5147
+ btn.addEventListener('click', () => {
5148
+ if (!a.keepOpen) closePalette();
5149
+ a.action();
5150
+ });
5151
+ body.appendChild(btn);
5152
+ });
5153
+ });
5154
+ }
5155
+
5156
+ function openPalette() {
5157
+ backdrop.classList.add('open');
5158
+ panel.classList.add('open');
5159
+ }
5160
+
5161
+ function closePalette() {
5162
+ backdrop.classList.remove('open');
5163
+ panel.classList.remove('open');
5164
+ }
5165
+
5166
+ function togglePalette() {
5167
+ if (panel.classList.contains('open')) closePalette();
5168
+ else openPalette();
5169
+ }
5170
+
5171
+ backdrop.addEventListener('click', closePalette);
5172
+ document.getElementById('palette-close').addEventListener('click', closePalette);
5173
+ document.getElementById('palette-trigger').addEventListener('click', togglePalette);
5174
+
5175
+ document.addEventListener('keydown', (e) => {
5176
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
5177
+ e.preventDefault();
5178
+ togglePalette();
5179
+ }
5180
+ if (e.key === 'Escape' && panel.classList.contains('open')) {
5181
+ closePalette();
5182
+ }
5183
+ });
5184
+
5185
+ renderPalette();
5186
+ })();
5187
+
5188
+ // ===== Theme Sub-Panel =====
5189
+ (function setupThemeSubpanel() {
5190
+ const subpanel = document.getElementById('theme-subpanel');
5191
+ const list = document.getElementById('theme-subpanel-list');
5192
+ document.getElementById('theme-subpanel-close').addEventListener('click', () => {
5193
+ subpanel.classList.remove('open');
5194
+ });
5195
+ function renderThemeList() {
5196
+ const cur = getTheme();
5197
+ list.innerHTML = THEMES.map(
5198
+ (t) =>
5199
+ '<button class="theme-subpanel-item' +
5200
+ (t.id === cur ? ' active' : '') +
5201
+ '" data-tid="' +
5202
+ t.id +
5203
+ '"><span class="theme-subpanel-swatch" style="background:' +
5204
+ t.bg +
5205
+ '"></span>' +
5206
+ esc(t.name) +
5207
+ '</button>',
5208
+ ).join('');
5209
+ list.querySelectorAll('.theme-subpanel-item').forEach((btn) => {
5210
+ btn.addEventListener('click', () => {
5211
+ applyTheme(btn.dataset.tid);
5212
+ renderThemeList();
5213
+ });
5214
+ });
5215
+ }
5216
+ window.openThemeSubpanel = function () {
5217
+ renderThemeList();
5218
+ subpanel.classList.add('open');
5219
+ };
5220
+ document.addEventListener('keydown', (e) => {
5221
+ if (e.key === 'Escape' && subpanel.classList.contains('open')) {
5222
+ subpanel.classList.remove('open');
5223
+ }
5224
+ });
5225
+ })();
3846
5226
 
3847
5227
  // ===== Service Worker =====
3848
5228
  if ('serviceWorker' in navigator) {