termbeam 1.4.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.
- package/README.md +3 -0
- package/package.json +1 -1
- package/public/terminal.html +750 -93
- package/src/routes.js +6 -2
- package/src/sessions.js +15 -3
- package/src/websocket.js +31 -4
package/README.md
CHANGED
|
@@ -79,6 +79,9 @@ termbeam --no-password # disable password protection
|
|
|
79
79
|
- **Initial command** — optionally launch a session straight into `htop`, `vim`, or any command
|
|
80
80
|
- **Shell detection** — auto-detects your shell on all platforms (PowerShell, cmd, bash, zsh, Git Bash, WSL)
|
|
81
81
|
- **QR code on startup** for instant phone connection
|
|
82
|
+
- **Command completion notifications** — get browser notifications when a command finishes in a background tab; toggle with the bell icon in the toolbar (opt-in, requires browser permission)
|
|
83
|
+
- **Terminal search** — press <kbd>Ctrl+F</kbd> / <kbd>Cmd+F</kbd> to open a search overlay with regex support, powered by xterm.js SearchAddon
|
|
84
|
+
- **Command palette** — press <kbd>Ctrl+K</kbd> / <kbd>Cmd+K</kbd> (or tap the ⚙️ button) to open a slide-out tool panel with categorized actions: Session, Search, View, Share, Notifications, and System
|
|
82
85
|
- **Light/dark theme** with persistent preference
|
|
83
86
|
- **Adjustable font size** via status bar controls, saved across sessions
|
|
84
87
|
- **Port preview** — reverse-proxy a single local web server port and preview it in the browser (HTTP only; no WebSocket/HMR; best with server-rendered apps)
|
package/package.json
CHANGED
package/public/terminal.html
CHANGED
|
@@ -611,7 +611,7 @@
|
|
|
611
611
|
}
|
|
612
612
|
.theme-wrap {
|
|
613
613
|
position: relative;
|
|
614
|
-
display:
|
|
614
|
+
display: none;
|
|
615
615
|
align-items: center;
|
|
616
616
|
}
|
|
617
617
|
.theme-picker {
|
|
@@ -662,7 +662,7 @@
|
|
|
662
662
|
height: 30px;
|
|
663
663
|
border-radius: 8px;
|
|
664
664
|
cursor: pointer;
|
|
665
|
-
display:
|
|
665
|
+
display: none;
|
|
666
666
|
align-items: center;
|
|
667
667
|
justify-content: center;
|
|
668
668
|
gap: 4px;
|
|
@@ -685,6 +685,75 @@
|
|
|
685
685
|
transform: scale(0.9);
|
|
686
686
|
}
|
|
687
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
|
+
|
|
688
757
|
/* ===== Terminals Wrapper ===== */
|
|
689
758
|
#terminals-wrapper {
|
|
690
759
|
position: absolute;
|
|
@@ -1668,17 +1737,12 @@
|
|
|
1668
1737
|
#tab-list {
|
|
1669
1738
|
display: none;
|
|
1670
1739
|
}
|
|
1671
|
-
|
|
1672
|
-
display: none;
|
|
1673
|
-
}
|
|
1674
|
-
#version-text {
|
|
1675
|
-
display: none;
|
|
1676
|
-
}
|
|
1740
|
+
|
|
1677
1741
|
#back-btn {
|
|
1678
1742
|
display: none;
|
|
1679
1743
|
}
|
|
1680
1744
|
#theme-wrap {
|
|
1681
|
-
display:
|
|
1745
|
+
display: none;
|
|
1682
1746
|
}
|
|
1683
1747
|
#stop-btn {
|
|
1684
1748
|
padding: 0 8px;
|
|
@@ -1695,6 +1759,195 @@
|
|
|
1695
1759
|
width: auto;
|
|
1696
1760
|
}
|
|
1697
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
|
+
}
|
|
1698
1951
|
</style>
|
|
1699
1952
|
</head>
|
|
1700
1953
|
<body>
|
|
@@ -1792,7 +2045,6 @@
|
|
|
1792
2045
|
</div>
|
|
1793
2046
|
<div id="tab-list"></div>
|
|
1794
2047
|
<div class="right">
|
|
1795
|
-
<span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
|
|
1796
2048
|
<button class="tab-bar-btn" id="tab-new-btn" title="New session">
|
|
1797
2049
|
<svg
|
|
1798
2050
|
width="14"
|
|
@@ -1807,67 +2059,6 @@
|
|
|
1807
2059
|
<line x1="5" y1="12" x2="19" y2="12" /></svg
|
|
1808
2060
|
><span class="new-btn-label">New</span>
|
|
1809
2061
|
</button>
|
|
1810
|
-
<button class="tab-bar-btn" id="split-toggle" title="Split view">
|
|
1811
|
-
<svg
|
|
1812
|
-
width="16"
|
|
1813
|
-
height="16"
|
|
1814
|
-
viewBox="0 0 24 24"
|
|
1815
|
-
fill="none"
|
|
1816
|
-
stroke="currentColor"
|
|
1817
|
-
stroke-width="2"
|
|
1818
|
-
stroke-linecap="round"
|
|
1819
|
-
stroke-linejoin="round"
|
|
1820
|
-
>
|
|
1821
|
-
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
1822
|
-
<line x1="12" y1="3" x2="12" y2="21" />
|
|
1823
|
-
</svg>
|
|
1824
|
-
</button>
|
|
1825
|
-
<div class="bar-group">
|
|
1826
|
-
<button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
|
|
1827
|
-
<button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
|
|
1828
|
-
</div>
|
|
1829
|
-
<button
|
|
1830
|
-
class="bar-btn"
|
|
1831
|
-
id="preview-btn"
|
|
1832
|
-
title="Preview local port"
|
|
1833
|
-
onclick="openPreviewModal()"
|
|
1834
|
-
>
|
|
1835
|
-
🌐
|
|
1836
|
-
</button>
|
|
1837
|
-
<button class="bar-btn" id="share-btn" title="Share link">
|
|
1838
|
-
<svg
|
|
1839
|
-
width="16"
|
|
1840
|
-
height="16"
|
|
1841
|
-
viewBox="0 0 24 24"
|
|
1842
|
-
fill="none"
|
|
1843
|
-
stroke="currentColor"
|
|
1844
|
-
stroke-width="2"
|
|
1845
|
-
stroke-linecap="round"
|
|
1846
|
-
stroke-linejoin="round"
|
|
1847
|
-
>
|
|
1848
|
-
<circle cx="18" cy="5" r="3" />
|
|
1849
|
-
<circle cx="6" cy="12" r="3" />
|
|
1850
|
-
<circle cx="18" cy="19" r="3" />
|
|
1851
|
-
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
|
1852
|
-
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
|
1853
|
-
</svg>
|
|
1854
|
-
</button>
|
|
1855
|
-
<button class="bar-btn" id="refresh-btn" title="Refresh app">
|
|
1856
|
-
<svg
|
|
1857
|
-
width="16"
|
|
1858
|
-
height="16"
|
|
1859
|
-
viewBox="0 0 24 24"
|
|
1860
|
-
fill="none"
|
|
1861
|
-
stroke="currentColor"
|
|
1862
|
-
stroke-width="2"
|
|
1863
|
-
stroke-linecap="round"
|
|
1864
|
-
stroke-linejoin="round"
|
|
1865
|
-
>
|
|
1866
|
-
<polyline points="23 4 23 10 17 10" />
|
|
1867
|
-
<polyline points="1 20 1 14 7 14" />
|
|
1868
|
-
<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" />
|
|
1869
|
-
</svg>
|
|
1870
|
-
</button>
|
|
1871
2062
|
<div class="theme-wrap" id="theme-wrap">
|
|
1872
2063
|
<button class="bar-btn" id="theme-toggle" title="Switch theme">
|
|
1873
2064
|
<svg
|
|
@@ -1928,6 +2119,23 @@
|
|
|
1928
2119
|
</div>
|
|
1929
2120
|
</div>
|
|
1930
2121
|
</div>
|
|
2122
|
+
<button class="bar-btn" id="palette-trigger" title="Tools (Ctrl+K)">
|
|
2123
|
+
<svg
|
|
2124
|
+
width="16"
|
|
2125
|
+
height="16"
|
|
2126
|
+
viewBox="0 0 24 24"
|
|
2127
|
+
fill="none"
|
|
2128
|
+
stroke="currentColor"
|
|
2129
|
+
stroke-width="2"
|
|
2130
|
+
stroke-linecap="round"
|
|
2131
|
+
stroke-linejoin="round"
|
|
2132
|
+
>
|
|
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" />
|
|
2137
|
+
</svg>
|
|
2138
|
+
</button>
|
|
1931
2139
|
<button id="stop-btn" title="Stop session">
|
|
1932
2140
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
1933
2141
|
<rect x="6" y="6" width="12" height="12" rx="2" /></svg
|
|
@@ -1946,7 +2154,16 @@
|
|
|
1946
2154
|
</div>
|
|
1947
2155
|
|
|
1948
2156
|
<!-- Terminals Wrapper (panes created dynamically) -->
|
|
1949
|
-
<div id="terminals-wrapper"
|
|
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>
|
|
1950
2167
|
|
|
1951
2168
|
<div id="copy-toast">Copied!</div>
|
|
1952
2169
|
|
|
@@ -1965,7 +2182,7 @@
|
|
|
1965
2182
|
<div class="key-row">
|
|
1966
2183
|
<button class="key-btn modifier" id="ctrl-btn" title="Toggle Ctrl modifier">Ctrl</button>
|
|
1967
2184
|
<button class="key-btn modifier" id="shift-btn" title="Toggle Shift modifier">Shift</button>
|
|
1968
|
-
<button class="key-btn special" data-key="
|
|
2185
|
+
<button class="key-btn special" data-key="tab" title="Autocomplete">Tab</button>
|
|
1969
2186
|
<button class="key-btn special key-danger" data-key="" title="Interrupt process">
|
|
1970
2187
|
^C
|
|
1971
2188
|
</button>
|
|
@@ -2212,9 +2429,29 @@
|
|
|
2212
2429
|
</div>
|
|
2213
2430
|
</div>
|
|
2214
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
|
+
|
|
2215
2451
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
2216
2452
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
2217
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>
|
|
2218
2455
|
<script>
|
|
2219
2456
|
// ===== Constants =====
|
|
2220
2457
|
const SESSION_COLORS = [
|
|
@@ -2235,6 +2472,33 @@
|
|
|
2235
2472
|
|
|
2236
2473
|
let splitSecondId = null;
|
|
2237
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
|
+
|
|
2238
2502
|
// Clipboard copy fallback for non-secure contexts (HTTP over LAN)
|
|
2239
2503
|
function copyFallback(text) {
|
|
2240
2504
|
const ta = document.createElement('textarea');
|
|
@@ -2592,10 +2856,98 @@
|
|
|
2592
2856
|
el.addEventListener('click', (e) => {
|
|
2593
2857
|
e.stopPropagation();
|
|
2594
2858
|
applyTheme(el.dataset.themeOption);
|
|
2595
|
-
document.getElementById('theme-picker').classList.remove('open');
|
|
2596
2859
|
});
|
|
2597
2860
|
});
|
|
2598
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
|
+
}
|
|
2949
|
+
});
|
|
2950
|
+
|
|
2599
2951
|
// ===== Font Loading (non-blocking) =====
|
|
2600
2952
|
const nerdFont = new FontFace(
|
|
2601
2953
|
'NerdFont',
|
|
@@ -2731,12 +3083,6 @@
|
|
|
2731
3083
|
loadShellsForModal();
|
|
2732
3084
|
startPolling();
|
|
2733
3085
|
|
|
2734
|
-
// Zoom
|
|
2735
|
-
document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
|
|
2736
|
-
document
|
|
2737
|
-
.getElementById('zoom-out')
|
|
2738
|
-
.addEventListener('click', () => applyZoom(fontSize - 2));
|
|
2739
|
-
|
|
2740
3086
|
// Pinch-to-zoom
|
|
2741
3087
|
(function setupPinchZoom() {
|
|
2742
3088
|
const wrapper = document.getElementById('terminals-wrapper');
|
|
@@ -2864,9 +3210,6 @@
|
|
|
2864
3210
|
);
|
|
2865
3211
|
}
|
|
2866
3212
|
|
|
2867
|
-
// Split toggle
|
|
2868
|
-
document.getElementById('split-toggle').addEventListener('click', toggleSplit);
|
|
2869
|
-
|
|
2870
3213
|
// Scroll to bottom when returning from idle / tab switch
|
|
2871
3214
|
document.addEventListener('visibilitychange', () => {
|
|
2872
3215
|
if (!document.hidden && activeId) {
|
|
@@ -2914,7 +3257,7 @@
|
|
|
2914
3257
|
fetch('/api/version')
|
|
2915
3258
|
.then((r) => r.json())
|
|
2916
3259
|
.then((d) => {
|
|
2917
|
-
|
|
3260
|
+
window._termbeamVersion = 'v' + d.version;
|
|
2918
3261
|
document.getElementById('side-panel-version').textContent = 'v' + d.version;
|
|
2919
3262
|
})
|
|
2920
3263
|
.catch(() => {});
|
|
@@ -2923,6 +3266,7 @@
|
|
|
2923
3266
|
// ===== Session Management =====
|
|
2924
3267
|
function addSession(data) {
|
|
2925
3268
|
if (managed.has(data.id)) return;
|
|
3269
|
+
managed.set(data.id, null); // reserve slot to prevent race condition
|
|
2926
3270
|
|
|
2927
3271
|
const term = new window.Terminal({
|
|
2928
3272
|
cursorBlink: true,
|
|
@@ -2941,8 +3285,10 @@
|
|
|
2941
3285
|
|
|
2942
3286
|
const fitAddon = new window.FitAddon.FitAddon();
|
|
2943
3287
|
const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
|
|
3288
|
+
const searchAddon = new window.SearchAddon.SearchAddon();
|
|
2944
3289
|
term.loadAddon(fitAddon);
|
|
2945
3290
|
term.loadAddon(webLinksAddon);
|
|
3291
|
+
term.loadAddon(searchAddon);
|
|
2946
3292
|
|
|
2947
3293
|
const container = document.createElement('div');
|
|
2948
3294
|
container.className = 'terminal-pane';
|
|
@@ -3092,6 +3438,7 @@
|
|
|
3092
3438
|
pid: data.pid,
|
|
3093
3439
|
term,
|
|
3094
3440
|
fitAddon,
|
|
3441
|
+
searchAddon,
|
|
3095
3442
|
container,
|
|
3096
3443
|
coalescedWrite,
|
|
3097
3444
|
scrollBtn,
|
|
@@ -3100,6 +3447,7 @@
|
|
|
3100
3447
|
reconnectTimer: null,
|
|
3101
3448
|
reconnectDelay: 3000,
|
|
3102
3449
|
lastActivity: data.lastActivity || Date.now(),
|
|
3450
|
+
silenceTimer: null,
|
|
3103
3451
|
};
|
|
3104
3452
|
|
|
3105
3453
|
managed.set(data.id, ms);
|
|
@@ -3211,6 +3559,7 @@
|
|
|
3211
3559
|
if (msg.type === 'output') {
|
|
3212
3560
|
ms.coalescedWrite(msg.data);
|
|
3213
3561
|
ms.lastActivity = Date.now();
|
|
3562
|
+
resetSilenceTimer(ms);
|
|
3214
3563
|
markUnreadOutput();
|
|
3215
3564
|
if (ms.id !== activeId && !ms.hasUnread) {
|
|
3216
3565
|
ms.hasUnread = true;
|
|
@@ -3331,6 +3680,10 @@
|
|
|
3331
3680
|
clearTimeout(ms.reconnectTimer);
|
|
3332
3681
|
ms.reconnectTimer = null;
|
|
3333
3682
|
}
|
|
3683
|
+
if (ms.silenceTimer) {
|
|
3684
|
+
clearTimeout(ms.silenceTimer);
|
|
3685
|
+
ms.silenceTimer = null;
|
|
3686
|
+
}
|
|
3334
3687
|
if (ms.ws)
|
|
3335
3688
|
try {
|
|
3336
3689
|
ms.ws.close();
|
|
@@ -3717,7 +4070,6 @@
|
|
|
3717
4070
|
// ===== Split View =====
|
|
3718
4071
|
function toggleSplit() {
|
|
3719
4072
|
splitMode = !splitMode;
|
|
3720
|
-
document.getElementById('split-toggle').classList.toggle('active', splitMode);
|
|
3721
4073
|
|
|
3722
4074
|
if (splitMode) {
|
|
3723
4075
|
// Find a second session to show
|
|
@@ -3804,7 +4156,12 @@
|
|
|
3804
4156
|
function sendKey(btn) {
|
|
3805
4157
|
if (!btn || !btn.dataset.key) return;
|
|
3806
4158
|
flashBtn(btn);
|
|
3807
|
-
let data =
|
|
4159
|
+
let data =
|
|
4160
|
+
btn.dataset.key === 'enter'
|
|
4161
|
+
? '\r'
|
|
4162
|
+
: btn.dataset.key === 'tab'
|
|
4163
|
+
? '\x09'
|
|
4164
|
+
: btn.dataset.key;
|
|
3808
4165
|
data = applyModifiers(data);
|
|
3809
4166
|
const ms = managed.get(activeId);
|
|
3810
4167
|
if (ms && ms.ws && ms.ws.readyState === 1) {
|
|
@@ -4337,6 +4694,18 @@
|
|
|
4337
4694
|
if (cmd) body.initialCommand = cmd;
|
|
4338
4695
|
if (color) body.color = color;
|
|
4339
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
|
+
|
|
4340
4709
|
try {
|
|
4341
4710
|
const res = await fetch('/api/sessions', {
|
|
4342
4711
|
method: 'POST',
|
|
@@ -4391,6 +4760,10 @@
|
|
|
4391
4760
|
clearTimeout(ms.reconnectTimer);
|
|
4392
4761
|
ms.reconnectTimer = null;
|
|
4393
4762
|
}
|
|
4763
|
+
if (ms.silenceTimer) {
|
|
4764
|
+
clearTimeout(ms.silenceTimer);
|
|
4765
|
+
ms.silenceTimer = null;
|
|
4766
|
+
}
|
|
4394
4767
|
if (ms.ws)
|
|
4395
4768
|
try {
|
|
4396
4769
|
ms.ws.close();
|
|
@@ -4524,12 +4897,11 @@
|
|
|
4524
4897
|
});
|
|
4525
4898
|
}
|
|
4526
4899
|
|
|
4527
|
-
|
|
4900
|
+
async function shareLink() {
|
|
4528
4901
|
const urlPromise = fetch('/api/share-token')
|
|
4529
4902
|
.then((r) => (r.ok ? r.json() : null))
|
|
4530
4903
|
.then((data) => (data && data.url) || location.href)
|
|
4531
4904
|
.catch(() => location.href);
|
|
4532
|
-
// ClipboardItem with a promise preserves user activation across the fetch
|
|
4533
4905
|
if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
|
|
4534
4906
|
try {
|
|
4535
4907
|
const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
|
|
@@ -4538,7 +4910,6 @@
|
|
|
4538
4910
|
return;
|
|
4539
4911
|
} catch {}
|
|
4540
4912
|
}
|
|
4541
|
-
// Fallback: resolve URL first, then try legacy methods
|
|
4542
4913
|
const url = await urlPromise;
|
|
4543
4914
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
4544
4915
|
try {
|
|
@@ -4552,10 +4923,9 @@
|
|
|
4552
4923
|
} else {
|
|
4553
4924
|
showShareUrlPrompt(url);
|
|
4554
4925
|
}
|
|
4555
|
-
}
|
|
4926
|
+
}
|
|
4556
4927
|
|
|
4557
|
-
|
|
4558
|
-
document.getElementById('refresh-btn').addEventListener('click', async () => {
|
|
4928
|
+
async function refreshApp() {
|
|
4559
4929
|
if ('caches' in window) {
|
|
4560
4930
|
const keys = await caches.keys();
|
|
4561
4931
|
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
@@ -4565,7 +4935,294 @@
|
|
|
4565
4935
|
if (reg) await reg.update();
|
|
4566
4936
|
}
|
|
4567
4937
|
location.reload();
|
|
4568
|
-
}
|
|
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
|
+
})();
|
|
4569
5226
|
|
|
4570
5227
|
// ===== Service Worker =====
|
|
4571
5228
|
if ('serviceWorker' in navigator) {
|
package/src/routes.js
CHANGED
|
@@ -114,7 +114,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
app.post('/api/sessions', auth.middleware, (req, res) => {
|
|
117
|
-
const { name, shell, args: shellArgs, cwd, initialCommand, color } = req.body || {};
|
|
117
|
+
const { name, shell, args: shellArgs, cwd, initialCommand, color, cols, rows } = req.body || {};
|
|
118
118
|
|
|
119
119
|
// Validate shell field
|
|
120
120
|
if (shell) {
|
|
@@ -146,6 +146,8 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
146
146
|
cwd: cwd || config.cwd,
|
|
147
147
|
initialCommand: initialCommand || null,
|
|
148
148
|
color: color || null,
|
|
149
|
+
cols: typeof cols === 'number' && cols > 0 && cols <= 500 ? Math.floor(cols) : undefined,
|
|
150
|
+
rows: typeof rows === 'number' && rows > 0 && rows <= 200 ? Math.floor(rows) : undefined,
|
|
149
151
|
});
|
|
150
152
|
res.json({ id, url: `/terminal?id=${id}` });
|
|
151
153
|
});
|
|
@@ -153,7 +155,9 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
153
155
|
// Available shells
|
|
154
156
|
app.get('/api/shells', auth.middleware, (_req, res) => {
|
|
155
157
|
const shells = detectShells();
|
|
156
|
-
|
|
158
|
+
const ds = config.defaultShell;
|
|
159
|
+
const match = shells.find((s) => s.cmd === ds || s.path === ds || s.name === ds);
|
|
160
|
+
res.json({ shells, default: match ? match.cmd : ds, cwd: config.cwd });
|
|
157
161
|
});
|
|
158
162
|
|
|
159
163
|
app.get('/api/sessions/:id/detect-port', auth.middleware, (req, res) => {
|
package/src/sessions.js
CHANGED
|
@@ -18,15 +18,24 @@ class SessionManager {
|
|
|
18
18
|
this.sessions = new Map();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
create({
|
|
21
|
+
create({
|
|
22
|
+
name,
|
|
23
|
+
shell,
|
|
24
|
+
args = [],
|
|
25
|
+
cwd,
|
|
26
|
+
initialCommand = null,
|
|
27
|
+
color = null,
|
|
28
|
+
cols = 120,
|
|
29
|
+
rows = 30,
|
|
30
|
+
}) {
|
|
22
31
|
const id = crypto.randomBytes(16).toString('hex');
|
|
23
32
|
if (!color) {
|
|
24
33
|
color = SESSION_COLORS[this.sessions.size % SESSION_COLORS.length];
|
|
25
34
|
}
|
|
26
35
|
const ptyProcess = pty.spawn(shell, args, {
|
|
27
36
|
name: 'xterm-256color',
|
|
28
|
-
cols
|
|
29
|
-
rows
|
|
37
|
+
cols,
|
|
38
|
+
rows,
|
|
30
39
|
cwd,
|
|
31
40
|
env: { ...process.env, TERM: 'xterm-256color' },
|
|
32
41
|
});
|
|
@@ -47,6 +56,9 @@ class SessionManager {
|
|
|
47
56
|
clients: new Set(),
|
|
48
57
|
scrollback: [],
|
|
49
58
|
scrollbackBuf: '',
|
|
59
|
+
hasHadClient: false,
|
|
60
|
+
_lastCols: cols,
|
|
61
|
+
_lastRows: rows,
|
|
50
62
|
};
|
|
51
63
|
|
|
52
64
|
ptyProcess.onData((data) => {
|
package/src/websocket.js
CHANGED
|
@@ -77,9 +77,16 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
attached = session;
|
|
80
|
-
session.clients
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// First client: defer adding to session.clients until after the
|
|
81
|
+
// first resize so we can decide whether the PTY needs resizing.
|
|
82
|
+
if (!session.hasHadClient) {
|
|
83
|
+
session.hasHadClient = true;
|
|
84
|
+
ws._pendingResize = true;
|
|
85
|
+
} else {
|
|
86
|
+
session.clients.add(ws);
|
|
87
|
+
if (session.scrollbackBuf.length > 0) {
|
|
88
|
+
ws.send(JSON.stringify({ type: 'output', data: session.scrollbackBuf }));
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
ws.send(JSON.stringify({ type: 'attached', sessionId: msg.sessionId }));
|
|
85
92
|
log.info(`Client attached to session ${msg.sessionId}`);
|
|
@@ -95,7 +102,27 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
95
102
|
const rows = Math.floor(msg.rows);
|
|
96
103
|
if (cols > 0 && cols <= 500 && rows > 0 && rows <= 200) {
|
|
97
104
|
ws._dims = { cols, rows };
|
|
98
|
-
|
|
105
|
+
if (ws._pendingResize) {
|
|
106
|
+
ws._pendingResize = false;
|
|
107
|
+
// Only discard scrollback and send SIGWINCH if the PTY was
|
|
108
|
+
// spawned at a different size (e.g. default 120×30).
|
|
109
|
+
// If the PTY already matches (new session sent dims in POST),
|
|
110
|
+
// just add the client and replay scrollback — no SIGWINCH,
|
|
111
|
+
// no duplicate prompt from slow themes like oh-my-posh.
|
|
112
|
+
const sizeChanged = cols !== attached._lastCols || rows !== attached._lastRows;
|
|
113
|
+
if (sizeChanged) {
|
|
114
|
+
attached.scrollbackBuf = '';
|
|
115
|
+
attached.clients.add(ws);
|
|
116
|
+
recalcPtySize(attached);
|
|
117
|
+
} else {
|
|
118
|
+
attached.clients.add(ws);
|
|
119
|
+
if (attached.scrollbackBuf.length > 0) {
|
|
120
|
+
ws.send(JSON.stringify({ type: 'output', data: attached.scrollbackBuf }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
recalcPtySize(attached);
|
|
125
|
+
}
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
} catch (err) {
|