Initial commit: Socktop WebTerm with k3s deployment
- Multi-architecture Docker image (ARM64 + AMD64) - Kubernetes manifests for 3-replica deployment - Traefik ingress configuration - NGINX Proxy Manager integration - ConfigMap-based configuration - Automated build and deployment scripts - Session monitoring tools
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Static Assets Directory
|
||||
|
||||
This directory contains custom static assets for the webterm application.
|
||||
|
||||
## How to Add Files
|
||||
|
||||
Simply place your files in this directory:
|
||||
|
||||
```bash
|
||||
# Example: Add a background image
|
||||
cp my-background.png static/bg.png
|
||||
|
||||
# Example: Add a custom CSS file
|
||||
cp my-styles.css static/custom.css
|
||||
|
||||
# Example: Add a logo
|
||||
cp logo.svg static/logo.svg
|
||||
```
|
||||
|
||||
## How to Access Files
|
||||
|
||||
Files in this directory are served at **TWO** different URL paths:
|
||||
|
||||
### Option 1: `/assets/*` Route (Recommended)
|
||||
|
||||
Files are automatically served from this directory at `/assets/*`:
|
||||
|
||||
```
|
||||
static/bg.png → http://localhost:8082/assets/bg.png
|
||||
static/logo.svg → http://localhost:8082/assets/logo.svg
|
||||
static/custom.css → http://localhost:8082/assets/custom.css
|
||||
```
|
||||
|
||||
**Use in CSS/HTML:**
|
||||
```css
|
||||
body {
|
||||
background-image: url('/assets/bg.png');
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/assets/custom.css" />
|
||||
<img src="/assets/logo.svg" alt="Logo" />
|
||||
```
|
||||
|
||||
### Option 2: `/static/*` Route (Manual Copy Required)
|
||||
|
||||
If you prefer to use `/static/*` URLs, copy the file to `node_modules/`:
|
||||
|
||||
```bash
|
||||
cp static/bg.png node_modules/
|
||||
```
|
||||
|
||||
Then access it at:
|
||||
```css
|
||||
background-image: url('/static/bg.png');
|
||||
```
|
||||
|
||||
**⚠️ Warning:** Files in `node_modules/` may be removed when you run `npm install` or `npm ci`.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Use `/assets/*` for your custom assets** because:
|
||||
- ✅ No need to copy files manually
|
||||
- ✅ Won't be lost when running npm commands
|
||||
- ✅ Clear separation between your assets and npm packages
|
||||
- ✅ Better organization and maintainability
|
||||
|
||||
Reserve `/static/*` for npm packages (xterm.js, addons, etc.).
|
||||
|
||||
## Current Files
|
||||
|
||||
- `terminado-addon.js` - Custom xterm.js addon for Terminado protocol
|
||||
- `bg.png` - Background image (1.3 MB)
|
||||
|
||||
## Server Configuration
|
||||
|
||||
The server is configured in `src/server.rs`:
|
||||
|
||||
```rust
|
||||
App::new()
|
||||
.service(actix_files::Files::new("/assets", "./static")) // This directory
|
||||
.service(actix_files::Files::new("/static", "./node_modules")) // npm packages
|
||||
```
|
||||
|
||||
## More Information
|
||||
|
||||
See `STATIC_ASSETS.md` in the project root for comprehensive documentation on:
|
||||
- Adding different types of assets (images, fonts, CSS, JS)
|
||||
- Path reference guide
|
||||
- Best practices
|
||||
- Troubleshooting
|
||||
- Performance optimization
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,418 @@
|
||||
/* Catppuccin Frappe Color Palette */
|
||||
:root {
|
||||
/* Base colors */
|
||||
--ctp-base: #303446;
|
||||
--ctp-mantle: #292c3c;
|
||||
--ctp-crust: #232634;
|
||||
|
||||
/* Surface colors */
|
||||
--ctp-surface0: #414559;
|
||||
--ctp-surface1: #51576d;
|
||||
--ctp-surface2: #626880;
|
||||
|
||||
/* Overlay colors */
|
||||
--ctp-overlay0: #737994;
|
||||
--ctp-overlay1: #838ba7;
|
||||
--ctp-overlay2: #949cbb;
|
||||
|
||||
/* Text colors */
|
||||
--ctp-text: #c6d0f5;
|
||||
--ctp-subtext1: #b5bfe2;
|
||||
--ctp-subtext0: #a5adce;
|
||||
|
||||
/* Accent colors */
|
||||
--ctp-lavender: #babbf1;
|
||||
--ctp-blue: #8caaee;
|
||||
--ctp-sapphire: #85c1dc;
|
||||
--ctp-sky: #99d1db;
|
||||
--ctp-teal: #81c8be;
|
||||
--ctp-green: #a6d189;
|
||||
--ctp-yellow: #e5c890;
|
||||
--ctp-peach: #ef9f76;
|
||||
--ctp-maroon: #ea999c;
|
||||
--ctp-red: #e78284;
|
||||
--ctp-mauve: #ca9ee6;
|
||||
--ctp-pink: #f4b8e4;
|
||||
--ctp-flamingo: #eebebe;
|
||||
--ctp-rosewater: #f2d5cf;
|
||||
|
||||
/* Layout */
|
||||
--max-terminal-width: 1200px;
|
||||
--max-terminal-height: 65vh;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-family:
|
||||
"Inter",
|
||||
"SF Pro Display",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
sans-serif;
|
||||
background-color: var(--ctp-base);
|
||||
background-image: url("/static/bg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
color: var(--ctp-text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
padding: 2rem 2rem 1.5rem 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--ctp-mauve) 0%,
|
||||
var(--ctp-blue) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.02em;
|
||||
text-shadow: 0 2px 20px rgba(202, 158, 230, 0.3);
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
font-size: 1.125rem;
|
||||
color: var(--ctp-subtext1);
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* Links Section */
|
||||
.links-section {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(65, 69, 89, 0.6);
|
||||
border: 1px solid rgba(186, 187, 241, 0.2);
|
||||
border-radius: 12px;
|
||||
color: var(--ctp-text);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
background: rgba(81, 87, 109, 0.8);
|
||||
border-color: var(--ctp-mauve);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(202, 158, 230, 0.2);
|
||||
}
|
||||
|
||||
.link-button i {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.link-button.github {
|
||||
border-color: rgba(186, 187, 241, 0.3);
|
||||
}
|
||||
|
||||
.link-button.github:hover {
|
||||
border-color: var(--ctp-lavender);
|
||||
box-shadow: 0 8px 24px rgba(186, 187, 241, 0.25);
|
||||
}
|
||||
|
||||
.link-button.crate {
|
||||
border-color: rgba(239, 159, 118, 0.3);
|
||||
}
|
||||
|
||||
.link-button.crate:hover {
|
||||
border-color: var(--ctp-peach);
|
||||
box-shadow: 0 8px 24px rgba(239, 159, 118, 0.25);
|
||||
}
|
||||
|
||||
.link-button.apt {
|
||||
border-color: rgba(166, 209, 137, 0.3);
|
||||
}
|
||||
|
||||
.link-button.apt:hover {
|
||||
border-color: var(--ctp-green);
|
||||
box-shadow: 0 8px 24px rgba(166, 209, 137, 0.25);
|
||||
}
|
||||
|
||||
/* Terminal Wrapper */
|
||||
.terminal-wrapper {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 0 2rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
/* Terminal Window Frame */
|
||||
.terminal-window {
|
||||
width: 100%;
|
||||
max-width: var(--max-terminal-width);
|
||||
max-height: var(--max-terminal-height);
|
||||
min-height: var(--max-terminal-height);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 60px rgba(0, 0, 0, 0.4),
|
||||
0 12px 24px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(186, 187, 241, 0.1);
|
||||
background: transparent;
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(186, 187, 241, 0.15);
|
||||
-webkit-mask-image: -webkit-radial-gradient(white, black);
|
||||
}
|
||||
|
||||
/* Terminal Title Bar */
|
||||
.terminal-titlebar {
|
||||
height: 44px;
|
||||
background: rgba(41, 44, 60, 0.8);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
user-select: none;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Traffic Light Buttons */
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.terminal-button {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.4);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.terminal-button:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.terminal-button.close {
|
||||
background: var(--ctp-red);
|
||||
}
|
||||
|
||||
.terminal-button.close:hover::before {
|
||||
background: rgba(35, 38, 52, 0.8);
|
||||
content: "×";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.terminal-button.minimize {
|
||||
background: var(--ctp-yellow);
|
||||
}
|
||||
|
||||
.terminal-button.minimize:hover::before {
|
||||
background: rgba(35, 38, 52, 0.8);
|
||||
}
|
||||
|
||||
.terminal-button.maximize {
|
||||
background: var(--ctp-green);
|
||||
}
|
||||
|
||||
.terminal-button.maximize:hover::before {
|
||||
background: rgba(35, 38, 52, 0.8);
|
||||
}
|
||||
|
||||
/* Terminal Title */
|
||||
.terminal-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: var(--ctp-subtext1);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Terminal Content */
|
||||
#terminal {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: rgba(48, 52, 70, 0.85);
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
padding: 8px 12px 8px 12px;
|
||||
}
|
||||
|
||||
/* Ensure xterm fills container and respects rounded corners */
|
||||
.xterm {
|
||||
height: 100% !important;
|
||||
background: rgba(48, 52, 70, 0.85) !important;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
.xterm-viewport {
|
||||
height: 100% !important;
|
||||
border-bottom-left-radius: 12px !important;
|
||||
border-bottom-right-radius: 12px !important;
|
||||
}
|
||||
|
||||
.xterm-screen {
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
/* Force canvas to clip at rounded corners */
|
||||
.xterm canvas {
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
/* Hide scrollbars */
|
||||
#terminal,
|
||||
.xterm,
|
||||
.xterm-viewport {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
#terminal::-webkit-scrollbar,
|
||||
.xterm::-webkit-scrollbar,
|
||||
.xterm-viewport::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--ctp-overlay1);
|
||||
font-size: 0.875rem;
|
||||
background: rgba(35, 38, 52, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(186, 187, 241, 0.1);
|
||||
}
|
||||
|
||||
.site-footer a {
|
||||
color: var(--ctp-mauve);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.site-footer a:hover {
|
||||
color: var(--ctp-lavender);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.links-section {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.terminal-wrapper {
|
||||
padding: 0 1rem 2rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero-section {
|
||||
padding: 2rem 1rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.terminal-titlebar {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.terminal-button {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Selection colors */
|
||||
::selection {
|
||||
background: var(--ctp-mauve);
|
||||
color: var(--ctp-base);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--ctp-mauve);
|
||||
color: var(--ctp-base);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Terminado WebSocket Addon for xterm.js 5.x
|
||||
*
|
||||
* This addon handles the Terminado protocol for xterm.js, allowing
|
||||
* bidirectional communication over WebSocket with a backend PTY process.
|
||||
*
|
||||
* The Terminado protocol uses JSON arrays:
|
||||
* - ["stdin", data] - send input to the terminal
|
||||
* - ["stdout", data] - receive output from the terminal
|
||||
* - ["set_size", rows, cols] - notify backend of terminal size changes
|
||||
*/
|
||||
|
||||
class TerminadoAddon {
|
||||
constructor() {
|
||||
this._disposables = [];
|
||||
this._socket = null;
|
||||
this._bidirectional = true;
|
||||
this._buffered = true;
|
||||
this._attachSocketBuffer = '';
|
||||
this._flushTimeout = null;
|
||||
}
|
||||
|
||||
activate(terminal) {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
this._disposables.length = 0;
|
||||
|
||||
if (this._flushTimeout) {
|
||||
clearTimeout(this._flushTimeout);
|
||||
this._flushTimeout = null;
|
||||
}
|
||||
|
||||
if (this._socket) {
|
||||
this.detach();
|
||||
}
|
||||
}
|
||||
|
||||
attach(socket, bidirectional = true, buffered = true) {
|
||||
if (this._socket) {
|
||||
this.detach();
|
||||
}
|
||||
|
||||
this._socket = socket;
|
||||
this._bidirectional = bidirectional !== false;
|
||||
this._buffered = buffered !== false;
|
||||
|
||||
// Handle incoming messages from the websocket
|
||||
this._messageHandler = (ev) => {
|
||||
try {
|
||||
const data = JSON.parse(ev.data);
|
||||
if (Array.isArray(data) && data[0] === 'stdout') {
|
||||
const output = data[1];
|
||||
if (this._buffered) {
|
||||
this._pushToBuffer(output);
|
||||
} else {
|
||||
this._terminal.write(output);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error handling terminado message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle terminal input (user typing)
|
||||
if (this._bidirectional) {
|
||||
const dataDisposable = this._terminal.onData((data) => {
|
||||
this._sendData(data);
|
||||
});
|
||||
this._disposables.push(dataDisposable);
|
||||
}
|
||||
|
||||
// Handle terminal resize events
|
||||
const resizeDisposable = this._terminal.onResize((size) => {
|
||||
this._setSize(size);
|
||||
});
|
||||
this._disposables.push(resizeDisposable);
|
||||
|
||||
// Handle socket close/error
|
||||
this._closeHandler = () => this.detach();
|
||||
this._errorHandler = () => this.detach();
|
||||
|
||||
socket.addEventListener('message', this._messageHandler);
|
||||
socket.addEventListener('close', this._closeHandler);
|
||||
socket.addEventListener('error', this._errorHandler);
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (!this._socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._messageHandler) {
|
||||
this._socket.removeEventListener('message', this._messageHandler);
|
||||
this._messageHandler = null;
|
||||
}
|
||||
|
||||
if (this._closeHandler) {
|
||||
this._socket.removeEventListener('close', this._closeHandler);
|
||||
this._closeHandler = null;
|
||||
}
|
||||
|
||||
if (this._errorHandler) {
|
||||
this._socket.removeEventListener('error', this._errorHandler);
|
||||
this._errorHandler = null;
|
||||
}
|
||||
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
this._disposables.length = 0;
|
||||
|
||||
this._socket = null;
|
||||
}
|
||||
|
||||
_sendData(data) {
|
||||
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this._socket.send(JSON.stringify(['stdin', data]));
|
||||
} catch (err) {
|
||||
console.error('Error sending data to terminal:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setSize(size) {
|
||||
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this._socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||||
} catch (err) {
|
||||
console.error('Error sending terminal size:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_pushToBuffer(data) {
|
||||
if (this._attachSocketBuffer) {
|
||||
this._attachSocketBuffer += data;
|
||||
} else {
|
||||
this._attachSocketBuffer = data;
|
||||
if (this._flushTimeout) {
|
||||
clearTimeout(this._flushTimeout);
|
||||
}
|
||||
this._flushTimeout = setTimeout(() => this._flushBuffer(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
_flushBuffer() {
|
||||
if (this._attachSocketBuffer && this._terminal) {
|
||||
this._terminal.write(this._attachSocketBuffer);
|
||||
this._attachSocketBuffer = '';
|
||||
}
|
||||
this._flushTimeout = null;
|
||||
}
|
||||
|
||||
// Public method to manually send size
|
||||
sendSize(rows, cols) {
|
||||
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this._socket.send(JSON.stringify(['set_size', rows, cols]));
|
||||
} catch (err) {
|
||||
console.error('Error sending manual terminal size:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public method to manually send command
|
||||
sendCommand(command) {
|
||||
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this._socket.send(JSON.stringify(['stdin', command]));
|
||||
} catch (err) {
|
||||
console.error('Error sending command:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in browsers
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TerminadoAddon;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Terminal Initialization Script for socktop webterm
|
||||
// Catppuccin Frappe theme with transparency
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Catppuccin Frappe theme for xterm
|
||||
var term = new Terminal({
|
||||
allowTransparency: true,
|
||||
fontFamily:
|
||||
'"JetBrains Mono", "Fira Code", "Cascadia Code", Consolas, monospace',
|
||||
fontSize: 14,
|
||||
lineHeight: 1.2,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
theme: {
|
||||
background: "rgba(48, 52, 70, 0.75)",
|
||||
foreground: "#c6d0f5",
|
||||
cursor: "#f2d5cf",
|
||||
cursorAccent: "#303446",
|
||||
selectionBackground: "rgba(202, 158, 230, 0.3)",
|
||||
|
||||
// ANSI colors
|
||||
black: "#51576d",
|
||||
red: "#e78284",
|
||||
green: "#a6d189",
|
||||
yellow: "#e5c890",
|
||||
blue: "#8caaee",
|
||||
magenta: "#f4b8e4",
|
||||
cyan: "#81c8be",
|
||||
white: "#b5bfe2",
|
||||
|
||||
// Bright ANSI colors
|
||||
brightBlack: "#626880",
|
||||
brightRed: "#e78284",
|
||||
brightGreen: "#a6d189",
|
||||
brightYellow: "#e5c890",
|
||||
brightBlue: "#8caaee",
|
||||
brightMagenta: "#f4b8e4",
|
||||
brightCyan: "#81c8be",
|
||||
brightWhite: "#a5adce",
|
||||
},
|
||||
});
|
||||
|
||||
// Create and load the FitAddon
|
||||
var fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
// Create and load the TerminadoAddon
|
||||
var terminadoAddon = new TerminadoAddon();
|
||||
term.loadAddon(terminadoAddon);
|
||||
|
||||
// Open terminal in the container
|
||||
var terminalContainer = document.getElementById("terminal");
|
||||
term.open(terminalContainer);
|
||||
|
||||
// Build websocket URL
|
||||
var protocol = location.protocol === "https:" ? "wss://" : "ws://";
|
||||
var socketURL =
|
||||
protocol +
|
||||
location.hostname +
|
||||
(location.port ? ":" + location.port : "") +
|
||||
window.WEBSOCKET_PATH;
|
||||
var sock = new WebSocket(socketURL);
|
||||
|
||||
// Fit-once strategy
|
||||
var fitDone = false;
|
||||
function fitOnceIfReady() {
|
||||
if (fitDone) return;
|
||||
if (!terminalContainer) return;
|
||||
var w = terminalContainer.clientWidth;
|
||||
var h = terminalContainer.clientHeight;
|
||||
if (!w || !h) return;
|
||||
|
||||
try {
|
||||
fitAddon.fit();
|
||||
fitDone = true;
|
||||
} catch (e) {
|
||||
console.error("Fit error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule fit
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
requestAnimationFrame(fitOnceIfReady);
|
||||
} else {
|
||||
window.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
function () {
|
||||
requestAnimationFrame(fitOnceIfReady);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
window.addEventListener(
|
||||
"load",
|
||||
function () {
|
||||
requestAnimationFrame(fitOnceIfReady);
|
||||
setTimeout(fitOnceIfReady, 150);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
// Send size and auto-launch command
|
||||
var autoSent = false;
|
||||
function sendSizeAndCommandOnce() {
|
||||
if (autoSent) return;
|
||||
if (!sock || sock.readyState !== WebSocket.OPEN) return;
|
||||
if (!fitDone) return;
|
||||
|
||||
var rows = term.rows || 0;
|
||||
var cols = term.cols || 0;
|
||||
|
||||
if (!rows || !cols) {
|
||||
var approxCharW = 9;
|
||||
var approxCharH = 18;
|
||||
cols = Math.max(
|
||||
1,
|
||||
Math.floor(terminalContainer.clientWidth / approxCharW),
|
||||
);
|
||||
rows = Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
terminalContainer.clientHeight / approxCharH,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (rows > 0 && cols > 0) {
|
||||
try {
|
||||
terminadoAddon.sendSize(rows, cols);
|
||||
terminadoAddon.sendCommand("socktop -P local\r");
|
||||
autoSent = true;
|
||||
} catch (e) {
|
||||
console.error("Failed to send initial commands:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket event handlers
|
||||
sock.addEventListener("open", function () {
|
||||
terminadoAddon.attach(sock, true, true);
|
||||
requestAnimationFrame(function () {
|
||||
fitOnceIfReady();
|
||||
setTimeout(sendSizeAndCommandOnce, 120);
|
||||
});
|
||||
});
|
||||
|
||||
function onFirstMessage() {
|
||||
fitOnceIfReady();
|
||||
try {
|
||||
sock.removeEventListener("message", onFirstMessage);
|
||||
} catch (e) {}
|
||||
setTimeout(sendSizeAndCommandOnce, 40);
|
||||
}
|
||||
sock.addEventListener("message", onFirstMessage);
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", function () {
|
||||
try {
|
||||
fitAddon.fit();
|
||||
} catch (e) {
|
||||
console.error("Resize fit error:", e);
|
||||
}
|
||||
});
|
||||
|
||||
sock.addEventListener("error", function (err) {
|
||||
console.error("WebSocket error:", err);
|
||||
});
|
||||
|
||||
sock.addEventListener("close", function () {
|
||||
console.log("WebSocket closed");
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user