Add PWA support and mobile fixes

This commit is contained in:
GitHub Copilot
2026-01-30 09:36:48 +00:00
parent ef5b8f08ae
commit e6deed7ccc
7 changed files with 61 additions and 23 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "webterm" name = "webterm"
version = "1.1.32" version = "1.2.0"
description = "Serve terminal sessions over the web" description = "Serve terminal sessions over the web"
authors = ["Will McGugan <will@textualize.io>"] authors = ["Will McGugan <will@textualize.io>"]
license = "MIT" license = "MIT"
+4 -1
View File
@@ -1036,6 +1036,10 @@ class LocalServer:
<html> <html>
<head> <head>
<title>Session Dashboard</title> <title>Session Dashboard</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#0d1117">
<link rel="icon" href="/static/icons/webterm-192.png" sizes="192x192">
<link rel="apple-touch-icon" href="/static/icons/webterm-192.png">
<style> <style>
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 16px; background: #0f172a; color: #e2e8f0; }} body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 16px; background: #0f172a; color: #e2e8f0; }}
h1 {{ margin-bottom: 8px; }} h1 {{ margin-bottom: 8px; }}
@@ -1174,7 +1178,6 @@ class LocalServer:
if (typeof win.focus === 'function') {{ if (typeof win.focus === 'function') {{
win.focus(); win.focus();
}} }}
win.location.href = url;
}} else {{ }} else {{
window.location.href = url; window.location.href = url;
}} }}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because one or more lines are too long
+19 -5
View File
@@ -409,6 +409,8 @@ class WebTerminal {
private mobileKeybar: HTMLElement | null = null; private mobileKeybar: HTMLElement | null = null;
private ctrlActive = false; private ctrlActive = false;
private shiftActive = false; private shiftActive = false;
private pendingCtrl = false;
private pendingShift = false;
private fontFamily: string; private fontFamily: string;
private fontSize: number; private fontSize: number;
@@ -664,12 +666,16 @@ class WebTerminal {
// Handle special keys via beforeinput to intercept before browser modifies textarea // Handle special keys via beforeinput to intercept before browser modifies textarea
textarea.addEventListener("beforeinput", (e) => { textarea.addEventListener("beforeinput", (e) => {
if (e.inputType === "insertText" && e.data && (this.ctrlActive || this.shiftActive)) { if (
e.inputType === "insertText"
&& e.data
&& (this.ctrlActive || this.shiftActive || this.pendingCtrl || this.pendingShift)
) {
let toSend = e.data; let toSend = e.data;
if (this.shiftActive && toSend.length === 1) { if ((this.shiftActive || this.pendingShift) && toSend.length === 1) {
toSend = toSend.toUpperCase(); toSend = toSend.toUpperCase();
} }
if (this.ctrlActive && toSend.length === 1) { if ((this.ctrlActive || this.pendingCtrl) && toSend.length === 1) {
const code = toSend.toUpperCase().charCodeAt(0); const code = toSend.toUpperCase().charCodeAt(0);
if (code >= 65 && code <= 90) { if (code >= 65 && code <= 90) {
toSend = String.fromCharCode(code - 64); toSend = String.fromCharCode(code - 64);
@@ -680,6 +686,8 @@ class WebTerminal {
this.send(["stdin", toSend]); this.send(["stdin", toSend]);
textarea.value = ""; textarea.value = "";
this.deactivateModifiers(); this.deactivateModifiers();
this.pendingCtrl = false;
this.pendingShift = false;
return; return;
} }
@@ -710,11 +718,11 @@ class WebTerminal {
if (value) { if (value) {
let toSend = value; let toSend = value;
// Apply Shift modifier (uppercase letters) // Apply Shift modifier (uppercase letters)
if (this.shiftActive) { if (this.shiftActive || this.pendingShift) {
toSend = value.toUpperCase(); toSend = value.toUpperCase();
} }
// Apply Ctrl modifier if active (convert letters to control codes) // Apply Ctrl modifier if active (convert letters to control codes)
if (this.ctrlActive) { if (this.ctrlActive || this.pendingCtrl) {
const firstChar = toSend[0] ?? ""; const firstChar = toSend[0] ?? "";
const code = firstChar.toUpperCase().charCodeAt(0); const code = firstChar.toUpperCase().charCodeAt(0);
if (code >= 65 && code <= 90) { if (code >= 65 && code <= 90) {
@@ -726,6 +734,8 @@ class WebTerminal {
this.send(["stdin", toSend]); this.send(["stdin", toSend]);
textarea.value = ""; textarea.value = "";
this.deactivateModifiers(); this.deactivateModifiers();
this.pendingCtrl = false;
this.pendingShift = false;
} }
}); });
@@ -1004,9 +1014,11 @@ class WebTerminal {
const modifier = (btn as HTMLElement).dataset.modifier; const modifier = (btn as HTMLElement).dataset.modifier;
if (modifier === "ctrl") { if (modifier === "ctrl") {
this.ctrlActive = !this.ctrlActive; this.ctrlActive = !this.ctrlActive;
this.pendingCtrl = this.ctrlActive;
btn.classList.toggle("active", this.ctrlActive); btn.classList.toggle("active", this.ctrlActive);
} else if (modifier === "shift") { } else if (modifier === "shift") {
this.shiftActive = !this.shiftActive; this.shiftActive = !this.shiftActive;
this.pendingShift = this.shiftActive;
btn.classList.toggle("active", this.shiftActive); btn.classList.toggle("active", this.shiftActive);
} }
this.focusMobileInput(); this.focusMobileInput();
@@ -1070,6 +1082,8 @@ class WebTerminal {
private deactivateModifiers(): void { private deactivateModifiers(): void {
this.ctrlActive = false; this.ctrlActive = false;
this.shiftActive = false; this.shiftActive = false;
this.pendingCtrl = false;
this.pendingShift = false;
this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((btn) => { this.mobileKeybar?.querySelectorAll("button[data-modifier]").forEach((btn) => {
btn.classList.remove("active"); btn.classList.remove("active");
}); });
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Webterm Dashboard",
"short_name": "webterm",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#0d1117",
"theme_color": "#0d1117",
"icons": [
{
"src": "/static/icons/webterm-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/icons/webterm-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}