Compare commits
2 Commits
053c9e8431
...
6539a7f2c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6539a7f2c4 | |||
| 790cfc581a |
+2
-2
@@ -8,12 +8,12 @@
|
|||||||
href="https://assets.jdbnet.co.uk/projects/ssh.png"
|
href="https://assets.jdbnet.co.uk/projects/ssh.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<meta name="theme-color" content="#0f1419" />
|
<meta name="theme-color" content="#0d1117" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap"
|
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<title>SSH</title>
|
<title>SSH</title>
|
||||||
|
|||||||
+32
-1
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { Folder, Pencil, Trash2 } from "lucide-vue-next";
|
import { Folder, Pencil, Trash2, Radio } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
type HostRow,
|
type HostRow,
|
||||||
@@ -37,6 +37,22 @@ const tabs = ref<{ id: string; hostId: number; label: string }[]>([]);
|
|||||||
const activePanes = ref<string[]>([]);
|
const activePanes = ref<string[]>([]);
|
||||||
const draggedTabId = ref<string | null>(null);
|
const draggedTabId = ref<string | null>(null);
|
||||||
|
|
||||||
|
const broadcastMode = ref(false);
|
||||||
|
const tabRefs = ref<Record<string, any>>({});
|
||||||
|
function setTabRef(el: any, id: string) {
|
||||||
|
if (el) tabRefs.value[id] = el;
|
||||||
|
else delete tabRefs.value[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBroadcast(data: string, sourceId: string) {
|
||||||
|
if (!broadcastMode.value) return;
|
||||||
|
for (const paneId of activePanes.value) {
|
||||||
|
if (paneId !== sourceId && tabRefs.value[paneId]) {
|
||||||
|
tabRefs.value[paneId].sendData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onTabDragStart(id: string) {
|
function onTabDragStart(id: string) {
|
||||||
draggedTabId.value = id;
|
draggedTabId.value = id;
|
||||||
}
|
}
|
||||||
@@ -744,6 +760,19 @@ async function deleteIdentityRow(id: number) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
v-if="activePanes.length > 1"
|
||||||
|
type="button"
|
||||||
|
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex border transition-colors"
|
||||||
|
:class="broadcastMode ? 'border-accent bg-accent/10 text-accent' : 'border-slate-800 text-slate-400 hover:border-slate-700 hover:text-white'"
|
||||||
|
@click="broadcastMode = !broadcastMode"
|
||||||
|
title="Broadcast input to all visible terminals"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<Radio class="h-3.5 w-3.5" />
|
||||||
|
Broadcast
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex"
|
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex"
|
||||||
@@ -1067,9 +1096,11 @@ async function deleteIdentityRow(id: number) {
|
|||||||
class="flex-1 min-w-0 h-full"
|
class="flex-1 min-w-0 h-full"
|
||||||
>
|
>
|
||||||
<TabContent
|
<TabContent
|
||||||
|
:ref="(el) => setTabRef(el, t.id)"
|
||||||
:host-id="t.hostId"
|
:host-id="t.hostId"
|
||||||
:visible="activePanes.includes(t.id)"
|
:visible="activePanes.includes(t.id)"
|
||||||
:show-sftp="showSftpPanel"
|
:show-sftp="showSftpPanel"
|
||||||
|
@broadcast-data="(data: string) => handleBroadcast(data, t.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ const props = defineProps<{
|
|||||||
showSftp: boolean;
|
showSftp: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'broadcast-data', data: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
sendData: (data: string) => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(new TextEncoder().encode(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const termEl = ref<HTMLElement | null>(null);
|
const termEl = ref<HTMLElement | null>(null);
|
||||||
const status = ref("Connecting…");
|
const status = ref("Connecting…");
|
||||||
const connId = ref<string | null>(null);
|
const connId = ref<string | null>(null);
|
||||||
@@ -79,12 +91,13 @@ onMounted(async () => {
|
|||||||
|
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontFamily: "IBM Plex Mono, monospace",
|
fontFamily: "DM Mono, ui-monospace, monospace",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
theme: {
|
theme: {
|
||||||
background: "#0a0e12",
|
background: "#0d1117",
|
||||||
foreground: "#e2e8f0",
|
foreground: "#e6edf3",
|
||||||
cursor: "#3d9aed",
|
cursor: "#1ebe8a",
|
||||||
|
selectionBackground: "rgba(30, 190, 138, 0.3)",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
fit = new FitAddon();
|
fit = new FitAddon();
|
||||||
@@ -96,6 +109,7 @@ onMounted(async () => {
|
|||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(new TextEncoder().encode(data));
|
ws.send(new TextEncoder().encode(data));
|
||||||
}
|
}
|
||||||
|
emit("broadcast-data", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
term.onResize(({ cols, rows }) => {
|
term.onResize(({ cols, rows }) => {
|
||||||
@@ -185,7 +199,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="termEl"
|
ref="termEl"
|
||||||
class="h-full min-h-[320px] rounded-lg border border-slate-800 bg-[#0a0e12] p-1"
|
class="h-full min-h-[320px] rounded-lg border border-slate-800 bg-[#0d1117] p-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="showSftp">
|
<template v-if="showSftp">
|
||||||
|
|||||||
@@ -5,18 +5,28 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
surface: {
|
surface: {
|
||||||
DEFAULT: "#0f1419",
|
DEFAULT: "#0d1117",
|
||||||
raised: "#151c24",
|
raised: "#161b22",
|
||||||
overlay: "#1a232e",
|
overlay: "#21262d",
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "#3d9aed",
|
DEFAULT: "#1ebe8a",
|
||||||
muted: "#2a6fa3",
|
muted: "#16966b",
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
200: "#e6edf3",
|
||||||
|
300: "#c9d1d9",
|
||||||
|
400: "#8b949e",
|
||||||
|
500: "#6e7681",
|
||||||
|
600: "#484f58",
|
||||||
|
700: "#30363d",
|
||||||
|
800: "#21262d",
|
||||||
|
900: "#161b22",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["IBM Plex Sans", "system-ui", "sans-serif"],
|
sans: ["Space Grotesk", "system-ui", "sans-serif"],
|
||||||
mono: ["IBM Plex Mono", "ui-monospace", "monospace"],
|
mono: ["DM Mono", "ui-monospace", "monospace"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user