Improve mobile support with half-baked custom keyboard

This commit is contained in:
LukeGus
2025-09-04 22:27:48 -05:00
parent ab07b2ba97
commit d1c55f5883
7 changed files with 216 additions and 19 deletions

2
.env
View File

@@ -1,2 +1,2 @@
VERSION=1.5.0
VITE_API_HOST=10.31.2.21
VITE_API_HOST=localhost

11
package-lock.json generated
View File

@@ -66,6 +66,7 @@
"react-i18next": "^15.7.3",
"react-resizable-panels": "^3.0.3",
"react-responsive": "^10.0.1",
"react-simple-keyboard": "^3.8.120",
"react-xtermjs": "^1.0.10",
"sonner": "^2.0.7",
"speakeasy": "^2.0.0",
@@ -12275,6 +12276,16 @@
"react": ">=16.8.0"
}
},
"node_modules/react-simple-keyboard": {
"version": "3.8.120",
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.120.tgz",
"integrity": "sha512-VREEGZWXUeqRKvRVg0n8hmoAqz/TSWZEs5UwbfLuan4yKvOQZUFHtS11QGnvIVYjkThh+JYslO2CHT4Lxf5d0w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",

View File

@@ -85,6 +85,7 @@
"react-i18next": "^15.7.3",
"react-resizable-panels": "^3.0.3",
"react-responsive": "^10.0.1",
"react-simple-keyboard": "^3.8.120",
"react-xtermjs": "^1.0.10",
"sonner": "^2.0.7",
"speakeasy": "^2.0.0",

View File

@@ -108,6 +108,17 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
useEffect(() => {
if (!terminal) return;
const textarea = (terminal as any)._core?._textarea as HTMLTextAreaElement | undefined;
if (textarea) {
textarea.setAttribute("readonly", "true");
textarea.setAttribute("inputmode", "none");
textarea.style.caretColor = "transparent";
}
}, [terminal]);
function handleWindowResize() {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
@@ -168,7 +179,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
scrollback: 10000,
fontSize: 14,
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
theme: {background: '#18181b', foreground: '#f7f7f7'},
theme: {background: '#09090b', foreground: '#f7f7f7'},
allowTransparency: true,
convertEol: true,
windowsMode: false,
@@ -209,7 +220,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
setVisible(true);
terminal.focus();
}, 100);
return () => {
@@ -229,7 +239,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
scrollback: 10000,
fontSize: 14,
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
theme: {background: '#18181b', foreground: '#f7f7f7'},
theme: {background: '#09090b', foreground: '#f7f7f7'},
allowTransparency: true,
convertEol: true,
windowsMode: false,
@@ -274,7 +284,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
setVisible(true);
terminal.focus();
}, 0);
const cols = terminal.cols;
@@ -313,7 +322,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
terminal.focus();
}, 0);
}
}, [isVisible, terminal]);
@@ -324,9 +332,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
if (terminal && isVisible) {
terminal.focus();
}
}, 0);
}, [isVisible, terminal]);
@@ -335,9 +340,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
ref={xtermRef}
className="h-full w-full m-1"
style={{opacity: visible && isVisible ? 1 : 0, overflow: 'hidden'}}
onClick={() => {
terminal.focus();
}}
/>
);
});

View File

@@ -0,0 +1,107 @@
import React, {useState} from "react";
import Keyboard from "react-simple-keyboard";
import "react-simple-keyboard/build/css/index.css";
import "./kb-dark-theme.css";
interface TerminalKeyboardProps {
onSendInput: (input: string) => void;
}
export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
const [layoutName, setLayoutName] = useState("default");
const onKeyPress = (button: string) => {
if (button === "{shift}") {
setLayoutName("shift");
return;
}
if (button === "{unshift}") {
setLayoutName("default");
return;
}
if (button === "{more}") {
setLayoutName("more")
return;
}
if (button === "{less}") {
setLayoutName("default");
return;
}
if (button === "{hide}") {
setLayoutName("hide");
return;
}
if (button === "{unhide}") {
setLayoutName("default");
return;
}
onSendInput(button);
};
return (
<div className="">
<Keyboard
layout={{
default: [
"q w e r t y u i o p",
"a s d f g h j k l",
"{shift} z x c v b n m {backspace}",
"{hide} {more} {space} {enter}",
],
shift: [
"Q W E R T Y U I O P",
"A S D F G H J K L",
"{unshift} Z X C V B N M {backspace}",
"{hide} {more} {space} {enter}",
],
more: [
"{arrowLeft} {arrowRight} {arrowUp} {arrowDown} {backspace}",
"{hide} {less} {space} {enter}",
],
hide: [
"{unhide}"
]
}}
layoutName={layoutName}
onKeyPress={onKeyPress}
display={{
"{shift}": "up",
"{unshift}": "dn",
"{backspace}": "del",
"{more}": "more",
"{less}": "less",
"{space}": "space",
"{enter}": "enter",
"{arrowLeft}": "←",
"{arrowRight}": "→",
"{arrowUp}": "↑",
"{arrowDown}": "↓",
"{hide}": "hide",
"{unhide}": "unhide",
}}
theme={"hg-theme-default dark-theme"}
useTouchEvents={true}
buttonTheme={[
{
class: "hg-space-big",
buttons: "{space}",
},
{
class: "hg-space-medium",
buttons: "{enter} {backspace}",
},
{
class: "hg-space-small",
buttons: "{hide} {less} {more}",
}
]}
/>
</div>
);
}

View File

@@ -0,0 +1,35 @@
.simple-keyboard.dark-theme {
background-color: rgb(24, 24, 27);
border-radius: 0;
}
.simple-keyboard.dark-theme .hg-button {
height: 50px;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
color: #bfbfbf;
border-bottom-color: rgb(122, 122, 122);
}
.simple-keyboard.dark-theme .hg-button:active {
background: rgba(83, 83, 83, 0.5);
color: #bfbfbf;
}
#root .simple-keyboard.dark-theme + .simple-keyboard-preview {
background: rgba(83, 83, 83, 0.5);
}
.hg-space-big {
width: 100px;
}
.hg-space-medium {
width: 60px;
}
.hg-space-small {
width: 1px;
}

View File

@@ -1,14 +1,55 @@
import {useRef} from "react";
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
export function MobileApp() {
const terminalRef = useRef<any>(null);
function handleKeyboardInput(input: string) {
if (!terminalRef.current?.sendInput) return;
const keyMap: Record<string, string> = {
"{backspace}": "\x7f",
"{space}": " ",
"{tab}": "\t",
"{enter}": "\r",
"{escape}": "\x1b",
"{arrowUp}": "\x1b[A",
"{arrowDown}": "\x1b[B",
"{arrowRight}": "\x1b[C",
"{arrowLeft}": "\x1b[D",
"{delete}": "\x1b[3~",
"{home}": "\x1b[H",
"{end}": "\x1b[F",
"{pageUp}": "\x1b[5~",
"{pageDown}": "\x1b[6~",
};
if (input in keyMap) {
terminalRef.current.sendInput(keyMap[input]);
} else {
terminalRef.current.sendInput(input);
}
}
return (
<div className="h-screen w-screen bg-[#18181b]">
<Terminal hostConfig={{
ip: "n/a",
port: 22,
username: "n/a",
password: "n/a"
}} isVisible={true}/>
<div className="h-screen w-screen flex flex-col bg-[#09090b] overflow-y-hidden overflow-x-hidden">
<Terminal
ref={terminalRef}
hostConfig={{
ip: "192.210.197.55",
port: 22,
username: "bugattiguy527",
password: "bugatti$123"
}}
isVisible={true}
/>
<TerminalKeyboard
onSendInput={handleKeyboardInput}
/>
<div className="w-full h-[80px] bg-[#18181BFF]">
</div>
</div>
)
}