Add PWA support and mobile fixes
This commit is contained in:
+1
-1
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user