feat: implement local terminal support with shell configuration and UI integration
This commit is contained in:
@@ -1 +1 @@
|
||||
ce0d75b28075a7688ba4535c94b4784c
|
||||
99c6bbb0236daee684251a5f62bbd974
|
||||
@@ -467,6 +467,16 @@
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.settings-shell-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settings-shell-row .form-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-success {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--success);
|
||||
|
||||
@@ -253,6 +253,12 @@
|
||||
.sidebar-item .protocol-indicator.proto-github::before {
|
||||
content: "GH";
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-vnc::before {
|
||||
content: "⊞";
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-local::before {
|
||||
content: "›_";
|
||||
}
|
||||
|
||||
.sidebar-item .protocol-indicator.proto-ssh {
|
||||
background: rgba(78, 205, 196, 0.18);
|
||||
@@ -270,6 +276,21 @@
|
||||
background: rgba(124, 138, 255, 0.18);
|
||||
color: var(--protocol-github);
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-vnc {
|
||||
background: rgba(232, 93, 74, 0.18);
|
||||
color: var(--protocol-vnc);
|
||||
}
|
||||
.sidebar-item .protocol-indicator.proto-local {
|
||||
background: rgba(139, 195, 74, 0.18);
|
||||
color: var(--protocol-local);
|
||||
}
|
||||
|
||||
/* Local machine item separator */
|
||||
.sidebar-item-local {
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
margin: var(--space-2) var(--space-2) var(--space-2);
|
||||
padding-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* Text block (name + host) */
|
||||
.sidebar-item-text {
|
||||
|
||||
@@ -114,6 +114,14 @@
|
||||
background: rgba(124, 138, 255, 0.15);
|
||||
color: var(--protocol-github);
|
||||
}
|
||||
.tab-badge.badge-vnc {
|
||||
background: rgba(232, 93, 74, 0.15);
|
||||
color: var(--protocol-vnc);
|
||||
}
|
||||
.tab-badge.badge-local {
|
||||
background: rgba(139, 195, 74, 0.15);
|
||||
color: var(--protocol-local);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
Status dot (connection state)
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
--protocol-raw: #ab47bc;
|
||||
--protocol-github: #7c8aff;
|
||||
--protocol-vnc: #e85d4a;
|
||||
--protocol-local: #8bc34a;
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
Spacing scale (4 px base unit)
|
||||
|
||||
@@ -541,6 +541,9 @@
|
||||
this._bindClick("btn-new-tab", () => this.profileEditor.openNew());
|
||||
this._bindClick("btn-broadcast", () => this.toggleBroadcast());
|
||||
|
||||
// Wire the "Local Machine" sidebar button
|
||||
this.sidebar.onConnectLocal = () => this.connectLocal();
|
||||
|
||||
// Right-click on broadcast button opens target selector
|
||||
const broadcastBtn = document.getElementById("btn-broadcast");
|
||||
if (broadcastBtn) {
|
||||
@@ -809,6 +812,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a local terminal session.
|
||||
*/
|
||||
async connectLocal() {
|
||||
try {
|
||||
const sessionInfo = await window.go.api.Service.ConnectLocal();
|
||||
this.state.addSession(sessionInfo);
|
||||
this.tabs.addTab(sessionInfo);
|
||||
|
||||
const panelId = `panel-${sessionInfo.id}`;
|
||||
const prefs = this.state.preferences || {};
|
||||
this.terminals.createTerminal(sessionInfo.id, panelId, {
|
||||
terminalFontFamily: prefs.defaultFontFamily,
|
||||
terminalFontSize: prefs.defaultFontSize,
|
||||
terminalScrollback: prefs.defaultScrollback,
|
||||
terminalCursorStyle: prefs.defaultCursorStyle,
|
||||
});
|
||||
|
||||
this.notifications.info("Opening local terminal…");
|
||||
} catch (err) {
|
||||
this.notifications.error("Local terminal failed: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a quick connect dialog for ad-hoc connections without saving a profile.
|
||||
*/
|
||||
|
||||
@@ -107,6 +107,14 @@
|
||||
action: () => this.app.quickConnect(),
|
||||
});
|
||||
|
||||
// Local terminal
|
||||
cmds.push({
|
||||
id: "local-terminal",
|
||||
label: "Open Local Terminal",
|
||||
section: "Actions",
|
||||
action: () => this.app.connectLocal(),
|
||||
});
|
||||
|
||||
// Tab actions
|
||||
cmds.push({
|
||||
id: "close-tab",
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
this._knownHosts = [];
|
||||
}
|
||||
|
||||
try {
|
||||
this._defaultShell = await window.go.api.Service.GetDefaultShell();
|
||||
} catch (_) {
|
||||
this._defaultShell = "";
|
||||
}
|
||||
|
||||
this._render();
|
||||
}
|
||||
|
||||
@@ -281,6 +287,45 @@
|
||||
{ min: 180, max: 500 },
|
||||
),
|
||||
);
|
||||
|
||||
// Local shell path with browse button
|
||||
const shellGroup = createElement("div", "form-group settings-field");
|
||||
const shellLabel = createElement("label", "form-label", {
|
||||
textContent: "Local shell",
|
||||
for: "setting-localShell",
|
||||
});
|
||||
shellGroup.appendChild(shellLabel);
|
||||
|
||||
const shellRow = createElement("div", "settings-shell-row");
|
||||
const shellInput = createElement("input", "form-input", {
|
||||
type: "text",
|
||||
id: "setting-localShell",
|
||||
name: "setting-localShell",
|
||||
value: p.localShell || "",
|
||||
placeholder: this._defaultShell || "/bin/zsh",
|
||||
});
|
||||
shellRow.appendChild(shellInput);
|
||||
|
||||
const browseBtn = createElement("button", "btn btn-sm btn-secondary", {
|
||||
textContent: "Browse…",
|
||||
type: "button",
|
||||
});
|
||||
browseBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const path = await window.go.api.Service.BrowseForShell();
|
||||
if (path) {
|
||||
shellInput.value = path;
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
shellRow.appendChild(browseBtn);
|
||||
shellGroup.appendChild(shellRow);
|
||||
|
||||
const shellHint = createElement("span", "form-hint", {
|
||||
textContent: "Leave empty to use system default",
|
||||
});
|
||||
shellGroup.appendChild(shellHint);
|
||||
body.appendChild(shellGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1050,6 +1095,7 @@
|
||||
"defaultScrollback",
|
||||
"defaultCursorStyle",
|
||||
"useNativePaste",
|
||||
"localShell",
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
this.el.appendChild(this._renderBulkActions());
|
||||
}
|
||||
|
||||
// Local Machine quick-connect button
|
||||
this.el.appendChild(this._renderLocalMachineItem());
|
||||
|
||||
// Favourites
|
||||
const favourites = profiles.filter((p) => p.favourite);
|
||||
if (favourites.length > 0) {
|
||||
@@ -531,6 +534,36 @@
|
||||
);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_renderLocalMachineItem() {
|
||||
const item = createElement("div", "sidebar-item sidebar-item-local");
|
||||
item.addEventListener("mousedown", (e) => e.preventDefault());
|
||||
|
||||
const indicator = createElement(
|
||||
"span",
|
||||
"protocol-indicator proto-local",
|
||||
{
|
||||
title: "Local",
|
||||
},
|
||||
);
|
||||
item.appendChild(indicator);
|
||||
|
||||
const textWrapper = createElement("div", "sidebar-item-text");
|
||||
const nameEl = createElement("span", "sidebar-item-name", {
|
||||
textContent: "Local Machine",
|
||||
});
|
||||
textWrapper.appendChild(nameEl);
|
||||
item.appendChild(textWrapper);
|
||||
|
||||
item.addEventListener("click", () => {
|
||||
if (this.onConnectLocal) {
|
||||
this.onConnectLocal();
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_renderBulkActions() {
|
||||
const bar = createElement("div", "sidebar-bulk-actions");
|
||||
|
||||
@@ -480,7 +480,9 @@
|
||||
const protoLabel =
|
||||
sessionInfo.protocol === "github"
|
||||
? "GitHub"
|
||||
: sessionInfo.protocol.toUpperCase();
|
||||
: sessionInfo.protocol === "local"
|
||||
? "Local"
|
||||
: sessionInfo.protocol.toUpperCase();
|
||||
const badge = createElement(
|
||||
"span",
|
||||
`tab-badge badge-${sessionInfo.protocol}`,
|
||||
|
||||
@@ -229,6 +229,8 @@
|
||||
return "proto-github";
|
||||
case "vnc":
|
||||
return "proto-vnc";
|
||||
case "local":
|
||||
return "proto-local";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
Vendored
+17
@@ -1,8 +1,11 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {models} from '../models';
|
||||
import {api} from '../models';
|
||||
import {context} from '../models';
|
||||
|
||||
export function BrowseForShell():Promise<string>;
|
||||
|
||||
export function CancelReconnect(arg1:string):Promise<void>;
|
||||
|
||||
export function CheckForUpdates():Promise<Record<string, any>>;
|
||||
@@ -11,6 +14,8 @@ export function ClearSessionHistory(arg1:string):Promise<void>;
|
||||
|
||||
export function CloseAllSessions():Promise<void>;
|
||||
|
||||
export function ConnectLocal():Promise<models.SessionInfo>;
|
||||
|
||||
export function ConnectQuick(arg1:models.Profile):Promise<models.SessionInfo>;
|
||||
|
||||
export function ConnectSession(arg1:string):Promise<models.SessionInfo>;
|
||||
@@ -27,6 +32,8 @@ export function DeleteProfileSecrets(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteRecording(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteSSHKey(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteSnippet(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteTOTPSecret(arg1:string):Promise<void>;
|
||||
@@ -45,20 +52,28 @@ export function FileExists(arg1:string):Promise<boolean>;
|
||||
|
||||
export function FileIsPrivateKey(arg1:string):Promise<boolean>;
|
||||
|
||||
export function GenerateSSHKey(arg1:string,arg2:string,arg3:number,arg4:string):Promise<void>;
|
||||
|
||||
export function GetAppInfo():Promise<Record<string, string>>;
|
||||
|
||||
export function GetDefaultShell():Promise<string>;
|
||||
|
||||
export function GetPreferences():Promise<models.AppPreferences>;
|
||||
|
||||
export function GetProfile(arg1:string):Promise<models.Profile>;
|
||||
|
||||
export function GetRecording(arg1:string):Promise<Record<string, any>>;
|
||||
|
||||
export function GetSSHPublicKey(arg1:string):Promise<string>;
|
||||
|
||||
export function GetSessionCwd(arg1:string):Promise<string>;
|
||||
|
||||
export function GetSessionHistory(arg1:string):Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetTOTPCode(arg1:string):Promise<string>;
|
||||
|
||||
export function GetVNCWebSocketURL(arg1:string):Promise<string>;
|
||||
|
||||
export function HasTOTPSecret(arg1:string):Promise<boolean>;
|
||||
|
||||
export function ImportProfiles(arg1:string,arg2:string):Promise<number>;
|
||||
@@ -93,6 +108,8 @@ export function ListProfilesByTag(arg1:string):Promise<Array<models.Profile>>;
|
||||
|
||||
export function ListRecordings():Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function ListSSHKeyPairs():Promise<Array<api.SSHKeyInfo>>;
|
||||
|
||||
export function ListSSHKeys():Promise<Array<string>>;
|
||||
|
||||
export function ListSessions():Promise<Array<models.SessionInfo>>;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function BrowseForShell() {
|
||||
return window['go']['api']['Service']['BrowseForShell']();
|
||||
}
|
||||
|
||||
export function CancelReconnect(arg1) {
|
||||
return window['go']['api']['Service']['CancelReconnect'](arg1);
|
||||
}
|
||||
@@ -18,6 +22,10 @@ export function CloseAllSessions() {
|
||||
return window['go']['api']['Service']['CloseAllSessions']();
|
||||
}
|
||||
|
||||
export function ConnectLocal() {
|
||||
return window['go']['api']['Service']['ConnectLocal']();
|
||||
}
|
||||
|
||||
export function ConnectQuick(arg1) {
|
||||
return window['go']['api']['Service']['ConnectQuick'](arg1);
|
||||
}
|
||||
@@ -50,6 +58,10 @@ export function DeleteRecording(arg1) {
|
||||
return window['go']['api']['Service']['DeleteRecording'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteSSHKey(arg1) {
|
||||
return window['go']['api']['Service']['DeleteSSHKey'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteSnippet(arg1) {
|
||||
return window['go']['api']['Service']['DeleteSnippet'](arg1);
|
||||
}
|
||||
@@ -86,10 +98,18 @@ export function FileIsPrivateKey(arg1) {
|
||||
return window['go']['api']['Service']['FileIsPrivateKey'](arg1);
|
||||
}
|
||||
|
||||
export function GenerateSSHKey(arg1, arg2, arg3, arg4) {
|
||||
return window['go']['api']['Service']['GenerateSSHKey'](arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
export function GetAppInfo() {
|
||||
return window['go']['api']['Service']['GetAppInfo']();
|
||||
}
|
||||
|
||||
export function GetDefaultShell() {
|
||||
return window['go']['api']['Service']['GetDefaultShell']();
|
||||
}
|
||||
|
||||
export function GetPreferences() {
|
||||
return window['go']['api']['Service']['GetPreferences']();
|
||||
}
|
||||
@@ -102,6 +122,10 @@ export function GetRecording(arg1) {
|
||||
return window['go']['api']['Service']['GetRecording'](arg1);
|
||||
}
|
||||
|
||||
export function GetSSHPublicKey(arg1) {
|
||||
return window['go']['api']['Service']['GetSSHPublicKey'](arg1);
|
||||
}
|
||||
|
||||
export function GetSessionCwd(arg1) {
|
||||
return window['go']['api']['Service']['GetSessionCwd'](arg1);
|
||||
}
|
||||
@@ -114,6 +138,10 @@ export function GetTOTPCode(arg1) {
|
||||
return window['go']['api']['Service']['GetTOTPCode'](arg1);
|
||||
}
|
||||
|
||||
export function GetVNCWebSocketURL(arg1) {
|
||||
return window['go']['api']['Service']['GetVNCWebSocketURL'](arg1);
|
||||
}
|
||||
|
||||
export function HasTOTPSecret(arg1) {
|
||||
return window['go']['api']['Service']['HasTOTPSecret'](arg1);
|
||||
}
|
||||
@@ -182,6 +210,10 @@ export function ListRecordings() {
|
||||
return window['go']['api']['Service']['ListRecordings']();
|
||||
}
|
||||
|
||||
export function ListSSHKeyPairs() {
|
||||
return window['go']['api']['Service']['ListSSHKeyPairs']();
|
||||
}
|
||||
|
||||
export function ListSSHKeys() {
|
||||
return window['go']['api']['Service']['ListSSHKeys']();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
export namespace api {
|
||||
|
||||
export class SSHKeyInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
bits: string;
|
||||
publicKey: string;
|
||||
privateKeyPath: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SSHKeyInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.type = source["type"];
|
||||
this.bits = source["bits"];
|
||||
this.publicKey = source["publicKey"];
|
||||
this.privateKeyPath = source["privateKeyPath"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace models {
|
||||
|
||||
export class AppPreferences {
|
||||
@@ -13,6 +38,7 @@ export namespace models {
|
||||
defaultScrollback: number;
|
||||
defaultCursorStyle: string;
|
||||
useNativePaste: boolean;
|
||||
localShell: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AppPreferences(source);
|
||||
@@ -32,6 +58,7 @@ export namespace models {
|
||||
this.defaultScrollback = source["defaultScrollback"];
|
||||
this.defaultCursorStyle = source["defaultCursorStyle"];
|
||||
this.useNativePaste = source["useNativePaste"];
|
||||
this.localShell = source["localShell"];
|
||||
}
|
||||
}
|
||||
export class AuditEntry {
|
||||
@@ -222,6 +249,46 @@ export namespace models {
|
||||
this.label = source["label"];
|
||||
}
|
||||
}
|
||||
export class VNCConfig {
|
||||
vncHost: string;
|
||||
vncPort: number;
|
||||
secretMode: string;
|
||||
viewOnly: boolean;
|
||||
scaleToFit: boolean;
|
||||
quality: number;
|
||||
sharedSession: boolean;
|
||||
sshTunnel: boolean;
|
||||
sshHost: string;
|
||||
sshPort: number;
|
||||
sshUsername: string;
|
||||
sshAuthMethod: string;
|
||||
sshSecretMode: string;
|
||||
sshPrivateKeyPath: string;
|
||||
sshAgentAuth: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new VNCConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.vncHost = source["vncHost"];
|
||||
this.vncPort = source["vncPort"];
|
||||
this.secretMode = source["secretMode"];
|
||||
this.viewOnly = source["viewOnly"];
|
||||
this.scaleToFit = source["scaleToFit"];
|
||||
this.quality = source["quality"];
|
||||
this.sharedSession = source["sharedSession"];
|
||||
this.sshTunnel = source["sshTunnel"];
|
||||
this.sshHost = source["sshHost"];
|
||||
this.sshPort = source["sshPort"];
|
||||
this.sshUsername = source["sshUsername"];
|
||||
this.sshAuthMethod = source["sshAuthMethod"];
|
||||
this.sshSecretMode = source["sshSecretMode"];
|
||||
this.sshPrivateKeyPath = source["sshPrivateKeyPath"];
|
||||
this.sshAgentAuth = source["sshAgentAuth"];
|
||||
}
|
||||
}
|
||||
export class RAWConfig {
|
||||
autoReconnect: boolean;
|
||||
lineEnding: string;
|
||||
@@ -410,6 +477,7 @@ export namespace models {
|
||||
telnet?: TelnetConfig;
|
||||
raw?: RAWConfig;
|
||||
github?: GitHubConfig;
|
||||
vnc?: VNCConfig;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Profile(source);
|
||||
@@ -442,6 +510,7 @@ export namespace models {
|
||||
this.telnet = this.convertValues(source["telnet"], TelnetConfig);
|
||||
this.raw = this.convertValues(source["raw"], RAWConfig);
|
||||
this.github = this.convertValues(source["github"], GitHubConfig);
|
||||
this.vnc = this.convertValues(source["vnc"], VNCConfig);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
@@ -522,6 +591,7 @@ export namespace models {
|
||||
this.updatedAt = source["updatedAt"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user