mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: convert serial console into the serial plugin
This commit is contained in:
1 parent
708eedb861
commit
791d8656a1
121 files changed
+2216
-1425
No files matched your search
@@ -1,195 +0,0 @@
|
||||
import { getErrorMessage } from "../utils/error-message.js";
|
||||
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
||||
import { SerialPort } from "serialport";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { DataCrypto } from "../utils/data-crypto.js";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
import { parseWsMessage } from "../utils/ws-message.js";
|
||||
import { extractWebSocketToken } from "../utils/ws-auth.js";
|
||||
|
||||
interface SerialConnectData {
|
||||
path: string;
|
||||
baudRate: number;
|
||||
dataBits?: 5 | 6 | 7 | 8;
|
||||
stopBits?: 1 | 2;
|
||||
parity?: "none" | "even" | "odd";
|
||||
}
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
const wss = new WebSocketServer({ host: "127.0.0.1", port: 30011 });
|
||||
|
||||
wss.on("error", (error) => {
|
||||
sshLogger.error("Serial WebSocket server error", error, {
|
||||
operation: "wss_error",
|
||||
});
|
||||
});
|
||||
|
||||
wss.on("connection", async (ws: WebSocket, req) => {
|
||||
let userId: string | undefined;
|
||||
|
||||
try {
|
||||
const token = extractWebSocketToken(req);
|
||||
|
||||
if (!token) {
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
if (!payload?.userId || payload.pendingTOTP) {
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
userId = payload.userId;
|
||||
} catch {
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const dataKey = DataCrypto.getUserDataKey(userId);
|
||||
if (!dataKey) {
|
||||
ws.send(JSON.stringify({ type: "error", data: "Data locked" }));
|
||||
ws.close(1008, "Data access required");
|
||||
return;
|
||||
}
|
||||
|
||||
let port: SerialPort | null = null;
|
||||
|
||||
const send = (msg: object) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (port?.isOpen) {
|
||||
port.close();
|
||||
}
|
||||
port = null;
|
||||
};
|
||||
|
||||
ws.on("message", async (raw: RawData) => {
|
||||
let type: string;
|
||||
let data: unknown;
|
||||
try {
|
||||
({ type, data } = parseWsMessage(raw));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case "list_ports": {
|
||||
try {
|
||||
const ports = await SerialPort.list();
|
||||
send({ type: "ports_list", data: ports });
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
data: getErrorMessage(err, "Failed to list ports"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "connect": {
|
||||
if (port?.isOpen) {
|
||||
port.close();
|
||||
port = null;
|
||||
}
|
||||
|
||||
const cfg = data as SerialConnectData;
|
||||
if (!cfg?.path || !cfg?.baudRate) {
|
||||
send({ type: "error", data: "Missing port path or baud rate" });
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
port = new SerialPort({
|
||||
path: cfg.path,
|
||||
baudRate: cfg.baudRate,
|
||||
dataBits: cfg.dataBits ?? 8,
|
||||
stopBits: cfg.stopBits ?? 1,
|
||||
parity: cfg.parity ?? "none",
|
||||
autoOpen: false,
|
||||
});
|
||||
|
||||
port.open((err) => {
|
||||
if (err) {
|
||||
sshLogger.error("Serial port open failed", err, {
|
||||
operation: "serial_open",
|
||||
path: cfg.path,
|
||||
userId,
|
||||
});
|
||||
send({ type: "error", data: err.message });
|
||||
port = null;
|
||||
return;
|
||||
}
|
||||
sshLogger.info("Serial port opened", {
|
||||
operation: "serial_open",
|
||||
path: cfg.path,
|
||||
baudRate: cfg.baudRate,
|
||||
userId,
|
||||
});
|
||||
send({ type: "connected" });
|
||||
});
|
||||
|
||||
port.on("data", (chunk: Buffer) => {
|
||||
send({ type: "data", data: chunk.toString("binary") });
|
||||
});
|
||||
|
||||
port.on("error", (err) => {
|
||||
send({ type: "error", data: err.message });
|
||||
});
|
||||
|
||||
port.on("close", () => {
|
||||
send({ type: "disconnected" });
|
||||
port = null;
|
||||
});
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
data: getErrorMessage(err, "Failed to open serial port"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "input": {
|
||||
if (!port?.isOpen) break;
|
||||
const input = typeof data === "string" ? data : "";
|
||||
if (!input) break;
|
||||
port.write(Buffer.from(input, "binary"), (err) => {
|
||||
if (err) {
|
||||
send({ type: "error", data: err.message });
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "disconnect": {
|
||||
cleanup();
|
||||
send({ type: "disconnected" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
sshLogger.error("Error handling serial WebSocket message", err, {
|
||||
operation: "serial_message_handler_error",
|
||||
userId,
|
||||
messageType: type,
|
||||
});
|
||||
send({ type: "error", data: "Failed to process message" });
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
ws.on("error", () => {
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ import * as serviceRegistry from "./service-registry.js";
|
||||
import type { ServiceRegistration } from "./service-registry.js";
|
||||
import * as secretRegistry from "./secret-registry.js";
|
||||
import type { SecretRegistration } from "./secret-registry.js";
|
||||
import { assertCapability } from "./permissions.js";
|
||||
import { assertCapability, hasCapability } from "./permissions.js";
|
||||
import { getActor, runAsActor } from "./actor.js";
|
||||
import { DisposableBag } from "./disposables.js";
|
||||
import {
|
||||
@@ -322,6 +322,27 @@ export function createPluginContext(
|
||||
},
|
||||
);
|
||||
|
||||
const capabilitiesRequire = async (capability: string) => {
|
||||
try {
|
||||
await assertCapability(pluginId, capability, declared);
|
||||
} catch (error) {
|
||||
await writeAudit(
|
||||
manifest,
|
||||
{ action: "capability_require", details: () => capability },
|
||||
{
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
await writeAudit(
|
||||
manifest,
|
||||
{ action: "capability_require", details: () => capability },
|
||||
{ success: true },
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
manifest,
|
||||
@@ -626,6 +647,11 @@ export function createPluginContext(
|
||||
require: (permission) => createRbacMiddleware(manifest, permission),
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
has: (capability) => hasCapability(pluginId, capability, declared),
|
||||
require: capabilitiesRequire,
|
||||
},
|
||||
|
||||
disposables: {
|
||||
add: (dispose) => {
|
||||
handle.bag.add(dispose, "plugin resource");
|
||||
|
||||
@@ -309,12 +309,12 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
// plugins/ssh-terminal, plugins/docker, plugins/host-metrics,
|
||||
// plugins/file-manager and plugins/tmux-monitor.
|
||||
// Every other plugin (AI, Proxmox, Remote Desktop, Fleets, Automations,
|
||||
// Network Topology, Workspaces, Web Endpoint, Tunnels) is absent for the
|
||||
// same reason: each one serves its routes under /plugin-api/<id>/ through
|
||||
// ctx.http on activate, so disabling it answers 503 instead of leaving a
|
||||
// dead import here. Automations' scheduler and tunnel autostart also
|
||||
// start from their own activate() rather than here.
|
||||
await import("./hosts/serial.js");
|
||||
// Network Topology, Workspaces, Web Endpoint, Tunnels, Serial) is absent
|
||||
// for the same reason: each one serves its routes under /plugin-api/<id>/
|
||||
// (or a WS route under /plugin-ws/<id>/) through ctx on activate, so
|
||||
// disabling it answers 503 instead of leaving a dead import here.
|
||||
// Automations' scheduler and tunnel autostart also start from their own
|
||||
// activate() rather than here.
|
||||
await import("./services/dashboard.js");
|
||||
await import("./services/homepage.js");
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* ctx.capabilities: a generic check for privileged code no ctx method wraps,
|
||||
* used first by the serial plugin to gate opening a physical device. Every
|
||||
* `require` call is audited, allowed or refused, the same as a guarded ctx
|
||||
* method.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const grants = new Map<string, string[]>();
|
||||
const auditEntries: Array<Record<string, unknown>> = [];
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginPermissionGrantRepository: () => ({
|
||||
listByPlugin: async (pluginId: string) =>
|
||||
(grants.get(pluginId) ?? []).map((capability) => ({
|
||||
pluginId,
|
||||
capability,
|
||||
})),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/audit-logger.js", () => ({
|
||||
logAudit: async (entry: Record<string, unknown>) => {
|
||||
auditEntries.push(entry);
|
||||
},
|
||||
}));
|
||||
|
||||
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
|
||||
import { invalidatePluginPermissionCache } from "../../plugins/permissions.js";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
|
||||
function manifestFor(pluginId: string, capabilities: string[]): PluginManifest {
|
||||
return {
|
||||
id: pluginId,
|
||||
name: pluginId,
|
||||
version: "1.0.0",
|
||||
description: "",
|
||||
author: { name: "test" },
|
||||
license: "MIT",
|
||||
category: "Productivity",
|
||||
engine: { termix: ">=2.9.0", api: "1" },
|
||||
capabilities,
|
||||
} as PluginManifest;
|
||||
}
|
||||
|
||||
function contextFor(pluginId: string, capabilities: string[]) {
|
||||
const manifest = manifestFor(pluginId, capabilities);
|
||||
const handle = createPluginHandle(pluginId, { activate: () => {} });
|
||||
return { ctx: createPluginContext(manifest, handle), handle };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
grants.clear();
|
||||
auditEntries.length = 0;
|
||||
invalidatePluginPermissionCache();
|
||||
});
|
||||
|
||||
describe("ctx.capabilities", () => {
|
||||
it("has() is false when the manifest never declared the capability", async () => {
|
||||
grants.set("demo", ["device:serial"]);
|
||||
const { ctx } = contextFor("demo", []);
|
||||
|
||||
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("has() is false when declared but not granted", async () => {
|
||||
grants.set("demo", []);
|
||||
const { ctx } = contextFor("demo", ["device:serial"]);
|
||||
|
||||
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("has() is true when declared and granted", async () => {
|
||||
grants.set("demo", ["device:serial"]);
|
||||
const { ctx } = contextFor("demo", ["device:serial"]);
|
||||
|
||||
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("require() throws and audits a refusal without the capability", async () => {
|
||||
grants.set("demo", []);
|
||||
const { ctx } = contextFor("demo", []);
|
||||
|
||||
await expect(ctx.capabilities.require("device:serial")).rejects.toThrow(
|
||||
/device:serial/,
|
||||
);
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_capability_require",
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("require() resolves and audits success when granted", async () => {
|
||||
grants.set("demo", ["device:serial"]);
|
||||
const { ctx } = contextFor("demo", ["device:serial"]);
|
||||
|
||||
await expect(
|
||||
ctx.capabilities.require("device:serial"),
|
||||
).resolves.toBeUndefined();
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_capability_require",
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,10 +30,6 @@ const swaggerOptions: SwaggerJSDocOptions = {
|
||||
url: "http://localhost:30006",
|
||||
description: "Dashboard server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30011",
|
||||
description: "Serial connection server",
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
|
||||
+1
-12
@@ -292,14 +292,6 @@ export type KnownTabType =
|
||||
*/
|
||||
export type TabType = KnownTabType | (string & {});
|
||||
|
||||
export type SerialConfig = {
|
||||
path: string;
|
||||
baudRate: number;
|
||||
dataBits: 5 | 6 | 7 | 8;
|
||||
stopBits: 1 | 2;
|
||||
parity: "none" | "even" | "odd";
|
||||
};
|
||||
|
||||
export type Tab = {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
@@ -319,7 +311,6 @@ export type Tab = {
|
||||
data?: Record<string, unknown>;
|
||||
/** Which collab room a collab tab is showing. */
|
||||
collabRoomId?: string;
|
||||
serialConfig?: SerialConfig;
|
||||
/** Present only on a split-screen container tab. Pane ids reference live child tabs. */
|
||||
splitConfig?: SplitTabConfig;
|
||||
/** Hides this session from the top-level tab bar while it belongs to a split tab. */
|
||||
@@ -423,7 +414,7 @@ export type WorkspaceTabSnapshot = {
|
||||
/** Stable key within the saved tab list, not the live Tab.id (which is regenerated on every open). */
|
||||
slotId: string;
|
||||
type: TabType;
|
||||
/** Set for host-bound tab types, resolved by Host.syncId on apply. Never set for "serial". */
|
||||
/** Set for host-bound tab types, resolved by Host.syncId on apply. */
|
||||
hostSyncId?: string | null;
|
||||
/** Denormalized snapshot for display and graceful-skip messaging if the host is later deleted. */
|
||||
hostNameSnapshot?: string | null;
|
||||
@@ -435,8 +426,6 @@ export type WorkspaceTabSnapshot = {
|
||||
fleetId?: number;
|
||||
/** The tab's plugin payload. */
|
||||
data?: Record<string, unknown>;
|
||||
/** Only present when type === "serial". Fully self-contained, no host resolution needed. */
|
||||
serialConfig?: SerialConfig;
|
||||
};
|
||||
|
||||
/** One dock's arrangement. `view` is a RailView, or null when the dock is closed. */
|
||||
|
||||
@@ -66,9 +66,6 @@ const QuickConnectPanel = lazy(() =>
|
||||
default: m.QuickConnectPanel,
|
||||
})),
|
||||
);
|
||||
const SerialPanel = lazy(() =>
|
||||
import("@/sidebar/SerialPanel").then((m) => ({ default: m.SerialPanel })),
|
||||
);
|
||||
const SplitScreenPanel = lazy(() =>
|
||||
import("@/sidebar/SplitScreenPanel").then((m) => ({
|
||||
default: m.SplitScreenPanel,
|
||||
@@ -146,7 +143,6 @@ import type {
|
||||
HostFolder,
|
||||
ThemeId,
|
||||
FontSizeId,
|
||||
SerialConfig,
|
||||
WorkspacePayload,
|
||||
} from "@/types/ui-types";
|
||||
import { applyAccentColor, applyFontSize, PANE_COUNTS } from "@/lib/theme";
|
||||
@@ -1263,12 +1259,6 @@ export function AppShell({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === "serial" && snapshot.serialConfig) {
|
||||
const newTabId = openSerialTab(snapshot.serialConfig);
|
||||
slotIdToNewTabId.set(snapshot.slotId, newTabId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === "singleton") {
|
||||
openSingletonTab(
|
||||
snapshot.type,
|
||||
@@ -1562,7 +1552,6 @@ export function AppShell({
|
||||
savedLabel?: string;
|
||||
initialFilePath?: string;
|
||||
initialPath?: string;
|
||||
serialConfig?: SerialConfig;
|
||||
joinSharedSessionId?: string | null;
|
||||
joinShareId?: string | null;
|
||||
collabRoomId?: string;
|
||||
@@ -1600,7 +1589,6 @@ export function AppShell({
|
||||
const savedLabel = restore?.savedLabel;
|
||||
const initialFilePath = restore?.initialFilePath;
|
||||
const initialPath = restore?.initialPath;
|
||||
const serialConfig = restore?.serialConfig;
|
||||
const joinSharedSessionId = restore?.joinSharedSessionId ?? null;
|
||||
const joinShareId = restore?.joinShareId ?? null;
|
||||
// A saved label that doesn't match the bare host name or the auto-numbered pattern is a custom label
|
||||
@@ -1628,7 +1616,6 @@ export function AppShell({
|
||||
joinShareId,
|
||||
initialFilePath,
|
||||
initialPath,
|
||||
serialConfig,
|
||||
collabRoomId: restore?.collabRoomId,
|
||||
},
|
||||
];
|
||||
@@ -1667,7 +1654,6 @@ export function AppShell({
|
||||
joinShareId,
|
||||
initialFilePath,
|
||||
initialPath,
|
||||
serialConfig,
|
||||
collabRoomId: restore?.collabRoomId,
|
||||
data: options?.data,
|
||||
},
|
||||
@@ -1717,53 +1703,6 @@ export function AppShell({
|
||||
[loadHosts, t],
|
||||
);
|
||||
|
||||
function openSerialTab(config: SerialConfig): string {
|
||||
const pseudoHost: Host = {
|
||||
id: `serial-${Date.now()}`,
|
||||
name: config.path
|
||||
? `${config.path} (${config.baudRate})`
|
||||
: `Serial (${config.baudRate})`,
|
||||
username: "",
|
||||
ip: "",
|
||||
port: 0,
|
||||
folder: "",
|
||||
online: false,
|
||||
cpu: null,
|
||||
ram: null,
|
||||
lastAccess: new Date().toISOString(),
|
||||
authType: "none",
|
||||
enableTerminal: false,
|
||||
enableCommandHistory: false,
|
||||
enableTunnel: false,
|
||||
enableFileManager: false,
|
||||
enableDocker: false,
|
||||
enableProxmox: false,
|
||||
enableProxmoxStats: false,
|
||||
enableTmuxMonitor: false,
|
||||
enableTerminalToolbar: false,
|
||||
enableAiAssistant: false,
|
||||
enableSsh: false,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
sshPort: 22,
|
||||
rdpPort: 3389,
|
||||
vncPort: 5900,
|
||||
telnetPort: 23,
|
||||
serverTunnels: [],
|
||||
quickActions: [],
|
||||
};
|
||||
const instanceId =
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
return openTab(pseudoHost, "serial", {
|
||||
instanceId,
|
||||
restoredSessionId: null,
|
||||
serialConfig: config,
|
||||
});
|
||||
}
|
||||
|
||||
/** A tab type that opens a fresh tab every time (a local shell). */
|
||||
function openMultiInstanceTab(type: TabType): string {
|
||||
const instanceId =
|
||||
@@ -2500,15 +2439,6 @@ export function AppShell({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "serial" && (
|
||||
<SerialPanel
|
||||
onConnect={(config) => {
|
||||
openSerialTab(config);
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{railView === "quick-connect" && (
|
||||
<QuickConnectPanel
|
||||
onConnect={(host, type) => {
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useXTerm } from "react-xtermjs";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { websocketAuthProtocols } from "@/lib/ws-auth";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme";
|
||||
import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes";
|
||||
import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles";
|
||||
import type { SerialConfig } from "@/types/ui-types";
|
||||
import type { SerialHandle } from "./serial-types";
|
||||
|
||||
type WebSerialPort = {
|
||||
open(options: {
|
||||
baudRate: number;
|
||||
dataBits?: number;
|
||||
stopBits?: number;
|
||||
parity?: string;
|
||||
}): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
readable: ReadableStream<Uint8Array> | null;
|
||||
writable: WritableStream<Uint8Array> | null;
|
||||
};
|
||||
|
||||
interface SerialProps {
|
||||
config: SerialConfig;
|
||||
isVisible: boolean;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
{ config, isVisible, instanceId },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { theme: appTheme } = useTheme();
|
||||
const { instance: terminal, ref: xtermRef } = useXTerm();
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const connectedRef = useRef(false);
|
||||
const webSerialReaderRef = useRef<ReadableStreamDefaultReader | null>(null);
|
||||
const webSerialWriterRef = useRef<WritableStreamDefaultWriter | null>(null);
|
||||
const webSerialPortRef = useRef<WebSerialPort | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const useWebSerial = !isElectron();
|
||||
|
||||
const write = useCallback(
|
||||
(text: string) => {
|
||||
terminal?.write(text);
|
||||
},
|
||||
[terminal],
|
||||
);
|
||||
|
||||
// ── Theme sync ─────────────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminal) return;
|
||||
const themeColors = resolveTermixThemeColors("termix", appTheme);
|
||||
const fontConfig = TERMINAL_FONTS.find(
|
||||
(f) => f.value === DEFAULT_TERMINAL_CONFIG.fontFamily,
|
||||
);
|
||||
ensureTerminalFontsLoaded(fontConfig?.value ?? TERMINAL_FONTS[0].value);
|
||||
terminal.options.theme = {
|
||||
background: themeColors.background,
|
||||
foreground: themeColors.foreground,
|
||||
cursor: themeColors.cursor,
|
||||
cursorAccent: themeColors.cursorAccent,
|
||||
selectionBackground: themeColors.selectionBackground,
|
||||
selectionForeground: themeColors.selectionForeground,
|
||||
black: themeColors.black,
|
||||
red: themeColors.red,
|
||||
green: themeColors.green,
|
||||
yellow: themeColors.yellow,
|
||||
blue: themeColors.blue,
|
||||
magenta: themeColors.magenta,
|
||||
cyan: themeColors.cyan,
|
||||
white: themeColors.white,
|
||||
brightBlack: themeColors.brightBlack,
|
||||
brightRed: themeColors.brightRed,
|
||||
brightGreen: themeColors.brightGreen,
|
||||
brightYellow: themeColors.brightYellow,
|
||||
brightBlue: themeColors.brightBlue,
|
||||
brightMagenta: themeColors.brightMagenta,
|
||||
brightCyan: themeColors.brightCyan,
|
||||
brightWhite: themeColors.brightWhite,
|
||||
};
|
||||
terminal.options.fontFamily =
|
||||
fontConfig?.fallback ?? TERMINAL_FONTS[0].fallback;
|
||||
terminal.options.fontSize = DEFAULT_TERMINAL_CONFIG.fontSize;
|
||||
}, [terminal, appTheme]);
|
||||
|
||||
// ── WebSocket (Electron) path ──────────────────────────────────────────
|
||||
|
||||
const buildWsUrl = useCallback(() => {
|
||||
// Serial is always local -- the device is physically attached to this
|
||||
// desktop machine, so it never routes through a remote server.
|
||||
return "ws://127.0.0.1:30011";
|
||||
}, []);
|
||||
|
||||
const disconnectWs = useCallback(() => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = null;
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
connectedRef.current = false;
|
||||
}, []);
|
||||
|
||||
const connectWs = useCallback(() => {
|
||||
disconnectWs();
|
||||
const url = buildWsUrl();
|
||||
if (!url) {
|
||||
write(`\r\n\x1b[31m${t("serial.errorNoServerUrl")}\x1b[0m\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(
|
||||
url,
|
||||
websocketAuthProtocols(localStorage.getItem("jwt")),
|
||||
);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ type: "connect", data: config }));
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data as string) as {
|
||||
type: string;
|
||||
data?: unknown;
|
||||
};
|
||||
switch (msg.type) {
|
||||
case "connected":
|
||||
connectedRef.current = true;
|
||||
write(
|
||||
`\x1b[32m${t("serial.connected", { path: config.path, baud: config.baudRate })}\x1b[0m\r\n`,
|
||||
);
|
||||
break;
|
||||
case "data":
|
||||
if (typeof msg.data === "string") write(msg.data);
|
||||
break;
|
||||
case "disconnected":
|
||||
connectedRef.current = false;
|
||||
write(`\r\n\x1b[33m${t("serial.disconnected")}\x1b[0m\r\n`);
|
||||
break;
|
||||
case "error":
|
||||
write(
|
||||
`\r\n\x1b[31m${t("serial.connectionError")}: ${msg.data as string}\x1b[0m\r\n`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed messages
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
connectedRef.current = false;
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
write(`\r\n\x1b[31m${t("serial.wsError")}\x1b[0m\r\n`);
|
||||
};
|
||||
}, [buildWsUrl, config, disconnectWs, t, write]);
|
||||
|
||||
// ── Web Serial API path ────────────────────────────────────────────────
|
||||
|
||||
const disconnectWebSerial = useCallback(async () => {
|
||||
try {
|
||||
webSerialReaderRef.current?.cancel();
|
||||
webSerialWriterRef.current?.releaseLock();
|
||||
await webSerialPortRef.current?.close();
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
webSerialReaderRef.current = null;
|
||||
webSerialWriterRef.current = null;
|
||||
webSerialPortRef.current = null;
|
||||
connectedRef.current = false;
|
||||
}, []);
|
||||
|
||||
const connectWebSerial = useCallback(async () => {
|
||||
await disconnectWebSerial();
|
||||
|
||||
if (!("serial" in navigator)) {
|
||||
write(`\r\n\x1b[31m${t("serial.notSupported")}\x1b[0m\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const serial = navigator.serial as {
|
||||
requestPort(): Promise<WebSerialPort>;
|
||||
};
|
||||
const port = await serial.requestPort();
|
||||
await port.open({
|
||||
baudRate: config.baudRate,
|
||||
dataBits: config.dataBits,
|
||||
stopBits: config.stopBits,
|
||||
parity: config.parity === "none" ? "none" : config.parity,
|
||||
});
|
||||
|
||||
webSerialPortRef.current = port;
|
||||
connectedRef.current = true;
|
||||
write(
|
||||
`\x1b[32m${t("serial.connected", { path: config.path || "serial", baud: config.baudRate })}\x1b[0m\r\n`,
|
||||
);
|
||||
|
||||
const reader = port.readable!.getReader();
|
||||
webSerialReaderRef.current = reader;
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
write(decoder.decode(value));
|
||||
}
|
||||
} catch {
|
||||
// port closed
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
connectedRef.current = false;
|
||||
write(`\r\n\x1b[33m${t("serial.disconnected")}\x1b[0m\r\n`);
|
||||
}
|
||||
})();
|
||||
|
||||
const writer = port.writable!.getWriter();
|
||||
webSerialWriterRef.current = writer;
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name !== "NotFoundError") {
|
||||
write(
|
||||
`\r\n\x1b[31m${t("serial.connectionError")}: ${err.message}\x1b[0m\r\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [config, disconnectWebSerial, t, write]);
|
||||
|
||||
// ── Unified connect/disconnect ─────────────────────────────────────────
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (useWebSerial) {
|
||||
connectWebSerial();
|
||||
} else {
|
||||
connectWs();
|
||||
}
|
||||
}, [useWebSerial, connectWebSerial, connectWs]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (useWebSerial) {
|
||||
disconnectWebSerial();
|
||||
} else {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ type: "disconnect" }));
|
||||
}
|
||||
disconnectWs();
|
||||
}
|
||||
}, [useWebSerial, disconnectWebSerial, disconnectWs]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
connect,
|
||||
disconnect,
|
||||
isConnected: () => connectedRef.current,
|
||||
sendInput: (data: string) => {
|
||||
if (useWebSerial) {
|
||||
const encoder = new TextEncoder();
|
||||
webSerialWriterRef.current?.write(encoder.encode(data)).catch(() => {});
|
||||
} else if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ type: "input", data }));
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// ── Terminal setup ─────────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminal) return;
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
fitAddonRef.current = fitAddon;
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.options.cursorBlink = DEFAULT_TERMINAL_CONFIG.cursorBlink;
|
||||
terminal.options.scrollback = DEFAULT_TERMINAL_CONFIG.scrollback;
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (!connectedRef.current) return;
|
||||
if (useWebSerial) {
|
||||
const encoder = new TextEncoder();
|
||||
webSerialWriterRef.current?.write(encoder.encode(data)).catch(() => {});
|
||||
} else if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ type: "input", data }));
|
||||
}
|
||||
});
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [terminal, instanceId]);
|
||||
|
||||
// ── Fit on visibility change ───────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible || !fitAddonRef.current) return;
|
||||
try {
|
||||
fitAddonRef.current.fit();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
// ── ResizeObserver ─────────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !terminal) return;
|
||||
const observer = new ResizeObserver(() => {
|
||||
try {
|
||||
fitAddonRef.current?.fit();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [terminal]);
|
||||
|
||||
// ── Not supported (Firefox etc.) ──────────────────────────────────────
|
||||
|
||||
if (useWebSerial && !("serial" in navigator)) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-1 gap-3 p-6 text-center">
|
||||
<div className="size-10 rounded-full bg-muted/40 flex items-center justify-center">
|
||||
<TriangleAlert className="size-5 text-muted-foreground/50" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
{t("serial.notSupportedTitle")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground max-w-xs">
|
||||
{t("serial.notSupported")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex h-full w-full">
|
||||
<div ref={xtermRef} className="flex-1 min-h-0" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface SerialHandle {
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
isConnected: () => boolean;
|
||||
sendInput: (data: string) => void;
|
||||
}
|
||||
@@ -11,13 +11,15 @@ interface OriginResolvableHost {
|
||||
const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
|
||||
|
||||
/**
|
||||
* Resolves which backend a given host's interactive connection (SSH,
|
||||
* Docker console, Serial, RDP/VNC/Telnet) should dial: the desktop app's
|
||||
* embedded local backend, or a connected remote sync server.
|
||||
* Resolves which backend a given host's interactive connection (SSH, Docker
|
||||
* console, RDP/VNC/Telnet) should dial: the desktop app's embedded local
|
||||
* backend, or a connected remote sync server. A plugin whose connection is
|
||||
* always local regardless of this setting (serial: the hardware is
|
||||
* physically attached to this desktop machine) skips this helper entirely
|
||||
* and calls app.wsUrl() without an origin option, which defaults to "local".
|
||||
*
|
||||
* Serial always resolves to "local" -- the hardware is physically attached
|
||||
* to this desktop machine. Everything else follows the host's own override
|
||||
* if set, falling back to the desktop-wide default.
|
||||
* Everything else follows the host's own override if set, falling back to
|
||||
* the desktop-wide default.
|
||||
*
|
||||
* RDP/VNC/Telnet are the exception to that fallback: left on Default they
|
||||
* resolve to "remote" rather than following the desktop-wide setting. They
|
||||
@@ -30,9 +32,6 @@ const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
|
||||
export async function resolveConnectionOrigin(
|
||||
host: OriginResolvableHost,
|
||||
): Promise<ConnectionOrigin> {
|
||||
if (host.connectionType === "serial") {
|
||||
return "local";
|
||||
}
|
||||
if (!isElectron()) {
|
||||
return "local";
|
||||
}
|
||||
|
||||
+4
-25
@@ -709,7 +709,6 @@
|
||||
"hostManager": "Host Manager",
|
||||
"credentials": "Credentials",
|
||||
"connections": "Connections",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Keep Menu Open",
|
||||
"collapseSideMenu": "Collapse Menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3092,30 +3091,6 @@
|
||||
"openProxmoxStats": "Open Proxmox Stats",
|
||||
"openTmuxMonitor": "Open Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baud Rate",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parity",
|
||||
"parityNone": "None",
|
||||
"parityEven": "Even",
|
||||
"parityOdd": "Odd",
|
||||
"connect": "Connect to Serial",
|
||||
"disconnect": "Disconnect",
|
||||
"refreshPorts": "Refresh ports",
|
||||
"connected": "Connected to {{path}} at {{baud}} baud",
|
||||
"disconnected": "Serial port disconnected",
|
||||
"connectionError": "Failed to open serial port",
|
||||
"wsError": "WebSocket error",
|
||||
"errorNoServerUrl": "No server URL configured",
|
||||
"notSupportedTitle": "Serial Not Supported",
|
||||
"notSupported": "Serial connections require a browser with Web Serial API support (Chrome, Edge, or Firefox 151+), or the Termix desktop app.",
|
||||
"hideHint": "You can hide the Serial tab under User Profile > Appearance > Sidebar > Navigation.",
|
||||
"browserPickerHint": "Click Connect and your browser will open a port picker to choose the device."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Add Channel",
|
||||
"editChannel": "Edit Channel",
|
||||
@@ -3256,6 +3231,10 @@
|
||||
"title": "Manage certificates",
|
||||
"consequence": "It can request and replace the server certificate."
|
||||
},
|
||||
"device:serial": {
|
||||
"title": "Access serial and USB devices",
|
||||
"consequence": "It can open a physical serial port on the machine running Termix."
|
||||
},
|
||||
"hosts:write": {
|
||||
"title": "Create and change hosts",
|
||||
"consequence": "It can add hosts and edit the ones you already have."
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gasheerbestuurder",
|
||||
"credentials": "Geloofsbriewe",
|
||||
"connections": "Verbindings",
|
||||
"serial": "Reeks",
|
||||
"keepSideMenuOpen": "Hou Kieslys Oop",
|
||||
"collapseSideMenu": "Vou Kieslys in",
|
||||
"roleAdministrator": "Administrateur",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Maak Proxmox-statistieke oop",
|
||||
"openTmuxMonitor": "Maak Tmux-monitor oop"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Reeks",
|
||||
"portLabel": "Hawe",
|
||||
"portPlaceholder": "/dev/ttyUSB0 of COM3",
|
||||
"baudRateLabel": "Baudtempo",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteit",
|
||||
"parityNone": "Geen",
|
||||
"parityEven": "Selfs",
|
||||
"parityOdd": "Vreemd",
|
||||
"connect": "Koppel aan serieel",
|
||||
"disconnect": "Ontkoppel",
|
||||
"refreshPorts": "Verfris poorte",
|
||||
"connected": "Verbind aan {{path}} teen {{baud}} baud",
|
||||
"disconnected": "Seriepoort ontkoppel",
|
||||
"connectionError": "Kon nie die seriepoort oopmaak nie",
|
||||
"wsError": "WebSocket-fout",
|
||||
"errorNoServerUrl": "Geen bediener-URL gekonfigureer nie",
|
||||
"notSupportedTitle": "Serienommer word nie ondersteun nie",
|
||||
"notSupported": "Seriële verbindings vereis 'n blaaier met Web Serial API-ondersteuning (Chrome, Edge of Firefox 151+), of die Termix-lessenaarprogram.",
|
||||
"hideHint": "Jy kan die Seriële oortjie versteek onder Gebruikersprofiel > Voorkoms > Sybalk > Navigasie.",
|
||||
"browserPickerHint": "Klik op Verbind en jou blaaier sal 'n poortkieser oopmaak om die toestel te kies."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Voeg kanaal by",
|
||||
"editChannel": "Wysig kanaal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "مدير المضيفين",
|
||||
"credentials": "بيانات الاعتماد",
|
||||
"connections": "الاتصالات",
|
||||
"serial": "تسلسلي",
|
||||
"keepSideMenuOpen": "أبقِ قائمة الطعام مفتوحة",
|
||||
"collapseSideMenu": "قائمة قابلة للطي",
|
||||
"roleAdministrator": "مسؤول",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "افتح إحصائيات Proxmox",
|
||||
"openTmuxMonitor": "افتح شاشة Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "منفذ تسلسلي",
|
||||
"portLabel": "المنفذ",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "معدل الباود",
|
||||
"dataBitsLabel": "البيانات",
|
||||
"stopBitsLabel": "التوقف",
|
||||
"parityLabel": "التكافؤ",
|
||||
"parityNone": "بلا",
|
||||
"parityEven": "زوجي",
|
||||
"parityOdd": "فردي",
|
||||
"connect": "اتصال",
|
||||
"disconnect": "قطع الاتصال",
|
||||
"refreshPorts": "تحديث المنافذ",
|
||||
"connected": "متصل بـ {{path}} بسرعة {{baud}} باود",
|
||||
"disconnected": "تم قطع الاتصال بالمنفذ التسلسلي",
|
||||
"connectionError": "فشل في فتح المنفذ التسلسلي",
|
||||
"wsError": "خطأ في WebSocket",
|
||||
"errorNoServerUrl": "لم يتم تكوين عنوان URL للخادم",
|
||||
"notSupportedTitle": "المنفذ التسلسلي غير مدعوم",
|
||||
"notSupported": "تتطلب الاتصالات التسلسلية متصفحًا يدعم Web Serial API (Chrome، أو Edge، أو Firefox 151+)، أو تطبيق Termix لسطح المكتب.",
|
||||
"hideHint": "يمكنك إخفاء تبويب المنفذ التسلسلي من الملف الشخصي > المظهر > الشريط الجانبي > التنقل.",
|
||||
"browserPickerHint": "انقر على Connect وسيفتح متصفحك نافذة اختيار المنفذ لتحديد الجهاز."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "إضافة قناة",
|
||||
"editChannel": "تعديل القناة",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Мениджър на домакини",
|
||||
"credentials": "Пълномощия",
|
||||
"connections": "Връзки",
|
||||
"serial": "Сериен",
|
||||
"keepSideMenuOpen": "Запази менюто отворено",
|
||||
"collapseSideMenu": "Свиване на менюто",
|
||||
"roleAdministrator": "Администратор",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Отворете статистиката на Proxmox",
|
||||
"openTmuxMonitor": "Отворете Tmux монитора"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Сериен",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 или COM3",
|
||||
"baudRateLabel": "Скорост на бодове",
|
||||
"dataBitsLabel": "Данни",
|
||||
"stopBitsLabel": "Спри",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Няма",
|
||||
"parityEven": "Дори",
|
||||
"parityOdd": "Странно",
|
||||
"connect": "Свързване към сериен",
|
||||
"disconnect": "Изключване",
|
||||
"refreshPorts": "Обновяване на портове",
|
||||
"connected": "Свързан към {{path}} при {{baud}} бод",
|
||||
"disconnected": "Серийният порт е прекъснат",
|
||||
"connectionError": "Неуспешно отваряне на серийния порт",
|
||||
"wsError": "Грешка в WebSocket",
|
||||
"errorNoServerUrl": "Няма конфигуриран URL адрес на сървъра",
|
||||
"notSupportedTitle": "Серийният номер не се поддържа",
|
||||
"notSupported": "Серийните връзки изискват браузър с поддръжка на Web Serial API (Chrome, Edge или Firefox 151+) или настолното приложение Termix.",
|
||||
"hideHint": "Можете да скриете раздела „Сериен номер“ в Потребителски профил > Външен вид > Странична лента > Навигация.",
|
||||
"browserPickerHint": "Щракнете върху „Свързване“ и браузърът ви ще отвори инструмент за избор на порт, за да изберете устройството."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Добавяне на канал",
|
||||
"editChannel": "Редактиране на канал",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "হোস্ট ম্যানেজার",
|
||||
"credentials": "যোগ্যতা",
|
||||
"connections": "সংযোগ",
|
||||
"serial": "সিরিয়াল",
|
||||
"keepSideMenuOpen": "মেনু খোলা রাখুন",
|
||||
"collapseSideMenu": "মেনু সংকুচিত করুন",
|
||||
"roleAdministrator": "প্রশাসক",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "ওপেন প্রক্সমক্স স্ট্যাটস",
|
||||
"openTmuxMonitor": "ওপেন Tmux মনিটর"
|
||||
},
|
||||
"serial": {
|
||||
"title": "সিরিয়াল",
|
||||
"portLabel": "বন্দর",
|
||||
"portPlaceholder": "/dev/ttyUSB0 অথবা COM3",
|
||||
"baudRateLabel": "বড রেট",
|
||||
"dataBitsLabel": "ডেটা",
|
||||
"stopBitsLabel": "থামুন",
|
||||
"parityLabel": "সমতা",
|
||||
"parityNone": "কোনোটিই না",
|
||||
"parityEven": "এমনকি",
|
||||
"parityOdd": "অদ্ভুত",
|
||||
"connect": "সিরিয়ালের সাথে সংযোগ করুন",
|
||||
"disconnect": "সংযোগ বিচ্ছিন্ন করুন",
|
||||
"refreshPorts": "পোর্টগুলি রিফ্রেশ করুন",
|
||||
"connected": "{{path}} এর সাথে {{baud}} বাউডে সংযুক্ত।",
|
||||
"disconnected": "সিরিয়াল পোর্ট সংযোগ বিচ্ছিন্ন",
|
||||
"connectionError": "সিরিয়াল পোর্ট খুলতে ব্যর্থ হয়েছে",
|
||||
"wsError": "ওয়েব সকেট ত্রুটি",
|
||||
"errorNoServerUrl": "কোন সার্ভার URL কনফিগার করা নেই",
|
||||
"notSupportedTitle": "সিরিয়াল সমর্থিত নয়",
|
||||
"notSupported": "সিরিয়াল সংযোগের জন্য ওয়েব সিরিয়াল এপিআই সমর্থনসহ একটি ব্রাউজার (ক্রোম, এজ, বা ফায়ারফক্স ১৫১+), অথবা টারমিক্স ডেস্কটপ অ্যাপ প্রয়োজন।",
|
||||
"hideHint": "আপনি ইউজার প্রোফাইল > অ্যাপিয়ারেন্স > সাইডবার > নেভিগেশন-এর অধীনে সিরিয়াল ট্যাবটি লুকাতে পারেন।",
|
||||
"browserPickerHint": "কানেক্ট-এ ক্লিক করলে আপনার ব্রাউজারে ডিভাইসটি বেছে নেওয়ার জন্য একটি পোর্ট পিকার খুলবে।"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "চ্যানেল যোগ করুন",
|
||||
"editChannel": "চ্যানেল সম্পাদনা করুন",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestor d'amfitrions",
|
||||
"credentials": "Credencials",
|
||||
"connections": "Connexions",
|
||||
"serial": "Sèrie",
|
||||
"keepSideMenuOpen": "Mantenir el menú obert",
|
||||
"collapseSideMenu": "Menú replegable",
|
||||
"roleAdministrator": "Administrador/a",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Obre les estadístiques de Proxmox",
|
||||
"openTmuxMonitor": "Obre el monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sèrie",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 o COM3",
|
||||
"baudRateLabel": "velocitat de bauds",
|
||||
"dataBitsLabel": "Dades",
|
||||
"stopBitsLabel": "Atura",
|
||||
"parityLabel": "Paritat",
|
||||
"parityNone": "Cap",
|
||||
"parityEven": "Fins i tot",
|
||||
"parityOdd": "Estrany",
|
||||
"connect": "Connecta't al serial",
|
||||
"disconnect": "Desconnecta",
|
||||
"refreshPorts": "Actualitza els ports",
|
||||
"connected": "Connectat a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Port sèrie desconnectat",
|
||||
"connectionError": "No s'ha pogut obrir el port sèrie",
|
||||
"wsError": "Error de WebSocket",
|
||||
"errorNoServerUrl": "No s'ha configurat cap URL del servidor",
|
||||
"notSupportedTitle": "Número de sèrie no compatible",
|
||||
"notSupported": "Les connexions en sèrie requereixen un navegador compatible amb l'API Web Serial (Chrome, Edge o Firefox 151+) o l'aplicació d'escriptori Termix.",
|
||||
"hideHint": "Podeu amagar la pestanya Sèrie a Perfil d'usuari > Aparença > Barra lateral > Navegació.",
|
||||
"browserPickerHint": "Feu clic a Connecta i el navegador obrirà un selector de ports per triar el dispositiu."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Afegeix un canal",
|
||||
"editChannel": "Edita el canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Hostitelský manažer",
|
||||
"credentials": "Pověření",
|
||||
"connections": "Připojení",
|
||||
"serial": "Seriál",
|
||||
"keepSideMenuOpen": "Ponechat nabídku otevřenou",
|
||||
"collapseSideMenu": "Sbalit nabídku",
|
||||
"roleAdministrator": "Správce",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Otevřít statistiky Proxmoxu",
|
||||
"openTmuxMonitor": "Otevřít monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriál",
|
||||
"portLabel": "Přístav",
|
||||
"portPlaceholder": "/dev/ttyUSB0 nebo COM3",
|
||||
"baudRateLabel": "Přenosová rychlost",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Zastávka",
|
||||
"parityLabel": "Parita",
|
||||
"parityNone": "Žádný",
|
||||
"parityEven": "Dokonce",
|
||||
"parityOdd": "Zvláštní",
|
||||
"connect": "Připojení k sériovému",
|
||||
"disconnect": "Odpojit",
|
||||
"refreshPorts": "Obnovit porty",
|
||||
"connected": "Připojeno k {{path}} rychlostí {{baud}} baudu",
|
||||
"disconnected": "Sériový port odpojen",
|
||||
"connectionError": "Nepodařilo se otevřít sériový port",
|
||||
"wsError": "Chyba WebSocketu",
|
||||
"errorNoServerUrl": "Není nakonfigurována žádná adresa URL serveru",
|
||||
"notSupportedTitle": "Sériové číslo není podporováno",
|
||||
"notSupported": "Sériová připojení vyžadují prohlížeč s podporou webového sériového API (Chrome, Edge nebo Firefox 151+) nebo desktopovou aplikaci Termix.",
|
||||
"hideHint": "Kartu Sériové číslo můžete skrýt v části Profil uživatele > Vzhled > Postranní panel > Navigace.",
|
||||
"browserPickerHint": "Klikněte na tlačítko Připojit a prohlížeč otevře výběr portu pro výběr zařízení."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Přidat kanál",
|
||||
"editChannel": "Upravit kanál",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Værtsadministrator",
|
||||
"credentials": "Akkreditiver",
|
||||
"connections": "Forbindelser",
|
||||
"serial": "Seriel",
|
||||
"keepSideMenuOpen": "Hold menuen åben",
|
||||
"collapseSideMenu": "Skjul menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Åbn Proxmox-statistik",
|
||||
"openTmuxMonitor": "Åbn Tmux-skærmen"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriel",
|
||||
"portLabel": "Havn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighed",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Endog",
|
||||
"parityOdd": "Ulige",
|
||||
"connect": "Tilslut til seriel",
|
||||
"disconnect": "Afbryde",
|
||||
"refreshPorts": "Opdater porte",
|
||||
"connected": "Forbundet til {{path}} ved {{baud}} baud",
|
||||
"disconnected": "Seriel port afbrudt",
|
||||
"connectionError": "Kunne ikke åbne seriel port",
|
||||
"wsError": "WebSocket-fejl",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigureret",
|
||||
"notSupportedTitle": "Seriel understøttes ikke",
|
||||
"notSupported": "Serielle forbindelser kræver en browser med Web Serial API-understøttelse (Chrome, Edge eller Firefox 151+) eller Termix-desktopappen.",
|
||||
"hideHint": "Du kan skjule fanen Seriel under Brugerprofil > Udseende > Sidebjælke > Navigation.",
|
||||
"browserPickerHint": "Klik på Opret forbindelse, og din browser åbner en portvælger, hvor du kan vælge enheden."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Tilføj kanal",
|
||||
"editChannel": "Rediger kanal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Host-Manager",
|
||||
"credentials": "Zugangsdaten",
|
||||
"connections": "Verbindungen",
|
||||
"serial": "Seriell",
|
||||
"keepSideMenuOpen": "Menü geöffnet lassen",
|
||||
"collapseSideMenu": "Menü ausklappen",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox-Statistiken öffnen",
|
||||
"openTmuxMonitor": "Tmux-Monitor öffnen"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 oder COM3",
|
||||
"baudRateLabel": "Baudrate",
|
||||
"dataBitsLabel": "Daten",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parität",
|
||||
"parityNone": "Keine",
|
||||
"parityEven": "Gerade",
|
||||
"parityOdd": "Ungerade",
|
||||
"connect": "Mit Seriell verbinden",
|
||||
"disconnect": "Trennen",
|
||||
"refreshPorts": "Ports aktualisieren",
|
||||
"connected": "Verbunden mit {{path}} bei {{baud}} Baud",
|
||||
"disconnected": "Serielle Schnittstelle getrennt",
|
||||
"connectionError": "Fehler beim Öffnen der seriellen Schnittstelle",
|
||||
"wsError": "WebSocket-Fehler",
|
||||
"errorNoServerUrl": "Keine Server-URL konfiguriert",
|
||||
"notSupportedTitle": "Seriell nicht unterstützt",
|
||||
"notSupported": "Serielle Verbindungen erfordern einen Browser mit Web Serial API-Unterstützung (Chrome, Edge oder Firefox 151+) oder die Termix-Desktop-App.",
|
||||
"hideHint": "Sie können den Seriell-Tab unter Benutzerprofil > Erscheinungsbild > Seitenleiste > Navigation ausblenden.",
|
||||
"browserPickerHint": "Klicken Sie auf Verbinden, und Ihr Browser öffnet eine Port-Auswahl, um das Gerät auszuwählen."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Kanal hinzufügen",
|
||||
"editChannel": "Kanal bearbeiten",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Διευθυντής Υποδοχής",
|
||||
"credentials": "Διαπιστευτήρια",
|
||||
"connections": "Συνδέσεις",
|
||||
"serial": "Σειράς",
|
||||
"keepSideMenuOpen": "Διατήρηση μενού ανοιχτού",
|
||||
"collapseSideMenu": "Σύμπτυξη μενού",
|
||||
"roleAdministrator": "Διαχειριστής",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Άνοιγμα στατιστικών Proxmox",
|
||||
"openTmuxMonitor": "Άνοιγμα παρακολούθησης Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Σειράς",
|
||||
"portLabel": "Λιμάνι",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ή COM3",
|
||||
"baudRateLabel": "Ρυθμός Baud",
|
||||
"dataBitsLabel": "Δεδομένα",
|
||||
"stopBitsLabel": "Στάση",
|
||||
"parityLabel": "Ισοτιμία",
|
||||
"parityNone": "Κανένας",
|
||||
"parityEven": "Ακόμη και",
|
||||
"parityOdd": "Περιττός",
|
||||
"connect": "Σύνδεση σε σειριακή σύνδεση",
|
||||
"disconnect": "Αποσυνδέω",
|
||||
"refreshPorts": "Ανανέωση θυρών",
|
||||
"connected": "Συνδεδεμένο με {{path}} σε {{baud}} baud",
|
||||
"disconnected": "Η σειριακή θύρα αποσυνδέθηκε",
|
||||
"connectionError": "Αποτυχία ανοίγματος σειριακής θύρας",
|
||||
"wsError": "Σφάλμα WebSocket",
|
||||
"errorNoServerUrl": "Δεν έχει ρυθμιστεί διεύθυνση URL διακομιστή",
|
||||
"notSupportedTitle": "Σειριακή Δεν Υποστηρίζεται",
|
||||
"notSupported": "Οι σειριακές συνδέσεις απαιτούν ένα πρόγραμμα περιήγησης με υποστήριξη Web Serial API (Chrome, Edge ή Firefox 151+) ή την εφαρμογή Termix για υπολογιστές.",
|
||||
"hideHint": "Μπορείτε να αποκρύψετε την καρτέλα Σειριακός αριθμός στην περιοχή Προφίλ χρήστη > Εμφάνιση > Πλαϊνή μπάρα > Πλοήγηση.",
|
||||
"browserPickerHint": "Κάντε κλικ στην επιλογή Σύνδεση και το πρόγραμμα περιήγησής σας θα ανοίξει ένα πρόγραμμα επιλογής θυρών για να επιλέξετε τη συσκευή."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Προσθήκη καναλιού",
|
||||
"editChannel": "Επεξεργασία καναλιού",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestor de hosts",
|
||||
"credentials": "Credenciales",
|
||||
"connections": "Conexiones",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Mantener el menú abierto",
|
||||
"collapseSideMenu": "Contraer menú",
|
||||
"roleAdministrator": "Administrador",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Abrir estadísticas de Proxmox",
|
||||
"openTmuxMonitor": "Abrir el monitor de Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Puerto",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baudios",
|
||||
"dataBitsLabel": "Datos",
|
||||
"stopBitsLabel": "Parada",
|
||||
"parityLabel": "Paridad",
|
||||
"parityNone": "Ninguna",
|
||||
"parityEven": "Par",
|
||||
"parityOdd": "Impar",
|
||||
"connect": "Conectar a Serial",
|
||||
"disconnect": "Desconectar",
|
||||
"refreshPorts": "Actualizar puertos",
|
||||
"connected": "Conectado a {{path}} a {{baud}} baudios",
|
||||
"disconnected": "Puerto serie desconectado",
|
||||
"connectionError": "Error al abrir el puerto serie",
|
||||
"wsError": "Error WebSocket",
|
||||
"errorNoServerUrl": "No se ha configurado URL de servidor",
|
||||
"notSupportedTitle": "Serial no compatible",
|
||||
"notSupported": "Las conexiones serie requieren un navegador compatible con Web Serial API (Chrome, Edge o Firefox 151+), o la aplicación de escritorio Termix.",
|
||||
"hideHint": "Puedes ocultar la pestaña Serial en Perfil de usuario > Apariencia > Barra lateral > Navegación.",
|
||||
"browserPickerHint": "Haz clic en Conectar y el navegador abrirá un selector de puertos para elegir el dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Añadir canal",
|
||||
"editChannel": "Editar canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Isäntäpäällikkö",
|
||||
"credentials": "Valtakirjat",
|
||||
"connections": "Yhteydet",
|
||||
"serial": "Sarjanumero",
|
||||
"keepSideMenuOpen": "Pidä valikko auki",
|
||||
"collapseSideMenu": "Pienennä valikko",
|
||||
"roleAdministrator": "Ylläpitäjä",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Avaa Proxmox-tilastot",
|
||||
"openTmuxMonitor": "Avaa Tmux-näyttö"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sarjanumero",
|
||||
"portLabel": "Portti",
|
||||
"portPlaceholder": "/dev/ttyUSB0 tai COM3",
|
||||
"baudRateLabel": "Baud-nopeus",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteetti",
|
||||
"parityNone": "Ei mitään",
|
||||
"parityEven": "Jopa",
|
||||
"parityOdd": "Pariton",
|
||||
"connect": "Yhdistä sarjaporttiin",
|
||||
"disconnect": "Katkaise yhteys",
|
||||
"refreshPorts": "Päivitä portit",
|
||||
"connected": "Yhdistetty {{path}} -laitteeseen {{baud}} baudilla",
|
||||
"disconnected": "Sarjaportti irrotettu",
|
||||
"connectionError": "Sarjaportin avaaminen epäonnistui",
|
||||
"wsError": "WebSocket-virhe",
|
||||
"errorNoServerUrl": "Palvelimen URL-osoitetta ei ole määritetty",
|
||||
"notSupportedTitle": "Sarjanumeroa ei tueta",
|
||||
"notSupported": "Sarjaliikenne vaatii selaimen, jossa on Web Serial API -tuki (Chrome, Edge tai Firefox 151+), tai Termix-työpöytäsovelluksen.",
|
||||
"hideHint": "Voit piilottaa Sarja-välilehden kohdassa Käyttäjäprofiili > Ulkoasu > Sivupalkki > Navigointi.",
|
||||
"browserPickerHint": "Napsauta Yhdistä, niin selaimesi avaa porttivalitsimen laitteen valitsemiseksi."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Lisää kanava",
|
||||
"editChannel": "Muokkaa kanavaa",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestionnaire d'hôtes",
|
||||
"credentials": "Identifiants",
|
||||
"connections": "Connexions",
|
||||
"serial": "Série",
|
||||
"keepSideMenuOpen": "Garder le menu ouvert",
|
||||
"collapseSideMenu": "Réduire le menu",
|
||||
"roleAdministrator": "Administrateur",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Ouvrir les statistiques de Proxmox",
|
||||
"openTmuxMonitor": "Moniteur Open Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Série",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Débit en bauds",
|
||||
"dataBitsLabel": "Données",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parité",
|
||||
"parityNone": "Aucune",
|
||||
"parityEven": "Paire",
|
||||
"parityOdd": "Impaire",
|
||||
"connect": "Connecter",
|
||||
"disconnect": "Déconnecter",
|
||||
"refreshPorts": "Actualiser les ports",
|
||||
"connected": "Connecté à {{path}} à {{baud}} bauds",
|
||||
"disconnected": "Port série déconnecté",
|
||||
"connectionError": "Échec de l'ouverture du port série",
|
||||
"wsError": "Erreur WebSocket",
|
||||
"errorNoServerUrl": "Aucune URL de serveur configurée",
|
||||
"notSupportedTitle": "Port série non pris en charge",
|
||||
"notSupported": "Les connexions série nécessitent un navigateur prenant en charge l'API Web Serial (Chrome, Edge, ou Firefox 151+), ou l'application de bureau Termix.",
|
||||
"hideHint": "Vous pouvez masquer l'onglet Série sous Profil utilisateur > Apparence > Barre latérale > Navigation.",
|
||||
"browserPickerHint": "Cliquez sur Connexion et votre navigateur ouvrira un sélecteur de port pour choisir le périphérique."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Ajouter un canal",
|
||||
"editChannel": "Modifier le canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "מנהל מארח",
|
||||
"credentials": "אישורים",
|
||||
"connections": "חיבורים",
|
||||
"serial": "סִדרָתִי",
|
||||
"keepSideMenuOpen": "שמור את התפריט פתוח",
|
||||
"collapseSideMenu": "כווץ תפריט",
|
||||
"roleAdministrator": "מְנַהֵל",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "פתח את סטטיסטיקות Proxmox",
|
||||
"openTmuxMonitor": "פתח את צג Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "סִדרָתִי",
|
||||
"portLabel": "נָמָל",
|
||||
"portPlaceholder": "/dev/ttyUSB0 או COM3",
|
||||
"baudRateLabel": "קצב באוד",
|
||||
"dataBitsLabel": "נְתוּנִים",
|
||||
"stopBitsLabel": "לְהַפְסִיק",
|
||||
"parityLabel": "שִׁוּוּי",
|
||||
"parityNone": "אַף לֹא אֶחָד",
|
||||
"parityEven": "אֲפִילוּ",
|
||||
"parityOdd": "מְשׁוּנֶה",
|
||||
"connect": "חיבור סריאלי",
|
||||
"disconnect": "לְנַתֵק",
|
||||
"refreshPorts": "רענון פורטים",
|
||||
"connected": "מחובר ל- {{path}} ב- {{baud}} באוד",
|
||||
"disconnected": "יציאה טורית מנותקת",
|
||||
"connectionError": "נכשלה פתיחת הפורט הטורי",
|
||||
"wsError": "שגיאת WebSocket",
|
||||
"errorNoServerUrl": "לא הוגדרה כתובת URL של שרת",
|
||||
"notSupportedTitle": "סריאלי לא נתמך",
|
||||
"notSupported": "חיבורים טוריים דורשים דפדפן עם תמיכה ב-Web Serial API (Chrome, Edge או Firefox 151+), או את אפליקציית שולחן העבודה Termix.",
|
||||
"hideHint": "ניתן להסתיר את הכרטיסייה 'סדרתי' תחת פרופיל משתמש > מראה > סרגל צד > ניווט.",
|
||||
"browserPickerHint": "לחץ על התחבר והדפדפן שלך יפתח בורר פורטים לבחירת המכשיר."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "הוסף ערוץ",
|
||||
"editChannel": "עריכת ערוץ",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "होस्ट प्रबंधक",
|
||||
"credentials": "क्रेडेंशियल्स",
|
||||
"connections": "कनेक्शन्स",
|
||||
"serial": "सीरियल",
|
||||
"keepSideMenuOpen": "मेनू खुला रखें",
|
||||
"collapseSideMenu": "मेनू को छोटा करें",
|
||||
"roleAdministrator": "प्रशासक",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "प्रोक्समॉक्स के आँकड़े खोलें",
|
||||
"openTmuxMonitor": "ओपन टीएमएक्स मॉनिटर"
|
||||
},
|
||||
"serial": {
|
||||
"title": "सीरियल",
|
||||
"portLabel": "पोर्ट",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "बॉड दर",
|
||||
"dataBitsLabel": "डेटा",
|
||||
"stopBitsLabel": "स्टॉप",
|
||||
"parityLabel": "पैरिटी",
|
||||
"parityNone": "कोई नहीं",
|
||||
"parityEven": "सम",
|
||||
"parityOdd": "विषम",
|
||||
"connect": "सीरियल कनेक्ट करें",
|
||||
"disconnect": "डिस्कनेक्ट करें",
|
||||
"refreshPorts": "पोर्ट रीफ़्रेश करें",
|
||||
"connected": "{{path}} से {{baud}} बॉड पर कनेक्टेड",
|
||||
"disconnected": "सीरियल पोर्ट डिस्कनेक्ट हो गया",
|
||||
"connectionError": "सीरियल पोर्ट खोलने में विफल",
|
||||
"wsError": "WebSocket त्रुटि",
|
||||
"errorNoServerUrl": "कोई सर्वर URL कॉन्फ़िगर नहीं है",
|
||||
"notSupportedTitle": "सीरियल समर्थित नहीं",
|
||||
"notSupported": "सीरियल कनेक्शन के लिए Web Serial API समर्थन वाले ब्राउज़र (Chrome, Edge, या Firefox 151+) या Termix डेस्कटॉप ऐप की आवश्यकता है।",
|
||||
"hideHint": "आप सीरियल टैब को User Profile > Appearance > Sidebar > Navigation में छिपा सकते हैं।",
|
||||
"browserPickerHint": "Connect पर क्लिक करें और आपका ब्राउज़र डिवाइस चुनने के लिए एक पोर्ट पिकर खोलेगा।"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "चैनल जोड़ें",
|
||||
"editChannel": "चैनल संपादित करें",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Házigazda-kezelő",
|
||||
"credentials": "Hitelesítő adatok",
|
||||
"connections": "Kapcsolatok",
|
||||
"serial": "Sorozatszám",
|
||||
"keepSideMenuOpen": "Menü nyitva tartása",
|
||||
"collapseSideMenu": "Menü összecsukása",
|
||||
"roleAdministrator": "Adminisztrátor",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox statisztikák megnyitása",
|
||||
"openTmuxMonitor": "Tmux monitor megnyitása"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sorozatszám",
|
||||
"portLabel": "Kikötő",
|
||||
"portPlaceholder": "/dev/ttyUSB0 vagy COM3",
|
||||
"baudRateLabel": "Baud sebesség",
|
||||
"dataBitsLabel": "Adat",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritás",
|
||||
"parityNone": "Egyik sem",
|
||||
"parityEven": "Még",
|
||||
"parityOdd": "Páratlan",
|
||||
"connect": "Csatlakozás soros porthoz",
|
||||
"disconnect": "Leválasztás",
|
||||
"refreshPorts": "Frissítse a portokat",
|
||||
"connected": "{{path}} -hoz csatlakoztatva {{baud}} baud sebességgel",
|
||||
"disconnected": "Soros port leválasztva",
|
||||
"connectionError": "Nem sikerült megnyitni a soros portot",
|
||||
"wsError": "WebSocket hiba",
|
||||
"errorNoServerUrl": "Nincs konfigurálva szerver URL",
|
||||
"notSupportedTitle": "Sorozatszám Nem támogatott",
|
||||
"notSupported": "A soros kapcsolatokhoz Web Serial API-t támogató böngésző (Chrome, Edge vagy Firefox 151+), vagy a Termix asztali alkalmazás szükséges.",
|
||||
"hideHint": "A Sorozat fület elrejtheted a Felhasználói profil > Megjelenés > Oldalsáv > Navigáció alatt.",
|
||||
"browserPickerHint": "Kattintson a Csatlakozás gombra, és a böngészője megnyit egy portválasztót az eszköz kiválasztásához."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Csatorna hozzáadása",
|
||||
"editChannel": "Csatorna szerkesztése",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Manajer Host",
|
||||
"credentials": "Kredensial",
|
||||
"connections": "Koneksi",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Biarkan Menu Tetap Terbuka",
|
||||
"collapseSideMenu": "Ciutkan Menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Buka Statistik Proxmox",
|
||||
"openTmuxMonitor": "Buka Monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Pelabuhan",
|
||||
"portPlaceholder": "/dev/ttyUSB0 atau COM3",
|
||||
"baudRateLabel": "Kecepatan Baud",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Berhenti",
|
||||
"parityLabel": "Keseimbangan",
|
||||
"parityNone": "Tidak ada",
|
||||
"parityEven": "Bahkan",
|
||||
"parityOdd": "Aneh",
|
||||
"connect": "Hubungkan ke Serial",
|
||||
"disconnect": "Memutuskan",
|
||||
"refreshPorts": "Penyegaran port",
|
||||
"connected": "Terhubung ke {{path}} pada kecepatan {{baud}} baud",
|
||||
"disconnected": "Port serial terputus",
|
||||
"connectionError": "Gagal membuka port serial",
|
||||
"wsError": "Kesalahan WebSocket",
|
||||
"errorNoServerUrl": "Tidak ada URL server yang dikonfigurasi.",
|
||||
"notSupportedTitle": "Nomor Seri Tidak Didukung",
|
||||
"notSupported": "Koneksi serial memerlukan browser dengan dukungan Web Serial API (Chrome, Edge, atau Firefox 151+), atau aplikasi desktop Termix.",
|
||||
"hideHint": "Anda dapat menyembunyikan tab Serial di bawah Profil Pengguna > Tampilan > Bilah Samping > Navigasi.",
|
||||
"browserPickerHint": "Klik Sambungkan dan browser Anda akan membuka pemilih port untuk memilih perangkat."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Tambahkan Saluran",
|
||||
"editChannel": "Edit Saluran",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestione host",
|
||||
"credentials": "Credenziali",
|
||||
"connections": "Connessioni",
|
||||
"serial": "Seriale",
|
||||
"keepSideMenuOpen": "Mantieni il menu aperto",
|
||||
"collapseSideMenu": "Comprimi menu",
|
||||
"roleAdministrator": "Amministratore",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Statistiche di Proxmox aperte",
|
||||
"openTmuxMonitor": "Monitor Tmux aperto"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriale",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baud rate",
|
||||
"dataBitsLabel": "Dati",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parità",
|
||||
"parityNone": "Nessuna",
|
||||
"parityEven": "Pari",
|
||||
"parityOdd": "Dispari",
|
||||
"connect": "Connetti alla Seriale",
|
||||
"disconnect": "Disconnetti",
|
||||
"refreshPorts": "Aggiorna porte",
|
||||
"connected": "Connesso a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Porta seriale disconnessa",
|
||||
"connectionError": "Impossibile aprire la porta seriale",
|
||||
"wsError": "Errore WebSocket",
|
||||
"errorNoServerUrl": "Nessun URL del server configurato",
|
||||
"notSupportedTitle": "Seriale non supportato",
|
||||
"notSupported": "Le connessioni seriali richiedono un browser con supporto Web Serial API (Chrome, Edge o Firefox 151+), oppure l'app desktop Termix.",
|
||||
"hideHint": "Puoi nascondere la scheda Seriale in Profilo Utente > Aspetto > Barra laterale > Navigazione.",
|
||||
"browserPickerHint": "Fai clic su Connetti e il browser aprirà un selettore di porte per scegliere il dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Aggiungi canale",
|
||||
"editChannel": "Modifica canale",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "ホストマネージャー",
|
||||
"credentials": "認証情報",
|
||||
"connections": "接続",
|
||||
"serial": "シリアル",
|
||||
"keepSideMenuOpen": "メニューを開いたままにする",
|
||||
"collapseSideMenu": "メニューを折りたたむ",
|
||||
"roleAdministrator": "管理者",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox統計を開く",
|
||||
"openTmuxMonitor": "Tmux Monitorを開く"
|
||||
},
|
||||
"serial": {
|
||||
"title": "シリアル",
|
||||
"portLabel": "ポート",
|
||||
"portPlaceholder": "/dev/ttyUSB0 または COM3",
|
||||
"baudRateLabel": "ボーレート",
|
||||
"dataBitsLabel": "データ",
|
||||
"stopBitsLabel": "ストップ",
|
||||
"parityLabel": "パリティ",
|
||||
"parityNone": "なし",
|
||||
"parityEven": "偶数",
|
||||
"parityOdd": "奇数",
|
||||
"connect": "シリアルに接続",
|
||||
"disconnect": "切断",
|
||||
"refreshPorts": "ポートを更新",
|
||||
"connected": "{{path}} に {{baud}} ボーで接続しました",
|
||||
"disconnected": "シリアルポートが切断されました",
|
||||
"connectionError": "シリアルポートを開けませんでした",
|
||||
"wsError": "WebSocket エラー",
|
||||
"errorNoServerUrl": "サーバー URL が設定されていません",
|
||||
"notSupportedTitle": "シリアル非対応",
|
||||
"notSupported": "シリアル接続には Web Serial API をサポートするブラウザ (Chrome, Edge, Firefox 151+) または Termix デスクトップアプリが必要です。",
|
||||
"hideHint": "シリアルタブは「ユーザープロファイル > 外観 > サイドバー > ナビゲーション」で非表示にできます。",
|
||||
"browserPickerHint": "接続をクリックすると、ブラウザがポートピッカーを開き、デバイスを選択できます。"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "チャンネルを追加",
|
||||
"editChannel": "チャンネルを編集",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "호스트 관리자",
|
||||
"credentials": "자격 증명",
|
||||
"connections": "연결",
|
||||
"serial": "시리얼",
|
||||
"keepSideMenuOpen": "메뉴를 열어 두세요",
|
||||
"collapseSideMenu": "메뉴 접기",
|
||||
"roleAdministrator": "관리자",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox 통계 열기",
|
||||
"openTmuxMonitor": "Tmux 모니터 열기"
|
||||
},
|
||||
"serial": {
|
||||
"title": "시리얼",
|
||||
"portLabel": "포트",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "보레이트",
|
||||
"dataBitsLabel": "데이터",
|
||||
"stopBitsLabel": "스톱",
|
||||
"parityLabel": "패리티",
|
||||
"parityNone": "없음",
|
||||
"parityEven": "짝수",
|
||||
"parityOdd": "홀수",
|
||||
"connect": "시리얼 연결",
|
||||
"disconnect": "연결 해제",
|
||||
"refreshPorts": "포트 새로 고침",
|
||||
"connected": "{{path}}에 {{baud}} 보로 연결됨",
|
||||
"disconnected": "시리얼 포트 연결 해제됨",
|
||||
"connectionError": "시리얼 포트를 열지 못했습니다.",
|
||||
"wsError": "WebSocket 오류",
|
||||
"errorNoServerUrl": "서버 URL이 구성되지 않았습니다.",
|
||||
"notSupportedTitle": "시리얼 미지원",
|
||||
"notSupported": "시리얼 연결에는 Web Serial API를 지원하는 브라우저(Chrome, Edge, Firefox 151+) 또는 Termix 데스크톱 앱이 필요합니다.",
|
||||
"hideHint": "Serial 탭은 사용자 프로필 > 모양 > 사이드바 > 탐색에서 숨길 수 있습니다.",
|
||||
"browserPickerHint": "연결 버튼을 클릭하면 브라우저에서 장치 선택을 위한 포트 선택기가 열립니다."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "채널 추가",
|
||||
"editChannel": "채널 편집",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Hostmanager",
|
||||
"credentials": "Referenties",
|
||||
"connections": "Verbindingen",
|
||||
"serial": "Serieel",
|
||||
"keepSideMenuOpen": "Menu open houden",
|
||||
"collapseSideMenu": "Menu inklappen",
|
||||
"roleAdministrator": "Beheerder",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Open Proxmox-statistieken",
|
||||
"openTmuxMonitor": "Open Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serieel",
|
||||
"portLabel": "Haven",
|
||||
"portPlaceholder": "/dev/ttyUSB0 of COM3",
|
||||
"baudRateLabel": "Baudrate",
|
||||
"dataBitsLabel": "Gegevens",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteit",
|
||||
"parityNone": "Geen",
|
||||
"parityEven": "Zelfs",
|
||||
"parityOdd": "Vreemd",
|
||||
"connect": "Verbinden via serieel",
|
||||
"disconnect": "Verbinding verbreken",
|
||||
"refreshPorts": "Vernieuw poorten",
|
||||
"connected": "Verbonden met {{path}} met {{baud}} baud",
|
||||
"disconnected": "Seriële poort losgekoppeld",
|
||||
"connectionError": "Het openen van de seriële poort is mislukt.",
|
||||
"wsError": "WebSocket-fout",
|
||||
"errorNoServerUrl": "Geen server-URL geconfigureerd",
|
||||
"notSupportedTitle": "Serienummer niet ondersteund",
|
||||
"notSupported": "Voor seriële verbindingen is een browser met Web Serial API-ondersteuning vereist (Chrome, Edge of Firefox 151+), of de Termix-desktopapp.",
|
||||
"hideHint": "Je kunt het tabblad 'Serienummer' verbergen via Gebruikersprofiel > Uiterlijk > Zijbalk > Navigatie.",
|
||||
"browserPickerHint": "Klik op Verbinden en uw browser opent een poortkiezer waarmee u het apparaat kunt selecteren."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Kanaal toevoegen",
|
||||
"editChannel": "Bewerk kanaal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Vertsadministrator",
|
||||
"credentials": "Legitimasjon",
|
||||
"connections": "Tilkoblinger",
|
||||
"serial": "Seriell",
|
||||
"keepSideMenuOpen": "Hold menyen åpen",
|
||||
"collapseSideMenu": "Skjul menyen",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Åpne Proxmox-statistikk",
|
||||
"openTmuxMonitor": "Åpne Tmux-skjermen"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Havn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighet",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stoppe",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Til og med",
|
||||
"parityOdd": "Merkelig",
|
||||
"connect": "Koble til seriell",
|
||||
"disconnect": "Frakople",
|
||||
"refreshPorts": "Oppdater porter",
|
||||
"connected": "Koblet til {{path}} ved {{baud}} baud",
|
||||
"disconnected": "Seriell port frakoblet",
|
||||
"connectionError": "Klarte ikke å åpne serieporten",
|
||||
"wsError": "WebSocket-feil",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigurert",
|
||||
"notSupportedTitle": "Serienummer støttes ikke",
|
||||
"notSupported": "Serielle tilkoblinger krever en nettleser med støtte for Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivebordsappen.",
|
||||
"hideHint": "Du kan skjule Seriell-fanen under Brukerprofil > Utseende > Sidefelt > Navigasjon.",
|
||||
"browserPickerHint": "Klikk på Koble til, så åpner nettleseren din en portvelger der du kan velge enheten."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Legg til kanal",
|
||||
"editChannel": "Rediger kanal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Menedżer hostów",
|
||||
"credentials": "Poświadczenia",
|
||||
"connections": "Połączenia",
|
||||
"serial": "Port szeregowy",
|
||||
"keepSideMenuOpen": "Zachowaj otwarte menu",
|
||||
"collapseSideMenu": "Zwiń menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Otwórz statystyki Proxmox",
|
||||
"openTmuxMonitor": "Otwórz monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Port szeregowy",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 lub COM3",
|
||||
"baudRateLabel": "Szybkość transmisji",
|
||||
"dataBitsLabel": "Dane",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parzystość",
|
||||
"parityNone": "Brak",
|
||||
"parityEven": "Parzysta",
|
||||
"parityOdd": "Nieparzysta",
|
||||
"connect": "Połącz z portem szeregowym",
|
||||
"disconnect": "Odłącz",
|
||||
"refreshPorts": "Odśwież porty",
|
||||
"connected": "Połączono z {{path}} z szybkością {{baud}} bodów",
|
||||
"disconnected": "Port szeregowy rozłączony",
|
||||
"connectionError": "Nie udało się otworzyć portu szeregowego",
|
||||
"wsError": "Błąd WebSocket",
|
||||
"errorNoServerUrl": "Brak skonfigurowanego adresu URL serwera",
|
||||
"notSupportedTitle": "Port szeregowy nieobsługiwany",
|
||||
"notSupported": "Połączenia szeregowe wymagają przeglądarki obsługującej Web Serial API (Chrome, Edge lub Firefox 151+) albo aplikacji desktopowej Termix.",
|
||||
"hideHint": "Kartę Port szeregowy można ukryć w: Profil użytkownika > Wygląd > Pasek boczny > Nawigacja.",
|
||||
"browserPickerHint": "Kliknij Połącz, a przeglądarka otworzy okno wyboru portu, w którym wskażesz urządzenie."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Dodaj kanał",
|
||||
"editChannel": "Edytuj kanał",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gerente de Hospedagem",
|
||||
"credentials": "Credenciais",
|
||||
"connections": "Conexões",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Mantenha o menu aberto",
|
||||
"collapseSideMenu": "Recolher menu",
|
||||
"roleAdministrator": "Administrador",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Estatísticas do Open Proxmox",
|
||||
"openTmuxMonitor": "Abra o monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
|
||||
"baudRateLabel": "Taxa de transmissão (Baud rate)",
|
||||
"dataBitsLabel": "Dados",
|
||||
"stopBitsLabel": "Parar",
|
||||
"parityLabel": "Paridade",
|
||||
"parityNone": "Nenhum",
|
||||
"parityEven": "Até",
|
||||
"parityOdd": "Chance",
|
||||
"connect": "Conectar à porta serial",
|
||||
"disconnect": "Desconectar",
|
||||
"refreshPorts": "Atualizar portas",
|
||||
"connected": "Conectado a {{path}} na taxa de transmissão de {{baud}}",
|
||||
"disconnected": "Porta serial desconectada",
|
||||
"connectionError": "Falha ao abrir a porta serial",
|
||||
"wsError": "Erro no WebSocket",
|
||||
"errorNoServerUrl": "Nenhuma URL de servidor configurada",
|
||||
"notSupportedTitle": "Serial não suportado",
|
||||
"notSupported": "As conexões seriais exigem um navegador com suporte à API Web Serial (Chrome, Edge ou Firefox 151+) ou o aplicativo Termix para desktop.",
|
||||
"hideHint": "Você pode ocultar a guia Serial em Perfil do Usuário > Aparência > Barra lateral > Navegação.",
|
||||
"browserPickerHint": "Clique em Conectar e seu navegador abrirá um seletor de portas para que você escolha o dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Adicionar canal",
|
||||
"editChannel": "Editar canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestor de Máquinas",
|
||||
"credentials": "Credenciais",
|
||||
"connections": "Ligações",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Mantenha o menu aberto",
|
||||
"collapseSideMenu": "Recolher menu",
|
||||
"roleAdministrator": "Administrador",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Estatísticas do Open Proxmox",
|
||||
"openTmuxMonitor": "Abra o monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
|
||||
"baudRateLabel": "Taxa Baud",
|
||||
"dataBitsLabel": "Dados",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paridade",
|
||||
"parityNone": "Nenhuma",
|
||||
"parityEven": "Par",
|
||||
"parityOdd": "Ímpar",
|
||||
"connect": "Ligar à Serial",
|
||||
"disconnect": "Desligar",
|
||||
"refreshPorts": "Atualizar portas",
|
||||
"connected": "Ligado a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Porta série desligada",
|
||||
"connectionError": "Falha ao abrir a porta série",
|
||||
"wsError": "Erro de WebSocket",
|
||||
"errorNoServerUrl": "Nenhum URL de servidor configurado",
|
||||
"notSupportedTitle": "Série não suportada",
|
||||
"notSupported": "As ligações série requerem um navegador com suporte à Web Serial API (Chrome, Edge ou Firefox 151+) ou a aplicação de ambiente de trabalho Termix.",
|
||||
"hideHint": "Pode ocultar o separador Série em Perfil do Utilizador > Aparência > Barra Lateral > Navegação.",
|
||||
"browserPickerHint": "Clique em Ligar e o seu navegador abrirá um seletor de porta para escolher o dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Adicionar Canal",
|
||||
"editChannel": "Editar Canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Manager de gazdă",
|
||||
"credentials": "Acreditări",
|
||||
"connections": "Conexiuni",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Mențineți meniul deschis",
|
||||
"collapseSideMenu": "Restrângeți meniul",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Deschideți statisticile Proxmox",
|
||||
"openTmuxMonitor": "Deschideți monitorul Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 sau COM3",
|
||||
"baudRateLabel": "Rată de baud",
|
||||
"dataBitsLabel": "Date",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritate",
|
||||
"parityNone": "Nici unul",
|
||||
"parityEven": "Chiar",
|
||||
"parityOdd": "Ciudat",
|
||||
"connect": "Conectare la serial",
|
||||
"disconnect": "Deconecta",
|
||||
"refreshPorts": "Reîmprospătați porturile",
|
||||
"connected": "Conectat la {{path}} la {{baud}} baud",
|
||||
"disconnected": "Portul serial deconectat",
|
||||
"connectionError": "Nu s-a putut deschide portul serial",
|
||||
"wsError": "Eroare WebSocket",
|
||||
"errorNoServerUrl": "Nu este configurată nicio adresă URL a serverului",
|
||||
"notSupportedTitle": "Număr de serie neacceptat",
|
||||
"notSupported": "Conexiunile seriale necesită un browser cu suport Web Serial API (Chrome, Edge sau Firefox 151+) sau aplicația desktop Termix.",
|
||||
"hideHint": "Puteți ascunde fila Serial din Profil utilizator > Aspect > Bara laterală > Navigare.",
|
||||
"browserPickerHint": "Faceți clic pe Conectare și browserul dvs. va deschide un selector de porturi pentru a alege dispozitivul."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Adăugați canal",
|
||||
"editChannel": "Editați canalul",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Менеджер хостов",
|
||||
"credentials": "Учетные данные",
|
||||
"connections": "Подключения",
|
||||
"serial": "Последовательный порт",
|
||||
"keepSideMenuOpen": "Оставить меню открытым",
|
||||
"collapseSideMenu": "Свернуть меню",
|
||||
"roleAdministrator": "Администратор",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Откройте статистику Proxmox",
|
||||
"openTmuxMonitor": "Открыть монитор Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Последовательный порт",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Скорость передачи",
|
||||
"dataBitsLabel": "Биты данных",
|
||||
"stopBitsLabel": "Стоп-биты",
|
||||
"parityLabel": "Чётность",
|
||||
"parityNone": "Нет",
|
||||
"parityEven": "Чётный",
|
||||
"parityOdd": "Нечётный",
|
||||
"connect": "Подключиться",
|
||||
"disconnect": "Отключиться",
|
||||
"refreshPorts": "Обновить порты",
|
||||
"connected": "Подключено к {{path}} на скорости {{baud}} бод",
|
||||
"disconnected": "Последовательный порт отключен",
|
||||
"connectionError": "Ошибка открытия последовательного порта",
|
||||
"wsError": "Ошибка WebSocket",
|
||||
"errorNoServerUrl": "Не настроен URL сервера",
|
||||
"notSupportedTitle": "Последовательный порт не поддерживается",
|
||||
"notSupported": "Последовательные подключения требуют браузер с поддержкой Web Serial API (Chrome, Edge или Firefox 151+) или десктопное приложение Termix.",
|
||||
"hideHint": "Вы можете скрыть вкладку «Serial» в разделе «Профиль пользователя» > «Оформление» > «Боковая панель» > «Навигация».",
|
||||
"browserPickerHint": "Нажмите «Подключиться», и браузер откроет окно выбора порта для выбора устройства."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Добавить канал",
|
||||
"editChannel": "Изменить канал",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Менаџер домаћина",
|
||||
"credentials": "Акредитиви",
|
||||
"connections": "Везе",
|
||||
"serial": "Серијски",
|
||||
"keepSideMenuOpen": "Остави мени отвореним",
|
||||
"collapseSideMenu": "Сакриј мени",
|
||||
"roleAdministrator": "Администратор",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Отвори Proxmox статистику",
|
||||
"openTmuxMonitor": "Отвори Tmux монитор"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Серијски",
|
||||
"portLabel": "Лука",
|
||||
"portPlaceholder": "/dev/ttyUSB0 или COM3",
|
||||
"baudRateLabel": "Брзина преноса података",
|
||||
"dataBitsLabel": "Подаци",
|
||||
"stopBitsLabel": "Заустави",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Ниједан",
|
||||
"parityEven": "Чак",
|
||||
"parityOdd": "Чудно",
|
||||
"connect": "Повежи се са серијским",
|
||||
"disconnect": "Прекини везу",
|
||||
"refreshPorts": "Освежи портове",
|
||||
"connected": "Повезано са {{path}} брзином {{baud}} бауда",
|
||||
"disconnected": "Серијски порт је искључен",
|
||||
"connectionError": "Отварање серијског порта није успело",
|
||||
"wsError": "Грешка WebSocket-а",
|
||||
"errorNoServerUrl": "Није конфигурисан URL сервера",
|
||||
"notSupportedTitle": "Серијски број није подржан",
|
||||
"notSupported": "Серијске везе захтевају прегледач са подршком за Web Serial API (Chrome, Edge или Firefox 151+) или Termix десктоп апликацију.",
|
||||
"hideHint": "Картицу Серијски број можете сакрити под Кориснички профил > Изглед > Бочна трака > Навигација.",
|
||||
"browserPickerHint": "Кликните на Повежи се и ваш прегледач ће отворити бирач портова за избор уређаја."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Додај канал",
|
||||
"editChannel": "Измени канал",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Värdhanterare",
|
||||
"credentials": "Referenser",
|
||||
"connections": "Anslutningar",
|
||||
"serial": "Serie",
|
||||
"keepSideMenuOpen": "Håll menyn öppen",
|
||||
"collapseSideMenu": "Minimera menyn",
|
||||
"roleAdministrator": "Administratör",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Öppna Proxmox-statistik",
|
||||
"openTmuxMonitor": "Öppna Tmux-monitorn"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serie",
|
||||
"portLabel": "Hamn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighet",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stopp",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Även",
|
||||
"parityOdd": "Udda",
|
||||
"connect": "Anslut till seriellt",
|
||||
"disconnect": "Koppla från",
|
||||
"refreshPorts": "Uppdatera portar",
|
||||
"connected": "Ansluten till {{path}} vid {{baud}} baud",
|
||||
"disconnected": "Seriell port frånkopplad",
|
||||
"connectionError": "Misslyckades med att öppna serieporten",
|
||||
"wsError": "WebSocket-fel",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigurerad",
|
||||
"notSupportedTitle": "Seriellt stöds inte",
|
||||
"notSupported": "Seriella anslutningar kräver en webbläsare med stöd för Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivbordsappen.",
|
||||
"hideHint": "Du kan dölja fliken Seriell under Användarprofil > Utseende > Sidofält > Navigering.",
|
||||
"browserPickerHint": "Klicka på Anslut så öppnar din webbläsare en portväljare där du kan välja enheten."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Lägg till kanal",
|
||||
"editChannel": "Redigera kanal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "ผู้จัดการโฮสต์",
|
||||
"credentials": "คุณสมบัติ",
|
||||
"connections": "การเชื่อมต่อ",
|
||||
"serial": "ซีเรียล",
|
||||
"keepSideMenuOpen": "เปิดเมนูค้างไว้",
|
||||
"collapseSideMenu": "ยุบเมนู",
|
||||
"roleAdministrator": "ผู้ดูแลระบบ",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "เปิดดูสถิติ Proxmox",
|
||||
"openTmuxMonitor": "เปิด Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "ซีเรียล",
|
||||
"portLabel": "ท่าเรือ",
|
||||
"portPlaceholder": "/dev/ttyUSB0 หรือ COM3",
|
||||
"baudRateLabel": "อัตราบิต",
|
||||
"dataBitsLabel": "ข้อมูล",
|
||||
"stopBitsLabel": "หยุด",
|
||||
"parityLabel": "ความเท่าเทียมกัน",
|
||||
"parityNone": "ไม่มี",
|
||||
"parityEven": "สม่ำเสมอ",
|
||||
"parityOdd": "แปลก",
|
||||
"connect": "เชื่อมต่อกับพอร์ตอนุกรม",
|
||||
"disconnect": "ตัดการเชื่อมต่อ",
|
||||
"refreshPorts": "พอร์ตรีเฟรช",
|
||||
"connected": "เชื่อมต่อกับ {{path}} ที่ความเร็ว {{baud}} บอด",
|
||||
"disconnected": "พอร์ตอนุกรมถูกตัดการเชื่อมต่อ",
|
||||
"connectionError": "ไม่สามารถเปิดพอร์ตอนุกรมได้",
|
||||
"wsError": "ข้อผิดพลาด WebSocket",
|
||||
"errorNoServerUrl": "ไม่ได้กำหนดค่า URL เซิร์ฟเวอร์",
|
||||
"notSupportedTitle": "ไม่รองรับหมายเลขซีเรียล",
|
||||
"notSupported": "การเชื่อมต่อแบบอนุกรมจำเป็นต้องใช้เบราว์เซอร์ที่รองรับ Web Serial API (Chrome, Edge หรือ Firefox เวอร์ชัน 151 ขึ้นไป) หรือแอปพลิเคชัน Termix บนเดสก์ท็อป",
|
||||
"hideHint": "คุณสามารถซ่อนแท็บ Serial ได้ที่ โปรไฟล์ผู้ใช้ > ลักษณะที่ปรากฏ > แถบด้านข้าง > การนำทาง",
|
||||
"browserPickerHint": "คลิก \"เชื่อมต่อ\" แล้วเบราว์เซอร์ของคุณจะเปิดตัวเลือกพอร์ตเพื่อเลือกอุปกรณ์"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "เพิ่มช่องทาง",
|
||||
"editChannel": "แก้ไขช่อง",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Ana Bilgisayar Yöneticisi",
|
||||
"credentials": "Kimlik Bilgileri",
|
||||
"connections": "Bağlantılar",
|
||||
"serial": "Seri",
|
||||
"keepSideMenuOpen": "Menüyü Açık Tutun",
|
||||
"collapseSideMenu": "Menüyü Daralt",
|
||||
"roleAdministrator": "Yönetici",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox İstatistiklerini Aç",
|
||||
"openTmuxMonitor": "Tmux Monitörünü Aç"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seri",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 veya COM3",
|
||||
"baudRateLabel": "Baud Hızı",
|
||||
"dataBitsLabel": "Veri",
|
||||
"stopBitsLabel": "Durdurma",
|
||||
"parityLabel": "Eşlik",
|
||||
"parityNone": "Yok",
|
||||
"parityEven": "Çift",
|
||||
"parityOdd": "Tek",
|
||||
"connect": "Seri Bağlantıya Bağlan",
|
||||
"disconnect": "Bağlantıyı Kes",
|
||||
"refreshPorts": "Portları yenile",
|
||||
"connected": "{{path}} ile {{baud}} baud hızında bağlandı.",
|
||||
"disconnected": "Seri port bağlantısı kesildi.",
|
||||
"connectionError": "Seri bağlantı noktası açılamadı",
|
||||
"wsError": "WebSocket hatası",
|
||||
"errorNoServerUrl": "Sunucu URL'si yapılandırılmamış",
|
||||
"notSupportedTitle": "Seri Bağlantı Desteklenmiyor",
|
||||
"notSupported": "Seri bağlantılar, Web Serial API desteği olan bir tarayıcı (Chrome, Edge veya Firefox 151+) ya da Termix masaüstü uygulaması gerektirir.",
|
||||
"hideHint": "Seri sekmesini Kullanıcı Profili > Görünüm > Kenar Çubuğu > Gezinme altından gizleyebilirsiniz.",
|
||||
"browserPickerHint": "Bağlan'a tıklayın, tarayıcınız cihazı seçmek için bir bağlantı noktası seçici açacaktır."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Kanal Ekle",
|
||||
"editChannel": "Kanalı Düzenle",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Менеджер хостингу",
|
||||
"credentials": "Облікові дані",
|
||||
"connections": "З'єднання",
|
||||
"serial": "Серійний",
|
||||
"keepSideMenuOpen": "Залишити меню відкритим",
|
||||
"collapseSideMenu": "Згорнути меню",
|
||||
"roleAdministrator": "Адміністратор",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Відкрити статистику Proxmox",
|
||||
"openTmuxMonitor": "Відкрити монітор Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Серійний",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 або COM3",
|
||||
"baudRateLabel": "Швидкість передачі даних",
|
||||
"dataBitsLabel": "Дані",
|
||||
"stopBitsLabel": "СТІЙ",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Жоден",
|
||||
"parityEven": "Навіть",
|
||||
"parityOdd": "Дивний",
|
||||
"connect": "Підключення до послідовного порту",
|
||||
"disconnect": "Відключитися",
|
||||
"refreshPorts": "Оновити порти",
|
||||
"connected": "Підключено до {{path}} зі швидкістю {{baud}} бод",
|
||||
"disconnected": "Послідовний порт відключено",
|
||||
"connectionError": "Не вдалося відкрити послідовний порт",
|
||||
"wsError": "Помилка WebSocket",
|
||||
"errorNoServerUrl": "URL-адреса сервера не налаштована",
|
||||
"notSupportedTitle": "Серійний номер не підтримується",
|
||||
"notSupported": "Для послідовних з’єднань потрібен браузер із підтримкою Web Serial API (Chrome, Edge або Firefox 151+) або настільний додаток Termix.",
|
||||
"hideHint": "Ви можете приховати вкладку «Серійний номер» у розділі «Профіль користувача» > «Зовнішній вигляд» > «Бічна панель» > «Навігація».",
|
||||
"browserPickerHint": "Натисніть «Підключитися», і ваш браузер відкриє вибір порту для вибору пристрою."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Додати канал",
|
||||
"editChannel": "Редагувати канал",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Quản lý máy chủ",
|
||||
"credentials": "Thông tin đăng nhập",
|
||||
"connections": "Kết nối",
|
||||
"serial": "Nối tiếp",
|
||||
"keepSideMenuOpen": "Giữ menu luôn mở",
|
||||
"collapseSideMenu": "Menu thu gọn",
|
||||
"roleAdministrator": "Quản trị viên",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Mở thống kê Proxmox",
|
||||
"openTmuxMonitor": "Mở Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Cổng nối tiếp",
|
||||
"portLabel": "Cổng",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Tốc độ Baud",
|
||||
"dataBitsLabel": "Dữ liệu",
|
||||
"stopBitsLabel": "Bit dừng",
|
||||
"parityLabel": "Kiểm tra chẵn lẻ",
|
||||
"parityNone": "Không",
|
||||
"parityEven": "Chẵn",
|
||||
"parityOdd": "Lẻ",
|
||||
"connect": "Kết nối cổng nối tiếp",
|
||||
"disconnect": "Ngắt kết nối",
|
||||
"refreshPorts": "Làm mới cổng",
|
||||
"connected": "Đã kết nối tới {{path}} với tốc độ {{baud}} baud",
|
||||
"disconnected": "Đã ngắt kết nối cổng nối tiếp",
|
||||
"connectionError": "Không thể mở cổng nối tiếp",
|
||||
"wsError": "Lỗi WebSocket",
|
||||
"errorNoServerUrl": "Chưa cấu hình URL máy chủ",
|
||||
"notSupportedTitle": "Không hỗ trợ Serial",
|
||||
"notSupported": "Kết nối nối tiếp yêu cầu trình duyệt hỗ trợ Web Serial API (Chrome, Edge, hoặc Firefox 151+), hoặc ứng dụng Termix desktop.",
|
||||
"hideHint": "Bạn có thể ẩn tab Serial trong Hồ sơ người dùng > Giao diện > Thanh bên > Điều hướng.",
|
||||
"browserPickerHint": "Nhấn Kết nối và trình duyệt sẽ mở bộ chọn cổng để chọn thiết bị."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Thêm kênh",
|
||||
"editChannel": "Sửa kênh",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "主机管理器",
|
||||
"credentials": "凭据",
|
||||
"connections": "连接",
|
||||
"serial": "串口",
|
||||
"keepSideMenuOpen": "保持菜单打开",
|
||||
"collapseSideMenu": "折叠菜单",
|
||||
"roleAdministrator": "管理员",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "打开 Proxmox 统计数据",
|
||||
"openTmuxMonitor": "打开 Tmux 监视器"
|
||||
},
|
||||
"serial": {
|
||||
"title": "串口",
|
||||
"portLabel": "端口",
|
||||
"portPlaceholder": "/dev/ttyUSB0 或 COM3",
|
||||
"baudRateLabel": "波特率",
|
||||
"dataBitsLabel": "数据",
|
||||
"stopBitsLabel": "停止位",
|
||||
"parityLabel": "奇偶校验",
|
||||
"parityNone": "无",
|
||||
"parityEven": "偶校验",
|
||||
"parityOdd": "奇校验",
|
||||
"connect": "连接串口",
|
||||
"disconnect": "断开",
|
||||
"refreshPorts": "刷新端口",
|
||||
"connected": "已连接到 {{path}},波特率 {{baud}}",
|
||||
"disconnected": "串口已断开",
|
||||
"connectionError": "打开串口失败",
|
||||
"wsError": "WebSocket 错误",
|
||||
"errorNoServerUrl": "未配置服务器 URL",
|
||||
"notSupportedTitle": "不支持串口",
|
||||
"notSupported": "串口连接需要支持 Web Serial API 的浏览器(Chrome、Edge 或 Firefox 151+),或者 Termix 桌面应用。",
|
||||
"hideHint": "您可以在用户资料 > 外观 > 侧边栏 > 导航中隐藏串口标签页。",
|
||||
"browserPickerHint": "点击连接,您的浏览器将打开端口选择器以选择设备。"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "添加渠道",
|
||||
"editChannel": "编辑渠道",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "主機管理員",
|
||||
"credentials": "憑證",
|
||||
"connections": "連線",
|
||||
"serial": "序列",
|
||||
"keepSideMenuOpen": "保持菜單打開",
|
||||
"collapseSideMenu": "折疊菜單",
|
||||
"roleAdministrator": "管理員",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "開啟 Proxmox 統計數據",
|
||||
"openTmuxMonitor": "打開 Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "序列",
|
||||
"portLabel": "連接埠",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "鮑率",
|
||||
"dataBitsLabel": "資料位元",
|
||||
"stopBitsLabel": "停止位元",
|
||||
"parityLabel": "同位檢查",
|
||||
"parityNone": "無",
|
||||
"parityEven": "偶",
|
||||
"parityOdd": "奇",
|
||||
"connect": "連線至序列埠",
|
||||
"disconnect": "中斷連線",
|
||||
"refreshPorts": "重新整理連接埠",
|
||||
"connected": "已連線至 {{path}},鮑率 {{baud}}",
|
||||
"disconnected": "序列埠已中斷連線",
|
||||
"connectionError": "開啟序列埠失敗",
|
||||
"wsError": "WebSocket 錯誤",
|
||||
"errorNoServerUrl": "未設定伺服器網址",
|
||||
"notSupportedTitle": "不支援序列埠",
|
||||
"notSupported": "序列連線需要支援 Web Serial API 的瀏覽器 (Chrome、Edge 或 Firefox 151+) 或 Termix 桌面應用程式。",
|
||||
"hideHint": "您可以在「使用者設定檔 > 外觀 > 側邊欄 > 導覽」中隱藏序列索引標籤。",
|
||||
"browserPickerHint": "按一下連線,瀏覽器會開啟連接埠選擇器讓您選取裝置。"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "新增通道",
|
||||
"editChannel": "編輯通道",
|
||||
|
||||
+10
-5
@@ -30,17 +30,22 @@ import {
|
||||
Share2,
|
||||
} from "lucide-react";
|
||||
import { tabIcon } from "@/shell/tabUtils";
|
||||
import { isSessionTabType } from "@/shell/tab-registry";
|
||||
import { isSessionTabType, isShareableTabType } from "@/shell/tab-registry";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import type { Tab, TabType, SplitMode } from "@/types/ui-types";
|
||||
import { SPLIT_MODES, PANE_COUNTS } from "@/lib/theme";
|
||||
|
||||
/**
|
||||
* Tabs holding a live connection that can be refreshed and shared: the
|
||||
* registered session tabs. Serial sessions are not shareable.
|
||||
* Tabs holding a live connection that can be refreshed: the registered
|
||||
* session tabs.
|
||||
*/
|
||||
function isConnectionTab(type: TabType): boolean {
|
||||
return type !== "serial" && isSessionTabType(type);
|
||||
return isSessionTabType(type);
|
||||
}
|
||||
|
||||
/** Connection tabs that also offer "Share session". */
|
||||
function isShareableConnectionTab(type: TabType): boolean {
|
||||
return isConnectionTab(type) && isShareableTabType(type);
|
||||
}
|
||||
|
||||
export function TabBar({
|
||||
@@ -467,7 +472,7 @@ export function TabBar({
|
||||
<RefreshCw className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
{isConnectionTab(tab.type) && onOpenShare && (
|
||||
{isShareableConnectionTab(tab.type) && onOpenShare && (
|
||||
<button
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getTabType } from "./tab-registry";
|
||||
* not a meaningful part of an arrangement.
|
||||
*/
|
||||
const CORE_CAPTURABLE = new Set([
|
||||
"serial",
|
||||
"homepage",
|
||||
"termix-id",
|
||||
"session-logs",
|
||||
@@ -52,10 +51,7 @@ const CORE_UNSAVED = new Set([
|
||||
*/
|
||||
export function isUnregisteredPluginTabType(type: string): boolean {
|
||||
return (
|
||||
!CORE_UNSAVED.has(type) &&
|
||||
!CORE_CAPTURABLE.has(type) &&
|
||||
type !== "serial" &&
|
||||
!getTabType(type)
|
||||
!CORE_UNSAVED.has(type) && !CORE_CAPTURABLE.has(type) && !getTabType(type)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,14 +90,13 @@ export function buildLayoutTabSnapshots(
|
||||
const snapshots: WorkspaceTabSnapshot[] = capturable.map((tab) => ({
|
||||
slotId: slotIdByTabId.get(tab.id)!,
|
||||
type: tab.type,
|
||||
hostSyncId: tab.type === "serial" ? null : (tab.host?.syncId ?? null),
|
||||
hostSyncId: tab.host?.syncId ?? null,
|
||||
hostNameSnapshot: tab.host?.name ?? null,
|
||||
label: tab.label,
|
||||
customLabel: tab.customLabel,
|
||||
initialFilePath: tab.initialFilePath,
|
||||
initialPath: tab.initialPath,
|
||||
data: tab.data,
|
||||
serialConfig: tab.type === "serial" ? tab.serialConfig : undefined,
|
||||
}));
|
||||
|
||||
return { snapshots, slotIdByTabId };
|
||||
@@ -139,14 +134,9 @@ export function resolveLayoutTabTarget(
|
||||
snapshot: WorkspaceTabSnapshot,
|
||||
allHosts: Host[],
|
||||
):
|
||||
| { kind: "serial" }
|
||||
| { kind: "singleton"; host?: Host }
|
||||
| { kind: "host"; host: Host }
|
||||
| { kind: "skip" } {
|
||||
if (snapshot.type === "serial") {
|
||||
return snapshot.serialConfig ? { kind: "serial" } : { kind: "skip" };
|
||||
}
|
||||
|
||||
let host: Host | undefined;
|
||||
if (snapshot.hostSyncId) {
|
||||
host = allHosts.find((h) => h.syncId === snapshot.hostSyncId);
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface TabTypeDef {
|
||||
persistent?: boolean;
|
||||
singleton?: boolean;
|
||||
session?: boolean;
|
||||
/** Whether a session tab offers "Share session". Default true. */
|
||||
shareable?: boolean;
|
||||
hostless?: boolean;
|
||||
restore?: (host: Host) => boolean;
|
||||
activityTypes?: string[];
|
||||
@@ -91,7 +93,7 @@ export const resetTabTypes = registry.reset;
|
||||
/** Core tab types that are saved and reopened after login. */
|
||||
const CORE_PERSISTENT = new Set<string>();
|
||||
/** Core tab types that hold a live session (close confirm, refresh). */
|
||||
const CORE_SESSION = new Set(["serial"]);
|
||||
const CORE_SESSION = new Set<string>();
|
||||
/** Core tab types that restore without a host. */
|
||||
const CORE_HOSTLESS = new Set(["dashboard"]);
|
||||
|
||||
@@ -115,6 +117,11 @@ export function isSessionTabType(type: string): boolean {
|
||||
return CORE_SESSION.has(type) || !!registry.get(type)?.session;
|
||||
}
|
||||
|
||||
/** Whether a session tab of this type offers "Share session". Default true. */
|
||||
export function isShareableTabType(type: string): boolean {
|
||||
return registry.get(type)?.shareable !== false;
|
||||
}
|
||||
|
||||
export function isHostlessTabType(type: string): boolean {
|
||||
return CORE_HOSTLESS.has(type) || !!registry.get(type)?.hostless;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
LayoutPanelLeft,
|
||||
Server,
|
||||
Settings,
|
||||
Usb,
|
||||
User,
|
||||
Fingerprint,
|
||||
Hammer,
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SerialHandle } from "@/features/serial/serial-types";
|
||||
import type { Tab, TabType } from "@/types/ui-types";
|
||||
import { hostToSSHHost } from "@/lib/host-to-ssh-host";
|
||||
import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder";
|
||||
@@ -46,11 +44,6 @@ const HomepageCanvas = lazy(() =>
|
||||
default: m.HomepageCanvas,
|
||||
})),
|
||||
);
|
||||
const Serial = lazy(() =>
|
||||
import("@/features/serial/Serial").then((m) => ({
|
||||
default: m.Serial,
|
||||
})),
|
||||
);
|
||||
// Rail panels promoted to full tabs.
|
||||
const TermixIdPanel = lazy(() =>
|
||||
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
|
||||
@@ -134,8 +127,6 @@ export function tabIcon(type: TabType) {
|
||||
return <User className="size-3.5" />;
|
||||
case "admin-settings":
|
||||
return <Settings className="size-3.5" />;
|
||||
case "serial":
|
||||
return <Usb className="size-3.5" />;
|
||||
case "homepage":
|
||||
return <LayoutGrid className="size-3.5" />;
|
||||
case "collab":
|
||||
@@ -237,18 +228,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) {
|
||||
/>,
|
||||
);
|
||||
|
||||
case "serial":
|
||||
if (!tab.serialConfig)
|
||||
return <EmptyState icon={Usb} messageKey="serial.notSupportedTitle" />;
|
||||
return withTabSuspense(
|
||||
<Serial
|
||||
ref={tab.terminalRef as React.Ref<SerialHandle>}
|
||||
config={tab.serialConfig}
|
||||
isVisible={isVisible}
|
||||
instanceId={tab.instanceId}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "homepage":
|
||||
return withTabSuspense(<HomepageCanvas />);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ export type CoreRailView =
|
||||
| "credentials"
|
||||
| "termix-id"
|
||||
| "quick-connect"
|
||||
| "serial"
|
||||
| ToolsTab
|
||||
| "connections"
|
||||
| "session-logs"
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RefreshCw, Usb, TriangleAlert } from "lucide-react";
|
||||
import { Input } from "@/components/input";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { websocketAuthProtocols } from "@/lib/ws-auth";
|
||||
import type { SerialConfig } from "@/types/ui-types";
|
||||
import { Select2 } from "@/components/select2";
|
||||
|
||||
const BAUD_RATES = [
|
||||
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
|
||||
921600,
|
||||
];
|
||||
|
||||
interface SerialPanelProps {
|
||||
onConnect: (config: SerialConfig) => void;
|
||||
}
|
||||
|
||||
// In Chrome/Edge the Web Serial API shows its own native port picker on requestPort(),
|
||||
// so getPorts() only returns previously-granted ports. We never need the user to type
|
||||
// a path — the browser handles selection at connect time.
|
||||
const useWebSerial = !isElectron() && "serial" in navigator;
|
||||
const serialUnsupported = !isElectron() && !("serial" in navigator);
|
||||
|
||||
export function SerialPanel({ onConnect }: SerialPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [path, setPath] = useState("");
|
||||
const [baudRate, setBaudRate] = useState(115200);
|
||||
const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8);
|
||||
const [stopBits, setStopBits] = useState<1 | 2>(1);
|
||||
const [parity, setParity] = useState<"none" | "even" | "odd">("none");
|
||||
// Electron only: ports discovered from the backend
|
||||
const [availablePorts, setAvailablePorts] = useState<string[]>([]);
|
||||
const [loadingPorts, setLoadingPorts] = useState(false);
|
||||
|
||||
const buildWsUrl = () => {
|
||||
// Serial is always local -- the device is physically attached to this
|
||||
// desktop machine, so it never routes through a remote server.
|
||||
return "ws://127.0.0.1:30011";
|
||||
};
|
||||
|
||||
const refreshPorts = useCallback(() => {
|
||||
if (!isElectron()) return;
|
||||
setLoadingPorts(true);
|
||||
const url = buildWsUrl();
|
||||
if (!url) {
|
||||
setLoadingPorts(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(
|
||||
url,
|
||||
websocketAuthProtocols(localStorage.getItem("jwt")),
|
||||
);
|
||||
ws.onopen = () => ws.send(JSON.stringify({ type: "list_ports" }));
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data as string) as {
|
||||
type: string;
|
||||
data?: { path: string }[];
|
||||
};
|
||||
if (msg.type === "ports_list" && Array.isArray(msg.data)) {
|
||||
const paths = msg.data.map((p) => p.path);
|
||||
setAvailablePorts(paths);
|
||||
if (!path && paths.length > 0) setPath(paths[0]);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
ws.close();
|
||||
setLoadingPorts(false);
|
||||
}
|
||||
};
|
||||
ws.onerror = () => setLoadingPorts(false);
|
||||
}, [path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) refreshPorts();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const connect = () => {
|
||||
// For Web Serial, path is irrelevant — the browser picker handles it.
|
||||
// We pass an empty string; Serial.tsx calls requestPort() itself.
|
||||
if (useWebSerial) {
|
||||
onConnect({ path: "", baudRate, dataBits, stopBits, parity });
|
||||
return;
|
||||
}
|
||||
const finalPath = path.trim();
|
||||
if (!finalPath) return;
|
||||
onConnect({ path: finalPath, baudRate, dataBits, stopBits, parity });
|
||||
};
|
||||
|
||||
// ── Not supported (Firefox, Safari, etc.) ─────────────────────────────
|
||||
|
||||
if (serialUnsupported) {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex flex-col gap-2 rounded border border-destructive/30 bg-destructive/5 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TriangleAlert className="size-3.5 text-destructive shrink-0" />
|
||||
<span className="text-xs font-semibold text-destructive">
|
||||
{t("serial.notSupportedTitle")}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{t("serial.notSupported")}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{t("serial.hideHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Web Serial (Chrome / Edge) ─────────────────────────────────────────
|
||||
|
||||
if (useWebSerial) {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{t("serial.browserPickerHint")}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.baudRateLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={baudRate}
|
||||
onChange={(e) => setBaudRate(Number(e.target.value))}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{BAUD_RATES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.dataBitsLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={dataBits}
|
||||
onChange={(e) =>
|
||||
setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8)
|
||||
}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{([5, 6, 7, 8] as const).map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.stopBitsLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={stopBits}
|
||||
onChange={(e) => setStopBits(Number(e.target.value) as 1 | 2)}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value={1}>1</option>
|
||||
<option value={2}>2</option>
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.parityLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={parity}
|
||||
onChange={(e) =>
|
||||
setParity(e.target.value as "none" | "even" | "odd")
|
||||
}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="none">{t("serial.parityNone")}</option>
|
||||
<option value="even">{t("serial.parityEven")}</option>
|
||||
<option value="odd">{t("serial.parityOdd")}</option>
|
||||
</Select2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={connect}
|
||||
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors mt-1"
|
||||
>
|
||||
<Usb className="size-3.5" />
|
||||
{t("serial.connect")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Electron (backend serialport) ─────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.portLabel")}
|
||||
</label>
|
||||
<button
|
||||
onClick={refreshPorts}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t("serial.refreshPorts")}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`size-3 ${loadingPorts ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{availablePorts.length > 0 && (
|
||||
<Select2
|
||||
value={path}
|
||||
onChange={(e) => setPath(e.target.value)}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{t("serial.portPlaceholder")}</option>
|
||||
{availablePorts.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t("serial.portPlaceholder")}
|
||||
value={path}
|
||||
onChange={(e) => setPath(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") connect();
|
||||
}}
|
||||
className="h-7 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.baudRateLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={baudRate}
|
||||
onChange={(e) => setBaudRate(Number(e.target.value))}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{BAUD_RATES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.dataBitsLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={dataBits}
|
||||
onChange={(e) =>
|
||||
setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8)
|
||||
}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{([5, 6, 7, 8] as const).map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.stopBitsLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={stopBits}
|
||||
onChange={(e) => setStopBits(Number(e.target.value) as 1 | 2)}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value={1}>1</option>
|
||||
<option value={2}>2</option>
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("serial.parityLabel")}
|
||||
</label>
|
||||
<Select2
|
||||
value={parity}
|
||||
onChange={(e) =>
|
||||
setParity(e.target.value as "none" | "even" | "odd")
|
||||
}
|
||||
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="none">{t("serial.parityNone")}</option>
|
||||
<option value="even">{t("serial.parityEven")}</option>
|
||||
<option value="odd">{t("serial.parityOdd")}</option>
|
||||
</Select2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={connect}
|
||||
disabled={!path.trim()}
|
||||
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed mt-1"
|
||||
>
|
||||
<Usb className="size-3.5" />
|
||||
{t("serial.connect")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ScrollText,
|
||||
Server,
|
||||
Settings,
|
||||
Usb,
|
||||
User,
|
||||
Zap,
|
||||
Presentation,
|
||||
@@ -104,7 +103,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
|
||||
separatorAfter: true,
|
||||
mobilePrimary: true,
|
||||
},
|
||||
{ id: "serial", icon: Usb, labelKey: "nav.serial", separatorAfter: true },
|
||||
{
|
||||
id: "ssh-tools",
|
||||
icon: Hammer,
|
||||
@@ -122,7 +120,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
|
||||
promotable: true,
|
||||
rightDockable: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: "session-logs",
|
||||
icon: ScrollText,
|
||||
|
||||
@@ -57,16 +57,6 @@ describe("resolveConnectionOrigin", () => {
|
||||
).resolves.toBe("local");
|
||||
});
|
||||
|
||||
it("always resolves serial to local, even with a remote override", async () => {
|
||||
win.IS_ELECTRON = true;
|
||||
await expect(
|
||||
resolveConnectionOrigin({
|
||||
connectionType: "serial",
|
||||
connectionOrigin: "remote",
|
||||
}),
|
||||
).resolves.toBe("local");
|
||||
});
|
||||
|
||||
it("resolves to local outside Electron regardless of connectionType", async () => {
|
||||
await expect(
|
||||
resolveConnectionOrigin({
|
||||
|
||||
@@ -128,34 +128,6 @@ describe("buildWorkspaceTabSnapshots", () => {
|
||||
expect(snapshots).toHaveLength(1);
|
||||
expect(snapshots[0].type).toBe("terminal");
|
||||
});
|
||||
|
||||
it("captures serialConfig only for serial tabs", () => {
|
||||
const tabs: Tab[] = [
|
||||
makeTab({
|
||||
id: "t1",
|
||||
type: "serial",
|
||||
serialConfig: {
|
||||
path: "/dev/ttyUSB0",
|
||||
baudRate: 9600,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: "none",
|
||||
},
|
||||
}),
|
||||
makeTab({ id: "t2", type: "terminal", host: makeHost() }),
|
||||
];
|
||||
|
||||
const { snapshots } = buildWorkspaceTabSnapshots(tabs);
|
||||
expect(snapshots[0].serialConfig).toEqual({
|
||||
path: "/dev/ttyUSB0",
|
||||
baudRate: 9600,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: "none",
|
||||
});
|
||||
expect(snapshots[0].hostSyncId).toBeNull();
|
||||
expect(snapshots[1].serialConfig).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("remapSlotIds", () => {
|
||||
@@ -175,35 +147,6 @@ describe("remapSlotIds", () => {
|
||||
describe("resolveWorkspaceTabTarget", () => {
|
||||
const hosts: Host[] = [makeHost({ id: "1", syncId: "sync-web-01" })];
|
||||
|
||||
it("resolves a serial snapshot with a config as 'serial'", () => {
|
||||
const snapshot: WorkspaceTabSnapshot = {
|
||||
slotId: "s1",
|
||||
type: "serial",
|
||||
label: "Serial",
|
||||
serialConfig: {
|
||||
path: "/dev/ttyUSB0",
|
||||
baudRate: 9600,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: "none",
|
||||
},
|
||||
};
|
||||
expect(resolveWorkspaceTabTarget(snapshot, hosts)).toEqual({
|
||||
kind: "serial",
|
||||
});
|
||||
});
|
||||
|
||||
it("skips a serial snapshot missing its config", () => {
|
||||
const snapshot: WorkspaceTabSnapshot = {
|
||||
slotId: "s1",
|
||||
type: "serial",
|
||||
label: "Serial",
|
||||
};
|
||||
expect(resolveWorkspaceTabTarget(snapshot, hosts)).toEqual({
|
||||
kind: "skip",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves a host-bound snapshot by matching syncId", () => {
|
||||
const snapshot: WorkspaceTabSnapshot = {
|
||||
slotId: "s1",
|
||||
@@ -253,7 +196,7 @@ describe("resolveWorkspaceTabTarget", () => {
|
||||
expect((result as { host?: Host }).host?.syncId).toBe("sync-web-01");
|
||||
});
|
||||
|
||||
it("skips a non-singleton, non-serial, non-plugin type with no resolvable host", () => {
|
||||
it("skips a non-singleton, non-plugin type with no resolvable host", () => {
|
||||
const snapshot: WorkspaceTabSnapshot = {
|
||||
slotId: "s1",
|
||||
type: "host-manager",
|
||||
|
||||
@@ -48,8 +48,9 @@ describe("RAIL_ITEMS", () => {
|
||||
// Guards against a destination silently disappearing now that the rail,
|
||||
// the settings toggles, the sidebar titles and the mobile bar all read
|
||||
// from this one list.
|
||||
// "sftp" moved to the file-manager plugin's own registerRailItem, and
|
||||
// "port-forwarding" to the tunnels plugin's.
|
||||
// "sftp" moved to the file-manager plugin's own registerRailItem,
|
||||
// "port-forwarding" to the tunnels plugin's, and "serial" to the serial
|
||||
// plugin's.
|
||||
expect(RAIL_ITEMS.map((item) => item.id)).toEqual([
|
||||
"hosts",
|
||||
"credentials",
|
||||
@@ -57,7 +58,6 @@ describe("RAIL_ITEMS", () => {
|
||||
"connections",
|
||||
"collab",
|
||||
"quick-connect",
|
||||
"serial",
|
||||
"ssh-tools",
|
||||
"macros",
|
||||
"session-logs",
|
||||
|
||||
Reference in new issue
Block a user