webtun 1.5.0 → 1.5.1
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 +123 -223
- package/package.json +1 -1
- package/public/favicon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +1 -0
- package/public/index.html +99 -73
package/README.md
CHANGED
|
@@ -1,237 +1,139 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-

|
|
5
|
-

|
|
6
|
-

|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
**Access your server from any browser — no VPN, no SSH client, no installing anything.**
|
|
10
|
-
|
|
11
|
-
[](install.sh)
|
|
12
|
-
[](https://github.com/unn-known1/webtun/releases/latest)
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Why WebTun?
|
|
17
|
-
|
|
18
|
-
| Traditional SSH | WebTun |
|
|
19
|
-
|----------------|--------|
|
|
20
|
-
| Need SSH client installed | Open any browser |
|
|
21
|
-
| Configure VPN or port forwarding | Cloudflare Tunnel auto-configured |
|
|
22
|
-
| Can't access from phone easily | PWA works on iOS/Android |
|
|
23
|
-
| Share access requires key exchange | Web-based sharing in 1 click |
|
|
24
|
-
| Corporate firewall blocks port 22 | Runs over HTTPS (port 443) |
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Features
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="public/icon.svg" width="80" height="80" alt="WebTun">
|
|
3
|
+
</p>
|
|
29
4
|
|
|
30
|
-
|
|
31
|
-
- **Real shell sessions** — node-pty backed, full bash/zsh support
|
|
32
|
-
- **Multi-tab terminal** — drag tabs to reorder, side-by-side sessions like your desktop
|
|
33
|
-
- **File explorer** — browse, upload, download, rename, delete, cut/copy/paste with conflict resolution (replace, merge, keep both, skip)
|
|
34
|
-
- **WebSocket + xterm.js** — responsive, low-latency typing
|
|
35
|
-
- **Bracketed paste** — Ctrl+V works correctly in TUI apps (vim, nano, htop, mc)
|
|
36
|
-
- **Command history** — automatic capture with smart dedup, works with pasted commands
|
|
37
|
-
|
|
38
|
-
### Mobile-First UX
|
|
39
|
-
- **Touch gestures** — swipe to close tabs, swipe from edge to open sidebar, pull-to-refresh file list
|
|
40
|
-
- **Mobile keyboard bar** — ESC, Tab, arrow keys, Ctrl combos (Ctrl+C, Ctrl+D, etc.)
|
|
41
|
-
- **Virtual keyboard aware** — terminal resizes when keyboard opens, no overlap
|
|
42
|
-
- **Pinch-to-zoom** — adjust terminal font size (10–28px) with two fingers
|
|
43
|
-
- **Selection mode** — toggle between terminal interaction and text selection
|
|
44
|
-
- **Responsive** — optimized for phones (480px), tablets (1024px), and landscape orientation
|
|
45
|
-
|
|
46
|
-
### Settings
|
|
47
|
-
- **Right-click menu toggle** — enable/disable custom terminal context menu (Settings > Terminal)
|
|
48
|
-
- **Terminal scroll** — reliable mouse wheel scroll in all apps, including TUI apps with mouse tracking
|
|
5
|
+
<h1 align="center">WebTun</h1>
|
|
49
6
|
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- **Folder download** — folders download as `.zip` archives automatically
|
|
55
|
-
- **Image viewer** — click images (.png, .jpg, .gif, .svg, .webp, .bmp, .ico) to preview inline
|
|
56
|
-
- **Cut, Copy, Paste** — multi-file support with conflict resolution dialog (replace, merge folders, keep both, skip, cancel)
|
|
57
|
-
- **Upload with progress** — per-file progress bar via XHR
|
|
58
|
-
- **Breadcrumb navigation** — tappable directory segments for quick navigation
|
|
59
|
-
- **Long-press context menu** — rename, delete, download, copy on touch devices
|
|
60
|
-
- **Text selection** — long-press file names to select and copy text on touch devices
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Self-hosted web terminal with file explorer, Cloudflare Tunnel, and PWA support</strong><br>
|
|
9
|
+
Access your server from any browser — no VPN, no SSH client, no installing anything.
|
|
10
|
+
</p>
|
|
61
11
|
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- Runs the server as a child process — no terminal needed
|
|
71
|
-
- Native window with system tray, copy/paste, and file dialogs
|
|
72
|
-
- Download the latest release from [GitHub Releases](https://github.com/unn-known1/webtun/releases/latest)
|
|
73
|
-
- Or build from source: `npm run dist:linux` / `npm run dist:win`
|
|
74
|
-
|
|
75
|
-
### PWA (Install as App)
|
|
76
|
-
- Add to iOS/Android home screen — looks and feels like native app
|
|
77
|
-
- Install banner prompts on first visit (Chrome) with iOS instructions fallback
|
|
78
|
-
- Works offline (shows last session state)
|
|
79
|
-
- Push notifications for tunnel status
|
|
80
|
-
|
|
81
|
-
### Cloudflare Tunnel (Zero-Config)
|
|
82
|
-
- Create tunnel from UI — no CLI commands
|
|
83
|
-
- Built-in health check warns if local server is unreachable before creating tunnel
|
|
84
|
-
- Live/orphan status + target responsiveness indicator in tunnel list
|
|
85
|
-
- Get public HTTPS URL instantly
|
|
86
|
-
- Tunnels survive server restarts
|
|
87
|
-
- Stop/kill tunnels from settings panel
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://github.com/unn-known1/webtun/releases/latest"><img src="https://img.shields.io/github/v/release/unn-known1/webtun?style=flat-square&logo=github" alt="Release"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/Terminal-xterm.js-2D5B8E?style=flat-square" alt="Terminal">
|
|
15
|
+
<img src="https://img.shields.io/badge/PWA-Installable-6BA428?style=flat-square" alt="PWA">
|
|
16
|
+
<img src="https://img.shields.io/badge/Tunnel-Cloudflare-F38020?style=flat-square" alt="Tunnel">
|
|
17
|
+
<img src="https://img.shields.io/badge/Desktop-Electron-47848F?style=flat-square" alt="Electron">
|
|
18
|
+
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
|
|
19
|
+
</p>
|
|
88
20
|
|
|
89
21
|
---
|
|
90
22
|
|
|
91
23
|
## Quick Start
|
|
92
24
|
|
|
93
|
-
### npm (Recommended)
|
|
94
25
|
```bash
|
|
95
26
|
npx webtun
|
|
96
27
|
```
|
|
97
28
|
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
npm install -g webtun
|
|
101
|
-
webtun
|
|
102
|
-
```
|
|
29
|
+
That's it. Open `http://localhost:3000` in your browser.
|
|
103
30
|
|
|
104
|
-
**Options:**
|
|
105
|
-
| Flag | Description |
|
|
106
|
-
|------|-------------|
|
|
107
|
-
| `--port, -p` | Port (default: 3000) |
|
|
108
|
-
| `--host, -h` | Host (default: 0.0.0.0) |
|
|
109
|
-
| `--pin` | Authentication PIN |
|
|
110
|
-
| `--tunnel, -t` | Start Cloudflare Tunnel |
|
|
111
|
-
| `--help` | Show help |
|
|
112
|
-
| `--version` | Show version |
|
|
113
|
-
|
|
114
|
-
**Environment Variables:**
|
|
115
|
-
| Variable | Description |
|
|
116
|
-
|----------|-------------|
|
|
117
|
-
| `PORT` | Server port (default: 3000) |
|
|
118
|
-
| `HOST` | Bind address (default: 0.0.0.0) |
|
|
119
|
-
| `PIN` | Authentication PIN (empty = no auth) |
|
|
120
|
-
| `SHELL` | Shell to use (default: PowerShell on Windows, bash/sh elsewhere) |
|
|
121
|
-
| `WORKSPACE_ROOT` | Root directory for file operations (default: ~) |
|
|
122
|
-
| `TRUST_PROXY` | Set to `true` if behind a reverse proxy (default: loopback only) |
|
|
123
|
-
| `WEBTUN_SHELL` | Override shell on Windows (e.g., `/usr/bin/bash` for Git Bash) |
|
|
124
|
-
|
|
125
|
-
**Examples:**
|
|
126
31
|
```bash
|
|
127
|
-
npx webtun # Start on port 3000
|
|
128
|
-
npx webtun --port 8080 # Custom port
|
|
129
32
|
npx webtun --pin secret123 # With PIN protection
|
|
130
|
-
npx webtun --tunnel # With Cloudflare Tunnel
|
|
131
|
-
npx webtun -p
|
|
33
|
+
npx webtun --tunnel # With Cloudflare Tunnel (public URL)
|
|
34
|
+
npx webtun -p 8080 -t # Custom port + tunnel
|
|
35
|
+
npx webtun --help # Show all options
|
|
36
|
+
npx webtun --version # Show version
|
|
132
37
|
```
|
|
133
38
|
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
# Allow scripts once during install
|
|
137
|
-
npm install -g --allow-scripts=webtun,node-pty webtun
|
|
39
|
+
### Install globally
|
|
138
40
|
|
|
139
|
-
|
|
140
|
-
npm config set allow-scripts=webtun,node-pty --location=user
|
|
41
|
+
```bash
|
|
141
42
|
npm install -g webtun
|
|
43
|
+
webtun
|
|
142
44
|
```
|
|
143
45
|
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
|
|
147
|
-
|
|
46
|
+
> **npm 12+ note:** Install scripts are blocked by default. Allow them for `node-pty`:
|
|
47
|
+
> ```bash
|
|
48
|
+
> npm install -g --allow-scripts=webtun,node-pty webtun
|
|
49
|
+
> ```
|
|
50
|
+
> Build tools required for native modules: `sudo apt-get install -y python3 make g++` (Debian/Ubuntu) or `xcode-select --install` (macOS).
|
|
148
51
|
|
|
149
|
-
|
|
150
|
-
xcode-select --install
|
|
151
|
-
```
|
|
52
|
+
### Other install methods
|
|
152
53
|
|
|
153
|
-
### One-Command Install
|
|
154
54
|
```bash
|
|
55
|
+
# One-command install
|
|
155
56
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/unn-Known1/webtun/main/install.sh)"
|
|
156
|
-
```
|
|
157
57
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
git clone https://github.com/unn-Known1/webtun.git
|
|
161
|
-
cd webtun
|
|
162
|
-
chmod +x setup.sh && ./setup.sh
|
|
163
|
-
npm start
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Google Colab (Instant Terminal)
|
|
167
|
-
```python
|
|
168
|
-
!rm -rf webtun && git clone https://github.com/unn-Known1/webtun.git && cd webtun && npm install --loglevel=error && node server.js > /tmp/webtun.log 2>&1 & sleep 4 && for i in 1 2 3; do curl -sf http://localhost:3000/api/auth/required >/dev/null && break; sleep 2; done && curl -s -X POST http://localhost:3000/api/tunnel -H 'Content-Type: application/json' -d '{"url":"http://localhost:3000"}' --max-time 20 | python3 -c "import sys,json; d=json.load(sys.stdin); print('🌐 WebTun ready at:', d.get('url','Error: '+d.get('error','')))"
|
|
58
|
+
# Manual setup
|
|
59
|
+
git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh && npm start
|
|
169
60
|
```
|
|
170
61
|
|
|
171
62
|
---
|
|
172
63
|
|
|
173
|
-
##
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
177
|
-
│ Your Browser │
|
|
178
|
-
│ ┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐ │
|
|
179
|
-
│ │ Terminal │ │ File │ │ Tunnel │ │ Settings │ │
|
|
180
|
-
│ │ (xterm) │ │ Explorer │ │ Manager │ │ Panel │ │
|
|
181
|
-
│ └─────┬────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘ │
|
|
182
|
-
│ │ │ │ │ │
|
|
183
|
-
└────────┼─────────────┼───────────────┼───────────────┼─────────┘
|
|
184
|
-
│ WebSocket │ REST API │ │
|
|
185
|
-
▼ ▼ ▼ │
|
|
186
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
187
|
-
│ Node.js Server │
|
|
188
|
-
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
|
|
189
|
-
│ │ WebSocket │ │ File API │ │ Cloudflare Tunnel │ │
|
|
190
|
-
│ │ Handler │ │ (read/write) │ │ Manager │ │
|
|
191
|
-
│ └──────┬──────┘ └──────┬───────┘ └───────────┬───────────┘ │
|
|
192
|
-
│ │ │ │ │
|
|
193
|
-
│ ▼ ▼ ▼ │
|
|
194
|
-
│ ┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
|
|
195
|
-
│ │ node-pty │ │ Local Filesystem │ │ cloudflared daemon │ │
|
|
196
|
-
│ │ (shell) │ │ (your server) │ │ (exposes to internet)│ │
|
|
197
|
-
│ └──────────┘ └──────────────────┘ └──────────────────────┘ │
|
|
198
|
-
└────────────────────────────────────────────────────────────────┘
|
|
199
|
-
│
|
|
200
|
-
▼
|
|
201
|
-
┌─────────────────┐
|
|
202
|
-
│ Internet Users │
|
|
203
|
-
│ (HTTPS URL) │
|
|
204
|
-
└─────────────────┘
|
|
205
|
-
```
|
|
64
|
+
## Features
|
|
206
65
|
|
|
207
|
-
|
|
66
|
+
### Terminal
|
|
67
|
+
- **Real shell sessions** — node-pty backed, full bash/zsh/PowerShell support
|
|
68
|
+
- **Multi-tab** — drag to reorder, side-by-side sessions
|
|
69
|
+
- **Bracketed paste** — Ctrl+V works in TUI apps (vim, nano, htop)
|
|
70
|
+
- **Command history** — keystroke-based capture, strips ANSI/control sequences
|
|
71
|
+
- **tmux sessions** — persistent sessions survive page reload
|
|
208
72
|
|
|
209
|
-
|
|
73
|
+
### File Explorer
|
|
74
|
+
- Browse, upload, download, rename, delete files
|
|
75
|
+
- **Multi-select** — batch delete, download, zip, cut, copy
|
|
76
|
+
- **Zip/Extract** — right-click any file or folder
|
|
77
|
+
- **Image viewer** — preview inline (png, jpg, gif, svg, webp)
|
|
78
|
+
- **Conflict resolution** — replace, merge, keep both, skip
|
|
79
|
+
- **Breadcrumb navigation** — tappable directory segments
|
|
80
|
+
|
|
81
|
+
### Mobile
|
|
82
|
+
- **Touch gestures** — swipe to close tabs, pull-to-refresh
|
|
83
|
+
- **Mobile keyboard bar** — ESC, Tab, arrows, Ctrl combos
|
|
84
|
+
- **Pinch-to-zoom** — adjust font size with two fingers
|
|
85
|
+
- **Virtual keyboard aware** — no overlap when keyboard opens
|
|
86
|
+
- **Selection mode** — toggle between interaction and text selection
|
|
87
|
+
|
|
88
|
+
### Cloudflare Tunnel
|
|
89
|
+
- Create tunnels from the UI — zero CLI commands
|
|
90
|
+
- Built-in health check before tunnel creation
|
|
91
|
+
- Live status indicators, tunnels survive restarts
|
|
92
|
+
- Stop/kill tunnels from settings panel
|
|
210
93
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
94
|
+
### System Stats
|
|
95
|
+
- CPU, Memory, Disk, Uptime — real-time with progress bars
|
|
96
|
+
- GPU detection (nvidia-smi, system_profiler, WMI)
|
|
97
|
+
- Multi-GPU support with VRAM, utilization, temperature
|
|
98
|
+
- Top processes sorted by CPU
|
|
99
|
+
|
|
100
|
+
### Desktop (Electron)
|
|
101
|
+
- Cross-platform app for Linux, Windows, macOS
|
|
102
|
+
- Native window with system tray and file dialogs
|
|
103
|
+
- Build: `npm run dist:linux` / `npm run dist:win` / `npm run dist:mac`
|
|
104
|
+
- [Download latest release](https://github.com/unn-known1/webtun/releases/latest)
|
|
105
|
+
|
|
106
|
+
### PWA
|
|
107
|
+
- Install to iOS/Android home screen
|
|
108
|
+
- Works offline with last session state
|
|
109
|
+
- Install banner with iOS instructions fallback
|
|
219
110
|
|
|
220
111
|
---
|
|
221
112
|
|
|
222
|
-
##
|
|
113
|
+
## Configuration
|
|
223
114
|
|
|
224
|
-
|
|
225
|
-
2. Go to **Tunnel** section
|
|
226
|
-
3. Enter URL (default: `http://localhost:3000`)
|
|
227
|
-
4. Click **Create** — a health check pings the target first, warns if unreachable
|
|
228
|
-
5. Copy the public URL — share it with anyone
|
|
229
|
-
6. View all tunnels with live status indicators (green/orange)
|
|
115
|
+
### CLI Flags
|
|
230
116
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
117
|
+
| Flag | Description | Default |
|
|
118
|
+
|------|-------------|---------|
|
|
119
|
+
| `--port, -p` | Server port | `3000` |
|
|
120
|
+
| `--host, -h` | Bind address | `0.0.0.0` |
|
|
121
|
+
| `--pin` | Authentication PIN | none |
|
|
122
|
+
| `--tunnel, -t` | Start Cloudflare Tunnel | off |
|
|
123
|
+
| `--help` | Show help | — |
|
|
124
|
+
| `--version` | Show version | — |
|
|
125
|
+
|
|
126
|
+
### Environment Variables
|
|
127
|
+
|
|
128
|
+
| Variable | Description | Default |
|
|
129
|
+
|----------|-------------|---------|
|
|
130
|
+
| `PORT` | Server port | `3000` |
|
|
131
|
+
| `HOST` | Bind address | `0.0.0.0` |
|
|
132
|
+
| `PIN` | Auth PIN (empty = no auth) | none |
|
|
133
|
+
| `SHELL` | Shell to use | platform default |
|
|
134
|
+
| `WORKSPACE_ROOT` | File explorer root | `~` |
|
|
135
|
+
| `TRUST_PROXY` | Trust `X-Forwarded-For` from first proxy | `false` |
|
|
136
|
+
| `WEBTUN_SHELL` | Override shell on Windows | PowerShell |
|
|
235
137
|
|
|
236
138
|
---
|
|
237
139
|
|
|
@@ -239,10 +141,8 @@ kill $(pgrep -f 'cloudflared tunnel')
|
|
|
239
141
|
|
|
240
142
|
| Layer | Technology |
|
|
241
143
|
|-------|------------|
|
|
242
|
-
| Frontend |
|
|
243
|
-
| Backend | Node.js
|
|
244
|
-
| Terminal | xterm.js + fit addon |
|
|
245
|
-
| Protocol | WebSocket (ws) |
|
|
144
|
+
| Frontend | Vanilla JS, xterm.js, CodeMirror, PWA |
|
|
145
|
+
| Backend | Node.js, Express, node-pty |
|
|
246
146
|
| Tunnel | Cloudflare Tunnel (cloudflared) |
|
|
247
147
|
| Desktop | Electron + electron-builder |
|
|
248
148
|
| Archives | archiver v8 + yauzl |
|
|
@@ -251,11 +151,11 @@ kill $(pgrep -f 'cloudflared tunnel')
|
|
|
251
151
|
|
|
252
152
|
## Security
|
|
253
153
|
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
258
|
-
-
|
|
154
|
+
- PIN authentication on all API endpoints (`x-pin-token` header or `?token=` query)
|
|
155
|
+
- CSP headers restrict script sources to CDN
|
|
156
|
+
- WebSocket origin check prevents cross-site hijacking
|
|
157
|
+
- Per-IP rate limiting on auth and search endpoints
|
|
158
|
+
- npm audit clean — 0 known vulnerabilities
|
|
259
159
|
|
|
260
160
|
---
|
|
261
161
|
|
|
@@ -263,31 +163,31 @@ kill $(pgrep -f 'cloudflared tunnel')
|
|
|
263
163
|
|
|
264
164
|
| Problem | Solution |
|
|
265
165
|
|---------|----------|
|
|
266
|
-
| Tunnel
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
270
|
-
| Windows
|
|
271
|
-
|
|
|
272
|
-
| Tunnel persists after server restart | Tunnels are auto-managed; manual cleanup: `kill $(pgrep -f 'cloudflared tunnel')` |
|
|
166
|
+
| Tunnel not loading | Check quota at [dash.cloudflare.com](https://dash.cloudflare.com) |
|
|
167
|
+
| Port 3000 in use | `PORT=3001 npm start` |
|
|
168
|
+
| Shell permission denied | `chsh -s /bin/bash` |
|
|
169
|
+
| Behind reverse proxy | Set `TRUST_PROXY=true` in `.env` |
|
|
170
|
+
| Windows shell wrong | Set `WEBTUN_SHELL=/usr/bin/bash` in `.env` |
|
|
171
|
+
| Tunnel persists after restart | `kill $(pgrep -f 'cloudflared tunnel')` |
|
|
273
172
|
|
|
274
173
|
---
|
|
275
174
|
|
|
276
|
-
##
|
|
277
|
-
|
|
278
|
-
1. Fork → Branch → Commit → PR
|
|
279
|
-
2. Follow existing code style (ES6+, no frameworks)
|
|
280
|
-
3. Test locally with `npm start`
|
|
281
|
-
4. Update this README if adding features
|
|
282
|
-
|
|
283
|
-
---
|
|
175
|
+
## Changelog
|
|
284
176
|
|
|
285
|
-
|
|
177
|
+
### v1.5.1
|
|
178
|
+
- Fixed command history saving terminal garbage instead of actual commands
|
|
179
|
+
- Fixed history 401 errors on Cloudflare tunnel URLs
|
|
180
|
+
- Fixed VT/ANSI parameter sequences leaking into history
|
|
181
|
+
- Fixed CodeMirror `defineSimpleMode` error
|
|
182
|
+
- Fixed console accessibility warnings (labels, form fields)
|
|
183
|
+
- Added safe `localStorage` wrapper for Tracking Prevention compatibility
|
|
184
|
+
- New terminal icon matching in-app logo
|
|
286
185
|
|
|
287
|
-
|
|
186
|
+
### v1.5.0
|
|
187
|
+
- Initial release
|
|
288
188
|
|
|
289
189
|
---
|
|
290
190
|
|
|
291
191
|
<p align="center">
|
|
292
|
-
<sub>
|
|
192
|
+
<sub>Built by <a href="https://github.com/unn-known1">Gaurang Patel</a> · MIT License</sub>
|
|
293
193
|
</p>
|
package/package.json
CHANGED
|
Binary file
|
package/public/icon-192.png
CHANGED
|
Binary file
|
package/public/icon-512.png
CHANGED
|
Binary file
|
package/public/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#c0caf5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"></rect><line x1="6" y1="8" x2="10" y2="8"></line><line x1="6" y1="12" x2="10" y2="12"></line><line x1="12" y1="8" x2="18" y2="8"></line><line x1="12" y1="12" x2="18" y2="12"></line></svg>
|
package/public/index.html
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
6
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
7
8
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
9
|
<meta name="theme-color" content="#1a1b26">
|
|
9
10
|
<title>WebTun</title>
|
|
11
|
+
<link rel="icon" href="/favicon.png" type="image/png">
|
|
10
12
|
<link rel="manifest" href="/manifest.json">
|
|
11
13
|
<link rel="apple-touch-icon" href="/icon-192.png">
|
|
12
14
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/yaml/yaml.min.js"></script>
|
|
33
35
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/sql/sql.min.js"></script>
|
|
34
36
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/go/go.min.js"></script>
|
|
37
|
+
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/addon/mode/simple.min.js"></script>
|
|
35
38
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/rust/rust.min.js"></script>
|
|
36
39
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/mode/properties/properties.min.js"></script>
|
|
37
40
|
<style>
|
|
@@ -857,9 +860,15 @@ svg { display: block; }
|
|
|
857
860
|
</head>
|
|
858
861
|
<body data-theme="light">
|
|
859
862
|
<script>
|
|
863
|
+
// Safe localStorage wrapper — degrades gracefully when Tracking Prevention blocks storage
|
|
864
|
+
const safeStorage = {
|
|
865
|
+
getItem(k) { try { return localStorage.getItem(k); } catch(e) { return null; } },
|
|
866
|
+
setItem(k, v) { try { localStorage.setItem(k, v); } catch(e) {} },
|
|
867
|
+
removeItem(k) { try { localStorage.removeItem(k); } catch(e) {} }
|
|
868
|
+
};
|
|
860
869
|
(function() {
|
|
861
870
|
try {
|
|
862
|
-
const settings = JSON.parse(
|
|
871
|
+
const settings = JSON.parse(safeStorage.getItem('wt-settings')) || {};
|
|
863
872
|
let theme = settings.theme || 'light';
|
|
864
873
|
if (theme === 'system') {
|
|
865
874
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dracula' : 'light';
|
|
@@ -1061,7 +1070,7 @@ svg { display: block; }
|
|
|
1061
1070
|
</div>
|
|
1062
1071
|
<div id="editor-area">
|
|
1063
1072
|
<textarea id="editor-textarea" spellcheck="false"></textarea>
|
|
1064
|
-
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-same-origin"></iframe></div>
|
|
1073
|
+
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-same-origin allow-scripts"></iframe></div>
|
|
1065
1074
|
</div>
|
|
1066
1075
|
</div>
|
|
1067
1076
|
</div>
|
|
@@ -1143,7 +1152,7 @@ svg { display: block; }
|
|
|
1143
1152
|
<div class="settings-section">
|
|
1144
1153
|
<h3>Appearance</h3>
|
|
1145
1154
|
<div class="setting-row">
|
|
1146
|
-
<label>Theme</label>
|
|
1155
|
+
<label for="s-theme">Theme</label>
|
|
1147
1156
|
<select id="s-theme" onchange="applyTheme(this.value)">
|
|
1148
1157
|
<option value="system">System</option>
|
|
1149
1158
|
<option value="light">Light</option>
|
|
@@ -1155,11 +1164,11 @@ svg { display: block; }
|
|
|
1155
1164
|
</select>
|
|
1156
1165
|
</div>
|
|
1157
1166
|
<div class="setting-row">
|
|
1158
|
-
<label>Font Size</label>
|
|
1167
|
+
<label for="s-fontsize">Font Size</label>
|
|
1159
1168
|
<input type="number" id="s-fontsize" min="8" max="32" value="14" onchange="applyFontSize(+this.value)">
|
|
1160
1169
|
</div>
|
|
1161
1170
|
<div class="setting-row">
|
|
1162
|
-
<label>Font Family</label>
|
|
1171
|
+
<label for="s-font">Font Family</label>
|
|
1163
1172
|
<select id="s-font" onchange="applyFont(this.value)">
|
|
1164
1173
|
<option value="'SF Mono', 'Fira Code', Consolas, monospace">System Mono</option>
|
|
1165
1174
|
<option value="'Fira Code', monospace">Fira Code</option>
|
|
@@ -1169,7 +1178,7 @@ svg { display: block; }
|
|
|
1169
1178
|
</select>
|
|
1170
1179
|
</div>
|
|
1171
1180
|
<div class="setting-row">
|
|
1172
|
-
<label>Cursor Style</label>
|
|
1181
|
+
<label for="s-cursor">Cursor Style</label>
|
|
1173
1182
|
<select id="s-cursor" onchange="applyCursor(this.value)">
|
|
1174
1183
|
<option value="block">Block</option>
|
|
1175
1184
|
<option value="underline">Underline</option>
|
|
@@ -1177,48 +1186,48 @@ svg { display: block; }
|
|
|
1177
1186
|
</select>
|
|
1178
1187
|
</div>
|
|
1179
1188
|
<div class="setting-row">
|
|
1180
|
-
<label>Cursor Blink</label>
|
|
1181
|
-
<div class="toggle" id="s-blink" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('blink')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('blink')}"></div>
|
|
1189
|
+
<label for="s-blink-cb">Cursor Blink</label>
|
|
1190
|
+
<div class="toggle" id="s-blink" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('blink')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('blink')}"><input type="checkbox" id="s-blink-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1182
1191
|
</div>
|
|
1183
1192
|
</div>
|
|
1184
1193
|
<div class="settings-section">
|
|
1185
1194
|
<h3>Terminal</h3>
|
|
1186
1195
|
<div class="setting-row">
|
|
1187
|
-
<label>Scrollback Lines</label>
|
|
1196
|
+
<label for="s-scrollback">Scrollback Lines</label>
|
|
1188
1197
|
<input type="number" id="s-scrollback" min="100" max="100000" value="5000" onchange="applyScrollback(+this.value)">
|
|
1189
1198
|
</div>
|
|
1190
1199
|
<div class="setting-row">
|
|
1191
|
-
<label>Bell Sound</label>
|
|
1192
|
-
<div class="toggle" id="s-bell" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('bell')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('bell')}"></div>
|
|
1200
|
+
<label for="s-bell-cb">Bell Sound</label>
|
|
1201
|
+
<div class="toggle" id="s-bell" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('bell')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('bell')}"><input type="checkbox" id="s-bell-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1193
1202
|
</div>
|
|
1194
1203
|
<div class="setting-row">
|
|
1195
|
-
<label>Right-Click Menu</label>
|
|
1196
|
-
<div class="toggle on" id="s-termRightClick" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('termRightClick')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('termRightClick')}"></div>
|
|
1204
|
+
<label for="s-termRightClick-cb">Right-Click Menu</label>
|
|
1205
|
+
<div class="toggle on" id="s-termRightClick" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('termRightClick')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('termRightClick')}"><input type="checkbox" id="s-termRightClick-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1197
1206
|
</div>
|
|
1198
1207
|
</div>
|
|
1199
1208
|
<div class="settings-section">
|
|
1200
1209
|
<h3>Interface</h3>
|
|
1201
1210
|
<div class="setting-row">
|
|
1202
|
-
<label>Show Mobile Keys</label>
|
|
1203
|
-
<div class="toggle" id="s-mobilekeys" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('mobilekeys')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('mobilekeys')}"></div>
|
|
1211
|
+
<label for="s-mobilekeys-cb">Show Mobile Keys</label>
|
|
1212
|
+
<div class="toggle" id="s-mobilekeys" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('mobilekeys')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('mobilekeys')}"><input type="checkbox" id="s-mobilekeys-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1204
1213
|
</div>
|
|
1205
1214
|
<div class="setting-row">
|
|
1206
|
-
<label>Confirm on Close</label>
|
|
1207
|
-
<div class="toggle on" id="s-confirmclose" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('confirmclose')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('confirmclose')}"></div>
|
|
1215
|
+
<label for="s-confirmclose-cb">Confirm on Close</label>
|
|
1216
|
+
<div class="toggle on" id="s-confirmclose" role="switch" tabindex="0" aria-checked="true" onclick="toggleSetting('confirmclose')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('confirmclose')}"><input type="checkbox" id="s-confirmclose-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1208
1217
|
</div>
|
|
1209
1218
|
<div class="setting-row">
|
|
1210
|
-
<label>Data Saver</label>
|
|
1211
|
-
<div class="toggle" id="s-datasaver" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('datasaver')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('datasaver')}"></div>
|
|
1219
|
+
<label for="s-datasaver-cb">Data Saver</label>
|
|
1220
|
+
<div class="toggle" id="s-datasaver" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('datasaver')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('datasaver')}"><input type="checkbox" id="s-datasaver-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1212
1221
|
</div>
|
|
1213
1222
|
<div class="setting-row electron-only" style="display:none">
|
|
1214
|
-
<label>Start on Login</label>
|
|
1215
|
-
<div class="toggle" id="s-autostart" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('autostart')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('autostart')}"></div>
|
|
1223
|
+
<label for="s-autostart-cb">Start on Login</label>
|
|
1224
|
+
<div class="toggle" id="s-autostart" role="switch" tabindex="0" aria-checked="false" onclick="toggleSetting('autostart')" onkeydown="if(event.key===' '||event.key==='Enter'){event.preventDefault();toggleSetting('autostart')}"><input type="checkbox" id="s-autostart-cb" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0"></div>
|
|
1216
1225
|
</div>
|
|
1217
1226
|
</div>
|
|
1218
1227
|
<div class="settings-section">
|
|
1219
1228
|
<h3>Tunnel</h3>
|
|
1220
1229
|
<div class="setting-row" style="flex-wrap:wrap;gap:6px">
|
|
1221
|
-
<label style="flex:0 0 auto">Local URL</label>
|
|
1230
|
+
<label for="tunnel-url" style="flex:0 0 auto">Local URL</label>
|
|
1222
1231
|
<div style="display:flex;flex:1;gap:6px;min-width:0">
|
|
1223
1232
|
<input type="text" id="tunnel-url" value="http://localhost:3000" placeholder="http://localhost:PORT" style="flex:1;min-width:0">
|
|
1224
1233
|
<button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:12px;flex-shrink:0" onclick="createTunnel()">Create</button>
|
|
@@ -1510,12 +1519,12 @@ function preventDoubleTap(el, fn, delay = 1000) {
|
|
|
1510
1519
|
|
|
1511
1520
|
function saveTabState() {
|
|
1512
1521
|
const state = tabs.map(t => ({ id: t.id, sessionId: t.sessionId, title: t.title }));
|
|
1513
|
-
try {
|
|
1522
|
+
try { safeStorage.setItem('wt-tabs', JSON.stringify(state)); } catch(e) { console.warn('Failed to save tabs:', e); }
|
|
1514
1523
|
}
|
|
1515
1524
|
|
|
1516
1525
|
function loadTabState() {
|
|
1517
1526
|
try {
|
|
1518
|
-
const val = JSON.parse(
|
|
1527
|
+
const val = JSON.parse(safeStorage.getItem('wt-tabs'));
|
|
1519
1528
|
return Array.isArray(val) ? val : [];
|
|
1520
1529
|
} catch(e) { console.warn('loadTabState error:', e); return []; }
|
|
1521
1530
|
}
|
|
@@ -1738,6 +1747,8 @@ function setupTabDragDrop(tabEl, id) {
|
|
|
1738
1747
|
function setupTabInlineRename(tabTitleSpan, tab) {
|
|
1739
1748
|
const startRename = (span) => {
|
|
1740
1749
|
const input = document.createElement('input');
|
|
1750
|
+
input.id = 'tab-rename-input';
|
|
1751
|
+
input.name = 'tab-rename';
|
|
1741
1752
|
input.type = 'text';
|
|
1742
1753
|
input.value = tab.title;
|
|
1743
1754
|
input.style.cssText = `width:${Math.max(60, tab.title.length * 9)}px;height:22px;font-size:12px;padding:0 6px`;
|
|
@@ -1912,7 +1923,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1912
1923
|
const ch = data.charCodeAt(i);
|
|
1913
1924
|
if (ch === 13 || ch === 10) { // Enter
|
|
1914
1925
|
try {
|
|
1915
|
-
// Prefer stored paste text
|
|
1926
|
+
// Prefer stored paste text
|
|
1916
1927
|
let text = tab._lastPasteText;
|
|
1917
1928
|
tab._lastPasteText = null;
|
|
1918
1929
|
if (text) {
|
|
@@ -1921,30 +1932,13 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1921
1932
|
text = lines[lines.length - 1] || lines[lines.length - 2] || text;
|
|
1922
1933
|
text = text.trim();
|
|
1923
1934
|
} else {
|
|
1924
|
-
//
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
// Scan upward to find the prompt line (contains $, #, or %)
|
|
1929
|
-
for (let r = cursorRow; r >= Math.max(0, cursorRow - 20); r--) {
|
|
1930
|
-
const row = buf.getLine(r);
|
|
1931
|
-
if (!row) break;
|
|
1932
|
-
const rowText = row.translateToString(false);
|
|
1933
|
-
// Prompt detection: line ends with $, #, or % followed by space
|
|
1934
|
-
if (/\s*[#$%]\s*$/.test(rowText)) {
|
|
1935
|
-
const promptEnd = rowText.search(/[#$%]\s*$/);
|
|
1936
|
-
if (promptEnd >= 0) {
|
|
1937
|
-
collected = rowText.substring(promptEnd + 1) + collected;
|
|
1938
|
-
}
|
|
1939
|
-
break;
|
|
1940
|
-
}
|
|
1941
|
-
collected = rowText + collected;
|
|
1942
|
-
}
|
|
1943
|
-
text = collected.trim();
|
|
1944
|
-
// Strip any trailing prompt artifacts from multi-row
|
|
1945
|
-
text = text.replace(/\s*[#$%]\s*$/, '').trim();
|
|
1935
|
+
// Use the tracked input buffer (more reliable than buffer scanning)
|
|
1936
|
+
text = (tab._currentInput || '').trim();
|
|
1937
|
+
// Strip any leaked VT/ANSI parameter junk that bypassed the escape parser
|
|
1938
|
+
text = text.replace(/^[>;\d\s]+(?=[a-zA-Z/\\~\-.])/, '').replace(/^[>;\d\s]+$/, '').trim();
|
|
1946
1939
|
}
|
|
1947
1940
|
if (text) addToCmdHist(text);
|
|
1941
|
+
tab._currentInput = ''; // Clear after history save
|
|
1948
1942
|
} catch {}
|
|
1949
1943
|
}
|
|
1950
1944
|
}
|
|
@@ -1952,6 +1946,27 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1952
1946
|
const buf = new Uint8Array(1 + enc.length);
|
|
1953
1947
|
buf[0] = 0x00; buf.set(enc, 1);
|
|
1954
1948
|
ws.send(buf.buffer);
|
|
1949
|
+
|
|
1950
|
+
// Update the keystroke buffer — skip all escape sequences and control characters
|
|
1951
|
+
for (let i = 0; i < data.length; i++) {
|
|
1952
|
+
const ch = data.charCodeAt(i);
|
|
1953
|
+
if (ch === 13 || ch === 10) { // Enter (handled above)
|
|
1954
|
+
continue;
|
|
1955
|
+
} else if (ch === 127 || ch === 8) { // Backspace / Delete
|
|
1956
|
+
if (tab._currentInput) tab._currentInput = tab._currentInput.slice(0, -1);
|
|
1957
|
+
} else if (ch === 27) { // ESC — skip entire escape sequence (CSI, SS3, OSC, DCS, etc.)
|
|
1958
|
+
i++; // skip first char after ESC (usually '[', 'O', ']', 'P')
|
|
1959
|
+
// Skip until we find a final char or run out of data
|
|
1960
|
+
while (i < data.length) {
|
|
1961
|
+
const c = data.charCodeAt(i);
|
|
1962
|
+
if (c >= 0x40 && c <= 0x7E) { i++; break; } // final char — sequence done
|
|
1963
|
+
i++;
|
|
1964
|
+
}
|
|
1965
|
+
} else if (ch >= 32 && ch < 0x7F) { // Printable ASCII only
|
|
1966
|
+
tab._currentInput = (tab._currentInput || '') + data[i];
|
|
1967
|
+
}
|
|
1968
|
+
// All other control chars (0x00-0x1F except 0x0D/0x0A) are silently dropped
|
|
1969
|
+
}
|
|
1955
1970
|
};
|
|
1956
1971
|
const sendResize = (cols, rows) => {
|
|
1957
1972
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -2185,6 +2200,10 @@ function initTerminal(tab) {
|
|
|
2185
2200
|
tab.searchAddon = searchAddon;
|
|
2186
2201
|
tab.closed = false;
|
|
2187
2202
|
tab.textarea = term.textarea || term.element?.querySelector('.xterm-textarea, textarea');
|
|
2203
|
+
if (tab.textarea && !tab.textarea.id) {
|
|
2204
|
+
tab.textarea.id = 'xterm-helper-' + tab.id;
|
|
2205
|
+
tab.textarea.name = 'terminal-input';
|
|
2206
|
+
}
|
|
2188
2207
|
|
|
2189
2208
|
// Intercept paste events to use bracketed paste mode (required for TUI apps like vim, nano, mc)
|
|
2190
2209
|
if (tab.textarea) {
|
|
@@ -2370,9 +2389,9 @@ function initTerminal(tab) {
|
|
|
2370
2389
|
if (tab.fitAddon) tab.fitAddon.fit();
|
|
2371
2390
|
if (!window._pinchSaveTimer) {
|
|
2372
2391
|
window._pinchSaveTimer = setTimeout(() => {
|
|
2373
|
-
const persisted = JSON.parse(
|
|
2392
|
+
const persisted = JSON.parse(safeStorage.getItem('wt-settings') || '{}');
|
|
2374
2393
|
persisted.fontSize = tab.term.options.fontSize;
|
|
2375
|
-
|
|
2394
|
+
safeStorage.setItem('wt-settings', JSON.stringify(persisted));
|
|
2376
2395
|
window._pinchSaveTimer = null;
|
|
2377
2396
|
}, 500);
|
|
2378
2397
|
}
|
|
@@ -3576,7 +3595,7 @@ async function openFileEditor(path) {
|
|
|
3576
3595
|
const splitArea = document.getElementById('editor-split-area');
|
|
3577
3596
|
const orientToggle = document.getElementById('editor-split-toggle');
|
|
3578
3597
|
try {
|
|
3579
|
-
const orient =
|
|
3598
|
+
const orient = safeStorage.getItem('wt-editor-split-orientation');
|
|
3580
3599
|
const isHoriz = orient === 'horizontal';
|
|
3581
3600
|
if (splitArea) splitArea.classList.toggle('horizontal', isHoriz);
|
|
3582
3601
|
if (orientToggle) {
|
|
@@ -3600,7 +3619,7 @@ async function openFileEditor(path) {
|
|
|
3600
3619
|
try {
|
|
3601
3620
|
const isHoriz = splitArea && splitArea.classList.contains('horizontal');
|
|
3602
3621
|
const key = isHoriz ? 'wt-editor-width' : 'wt-editor-height';
|
|
3603
|
-
const saved =
|
|
3622
|
+
const saved = safeStorage.getItem(key);
|
|
3604
3623
|
if (saved) {
|
|
3605
3624
|
document.documentElement.style.setProperty('--editor-size', saved + 'px');
|
|
3606
3625
|
}
|
|
@@ -3933,7 +3952,7 @@ function searchKeydown(e) {
|
|
|
3933
3952
|
// ═══════════════════════════════════════════════════════
|
|
3934
3953
|
function loadSettings() {
|
|
3935
3954
|
try {
|
|
3936
|
-
const s = JSON.parse(
|
|
3955
|
+
const s = JSON.parse(safeStorage.getItem('wt-settings'));
|
|
3937
3956
|
if (s && typeof s === 'object' && !Array.isArray(s)) {
|
|
3938
3957
|
if (typeof s.theme === 'string' && ['system','tokyonight','light','solarized','gruvbox','dracula','monokai'].includes(s.theme)) settings.theme = s.theme;
|
|
3939
3958
|
if (typeof s.fontSize === 'number' && s.fontSize >= 8 && s.fontSize <= 32) settings.fontSize = s.fontSize;
|
|
@@ -3965,7 +3984,7 @@ function loadSettings() {
|
|
|
3965
3984
|
}
|
|
3966
3985
|
}
|
|
3967
3986
|
|
|
3968
|
-
function saveSettings() {
|
|
3987
|
+
function saveSettings() { safeStorage.setItem('wt-settings', JSON.stringify(settings)); }
|
|
3969
3988
|
|
|
3970
3989
|
function saveSetting(k, v) { settings[k] = v; saveSettings(); }
|
|
3971
3990
|
|
|
@@ -4265,7 +4284,7 @@ function setupSidebarResize() {
|
|
|
4265
4284
|
document.removeEventListener('mouseup', stopResize);
|
|
4266
4285
|
document.removeEventListener('touchmove', doResize);
|
|
4267
4286
|
document.removeEventListener('touchend', stopResize);
|
|
4268
|
-
try {
|
|
4287
|
+
try { safeStorage.setItem('wt-sidebar-width', String(sidebar.offsetWidth)); } catch(e) { console.warn(e); }
|
|
4269
4288
|
}
|
|
4270
4289
|
handle.addEventListener('mousedown', startResize);
|
|
4271
4290
|
handle.addEventListener('touchstart', startResize, { passive: false });
|
|
@@ -4274,7 +4293,7 @@ function setupSidebarResize() {
|
|
|
4274
4293
|
function restoreSidebarWidth() {
|
|
4275
4294
|
if (window.innerWidth <= 768) return;
|
|
4276
4295
|
try {
|
|
4277
|
-
const savedW =
|
|
4296
|
+
const savedW = safeStorage.getItem('wt-sidebar-width');
|
|
4278
4297
|
if (savedW) {
|
|
4279
4298
|
const w = Math.max(150, Math.min(600, parseInt(savedW)));
|
|
4280
4299
|
if (w) {
|
|
@@ -4352,7 +4371,7 @@ function setupEditorResize() {
|
|
|
4352
4371
|
const v = document.documentElement.style.getPropertyValue('--editor-size');
|
|
4353
4372
|
if (v) {
|
|
4354
4373
|
const key = isHorizontal() ? 'wt-editor-width' : 'wt-editor-height';
|
|
4355
|
-
|
|
4374
|
+
safeStorage.setItem(key, v.replace('px', ''));
|
|
4356
4375
|
}
|
|
4357
4376
|
} catch(e) { console.warn(e); }
|
|
4358
4377
|
}
|
|
@@ -4391,7 +4410,7 @@ function toggleEditorSplit() {
|
|
|
4391
4410
|
// Restore saved size for this orientation
|
|
4392
4411
|
try {
|
|
4393
4412
|
const key = isHoriz ? 'wt-editor-width' : 'wt-editor-height';
|
|
4394
|
-
const saved =
|
|
4413
|
+
const saved = safeStorage.getItem(key);
|
|
4395
4414
|
if (saved) {
|
|
4396
4415
|
document.documentElement.style.setProperty('--editor-size', saved + 'px');
|
|
4397
4416
|
}
|
|
@@ -4403,7 +4422,7 @@ function toggleEditorSplit() {
|
|
|
4403
4422
|
});
|
|
4404
4423
|
|
|
4405
4424
|
// Persist orientation
|
|
4406
|
-
try {
|
|
4425
|
+
try { safeStorage.setItem('wt-editor-split-orientation', isHoriz ? 'horizontal' : 'vertical'); } catch(e) { console.warn(e); }
|
|
4407
4426
|
}
|
|
4408
4427
|
|
|
4409
4428
|
// ═══════════════════════════════════════════════════════
|
|
@@ -4679,6 +4698,8 @@ function pasteToTerminal() {
|
|
|
4679
4698
|
function fallbackPaste(callback) {
|
|
4680
4699
|
if (!_pasteInput) {
|
|
4681
4700
|
_pasteInput = document.createElement('textarea');
|
|
4701
|
+
_pasteInput.id = 'fallback-paste-area';
|
|
4702
|
+
_pasteInput.name = 'paste-input';
|
|
4682
4703
|
_pasteInput.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;z-index:-1';
|
|
4683
4704
|
document.body.appendChild(_pasteInput);
|
|
4684
4705
|
_pasteInput.addEventListener('paste', () => {
|
|
@@ -4912,7 +4933,7 @@ function closeOverlay(id) {
|
|
|
4912
4933
|
overlay.classList.remove('open');
|
|
4913
4934
|
removeFocusTrap();
|
|
4914
4935
|
if (lastFocusedElement) {
|
|
4915
|
-
setTimeout(() => lastFocusedElement
|
|
4936
|
+
setTimeout(() => lastFocusedElement?.focus(), 50);
|
|
4916
4937
|
lastFocusedElement = null;
|
|
4917
4938
|
}
|
|
4918
4939
|
}
|
|
@@ -5092,9 +5113,9 @@ function showPinScreen() {
|
|
|
5092
5113
|
// BOOKMARKS
|
|
5093
5114
|
// ═══════════════════════════════════════════════════════
|
|
5094
5115
|
function getBookmarks() {
|
|
5095
|
-
try { return JSON.parse(
|
|
5116
|
+
try { return JSON.parse(safeStorage.getItem('wt-bookmarks')) || []; } catch(e) { console.warn('Failed to parse bookmarks:', e); return []; }
|
|
5096
5117
|
}
|
|
5097
|
-
function saveBookmarks(bm) {
|
|
5118
|
+
function saveBookmarks(bm) { safeStorage.setItem('wt-bookmarks', JSON.stringify(bm)); renderBookmarks(); }
|
|
5098
5119
|
|
|
5099
5120
|
function renderBookmarks() {
|
|
5100
5121
|
const list = document.getElementById('bookmarks-list');
|
|
@@ -5143,11 +5164,11 @@ function bookmarkCurrentDir() {
|
|
|
5143
5164
|
toast('Bookmarked', 'success');
|
|
5144
5165
|
}
|
|
5145
5166
|
|
|
5146
|
-
let bookmarksOpen = (() => { try { const v =
|
|
5167
|
+
let bookmarksOpen = (() => { try { const v = safeStorage.getItem('wt-bm-open'); return v !== null ? v === 'true' : true; } catch(e) { console.warn('Failed to read bookmarksOpen:', e); return true; } })();
|
|
5147
5168
|
function toggleBookmarks() {
|
|
5148
5169
|
bookmarksOpen = !bookmarksOpen;
|
|
5149
5170
|
document.getElementById('bookmarks-list').classList.toggle('open', bookmarksOpen);
|
|
5150
|
-
try {
|
|
5171
|
+
try { safeStorage.setItem('wt-bm-open', bookmarksOpen); } catch(e) { console.warn(e); }
|
|
5151
5172
|
}
|
|
5152
5173
|
|
|
5153
5174
|
// ═══════════════════════════════════════════════════════
|
|
@@ -5220,10 +5241,10 @@ function finderKeydown(e) {
|
|
|
5220
5241
|
const DEFAULT_CMDS = window.DEFAULT_CMDS || [];
|
|
5221
5242
|
|
|
5222
5243
|
function getCmdLib() {
|
|
5223
|
-
try { return JSON.parse(
|
|
5244
|
+
try { return JSON.parse(safeStorage.getItem('wt-cmdlib')) || []; } catch(e) { return []; }
|
|
5224
5245
|
}
|
|
5225
5246
|
function saveCmdLib(cmds) {
|
|
5226
|
-
|
|
5247
|
+
safeStorage.setItem('wt-cmdlib', JSON.stringify(cmds));
|
|
5227
5248
|
}
|
|
5228
5249
|
|
|
5229
5250
|
function toggleCmdLib() {
|
|
@@ -5383,10 +5404,10 @@ let cmdHistMax = 50;
|
|
|
5383
5404
|
let _cmdHistCache = [];
|
|
5384
5405
|
|
|
5385
5406
|
function histHeaders() {
|
|
5386
|
-
return { 'Content-Type': 'application/json', 'x-pin-token':
|
|
5407
|
+
return { 'Content-Type': 'application/json', 'x-pin-token': authToken || '' };
|
|
5387
5408
|
}
|
|
5388
5409
|
function histHeadersDelete() {
|
|
5389
|
-
return { 'x-pin-token':
|
|
5410
|
+
return { 'x-pin-token': authToken || '' };
|
|
5390
5411
|
}
|
|
5391
5412
|
async function getCmdHist() {
|
|
5392
5413
|
try {
|
|
@@ -5406,10 +5427,15 @@ function loadHistMax() {
|
|
|
5406
5427
|
}
|
|
5407
5428
|
async function addToCmdHist(cmd) {
|
|
5408
5429
|
if (!cmd || !cmd.trim()) return;
|
|
5409
|
-
// Strip ANSI
|
|
5410
|
-
const clean = cmd
|
|
5411
|
-
.replace(/\x1b
|
|
5412
|
-
.replace(
|
|
5430
|
+
// Strip all ANSI/VT escape sequences and control characters
|
|
5431
|
+
const clean = cmd
|
|
5432
|
+
.replace(/\x1b[^a-zA-Z0-9]*[a-zA-Z0-9~]/g, '') // CSI: ESC [ ... final
|
|
5433
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC: ESC ] ... BEL/ST
|
|
5434
|
+
.replace(/\x1b[OPP][^\x40-\x7e]*[\x40-\x7e]/g, '') // SS3/DCS: ESC O/P ... final
|
|
5435
|
+
.replace(/\x1b./g, '') // Any other ESC sequence
|
|
5436
|
+
.replace(/[\x00-\x1f\x7f]/g, '') // All remaining control chars
|
|
5437
|
+
.replace(/^[>;\d\s]+(?=[a-zA-Z/\\~\-.])/, '') // VT params before a real command char
|
|
5438
|
+
.replace(/^[>;\d\s]+$/, '') // Pure VT params, no command at all
|
|
5413
5439
|
.trim();
|
|
5414
5440
|
if (!clean) return;
|
|
5415
5441
|
try {
|
|
@@ -5627,7 +5653,7 @@ async function refreshSystemStats(showLoading) {
|
|
|
5627
5653
|
// ═══════════════════════════════════════════════════════
|
|
5628
5654
|
let deferredPrompt = null;
|
|
5629
5655
|
let installBannerDismissed = false;
|
|
5630
|
-
try { installBannerDismissed =
|
|
5656
|
+
try { installBannerDismissed = safeStorage.getItem('wt-install-dismissed') === 'true'; } catch(e) { console.warn(e); }
|
|
5631
5657
|
|
|
5632
5658
|
window.addEventListener('beforeinstallprompt', e => {
|
|
5633
5659
|
e.preventDefault();
|
|
@@ -5666,7 +5692,7 @@ function installPWA() {
|
|
|
5666
5692
|
function dismissInstall() {
|
|
5667
5693
|
document.getElementById('install-banner').style.display = 'none';
|
|
5668
5694
|
document.getElementById('toast-container').classList.remove('banner-visible');
|
|
5669
|
-
try {
|
|
5695
|
+
try { safeStorage.setItem('wt-install-dismissed', 'true'); } catch(e) { console.warn(e); }
|
|
5670
5696
|
installBannerDismissed = true;
|
|
5671
5697
|
}
|
|
5672
5698
|
|