feat(terminal): add embedded PTY-backed terminal dock with per-tab sessions
Adds a built-in terminal dock that runs a real pseudo-terminal per repository tab. - Backend: new src-tauri/src/terminal.rs implements TerminalState and Tauri commands terminal_open, terminal_write, terminal_resize and terminal_close using the portable-pty crate. Terminal output is emitted via "terminal:data" and exits via "terminal:exit". Includes unit tests for UTF-8 chunk handling. Cargo.toml updated to depend on portable-pty and main.rs registers the state/commands. - Frontend: App.svelte/UI and app.css updated to show a resizable terminal dock, toggleable with Ctrl+` and persisted open/height state. TerminalPanel.svelte is lazy-loaded per repo tab (frontend provides session ids like repo:<path>). package.json adds @xterm/xterm and @xterm/addon-fit for the frontend terminal surface. - Docs: CHANGELOG.md notes the new embedded terminal feature. Behavior: each repository tab has its own PTY-backed shell started in the repo cwd; the user's shell (SHELL/COMSPEC) is used so prompts, aliases and interactive behavior work as in a normal terminal.
This commit is contained in:
+111
-1
@@ -5,7 +5,7 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { AlertCircle, Cherry, CloudOff, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
|
||||
import { AlertCircle, Cherry, CloudOff, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, SquareTerminal, Star, X } from "@lucide/svelte";
|
||||
import { beginFrontendShutdown, resumeFrontend } from "./lib/telemetry";
|
||||
import { setLanguage, t } from "./lib/i18n.svelte";
|
||||
import type { ConfirmRequest } from "./lib/components/ConfirmDialog.svelte";
|
||||
@@ -298,6 +298,10 @@
|
||||
const AUTO_REFRESH_ENABLED_KEY = "gitlite.autoRefreshEnabled.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||
const TERMINAL_HEIGHT_KEY = "gitlite.terminalHeight.v1";
|
||||
const TERMINAL_OPEN_KEY = "gitlite.terminalOpen.v1";
|
||||
const TERMINAL_MIN_HEIGHT = 120;
|
||||
const TERMINAL_MAX_HEIGHT = 720;
|
||||
const SIDEBAR_VISIBILITY_KEY = "gitlite.sidebarVisibility.v1";
|
||||
const SIDEBAR_PANEL_HEIGHTS_KEY = "gitlite.sidebarPanelHeights.v2";
|
||||
const BRANCH_PANEL_COLLAPSED_KEY = "gitlite.branchPanelCollapsed.v1";
|
||||
@@ -566,6 +570,11 @@
|
||||
let resizeStartY = 0;
|
||||
let resizeStartHeight = 0;
|
||||
let leftSidebarWidth = loadLeftSidebarWidth();
|
||||
let terminalHeight = loadTerminalHeight();
|
||||
let terminalOpen = loadTerminalOpen();
|
||||
let resizingTerminal = false;
|
||||
let terminalResizeStartY = 0;
|
||||
let terminalResizeStartHeight = 0;
|
||||
let resizingLeftSidebar = false;
|
||||
let leftSidebarResizeStartX = 0;
|
||||
let leftSidebarResizeStartWidth = 0;
|
||||
@@ -2041,6 +2050,60 @@
|
||||
persistCommitPanelHeight(commitPanelHeight);
|
||||
}
|
||||
|
||||
// ── Embedded terminal ──────────────────────────────────────────────────────
|
||||
function clampTerminalHeight(value: number): number {
|
||||
return Math.min(TERMINAL_MAX_HEIGHT, Math.max(TERMINAL_MIN_HEIGHT, Math.round(value)));
|
||||
}
|
||||
|
||||
function loadTerminalHeight(): number {
|
||||
try {
|
||||
const stored = Number(localStorage.getItem(TERMINAL_HEIGHT_KEY));
|
||||
return Number.isFinite(stored) && stored > 0 ? clampTerminalHeight(stored) : 260;
|
||||
} catch { return 260; }
|
||||
}
|
||||
|
||||
function persistTerminalHeight(value: number) {
|
||||
try { localStorage.setItem(TERMINAL_HEIGHT_KEY, String(value)); } catch { /* storage may be unavailable */ }
|
||||
}
|
||||
|
||||
function loadTerminalOpen(): boolean {
|
||||
try { return localStorage.getItem(TERMINAL_OPEN_KEY) === "1"; } catch { return false; }
|
||||
}
|
||||
|
||||
function toggleTerminal() {
|
||||
terminalOpen = !terminalOpen;
|
||||
try { localStorage.setItem(TERMINAL_OPEN_KEY, terminalOpen ? "1" : "0"); } catch { /* storage may be unavailable */ }
|
||||
}
|
||||
|
||||
function startTerminalResize(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
resizingTerminal = true;
|
||||
terminalResizeStartY = event.clientY;
|
||||
terminalResizeStartHeight = terminalHeight;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function onTerminalResizeMove(event: PointerEvent) {
|
||||
if (!resizingTerminal) return;
|
||||
// The dock grows upwards, so a smaller clientY means a taller terminal.
|
||||
terminalHeight = clampTerminalHeight(terminalResizeStartHeight + (terminalResizeStartY - event.clientY));
|
||||
}
|
||||
|
||||
function endTerminalResize(event: PointerEvent) {
|
||||
if (!resizingTerminal) return;
|
||||
resizingTerminal = false;
|
||||
persistTerminalHeight(terminalHeight);
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function onTerminalResizeKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||
event.preventDefault();
|
||||
terminalHeight = clampTerminalHeight(terminalHeight + (event.key === "ArrowUp" ? 20 : -20));
|
||||
persistTerminalHeight(terminalHeight);
|
||||
}
|
||||
|
||||
function startLeftSidebarResize(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
resizingLeftSidebar = true;
|
||||
@@ -6012,6 +6075,13 @@
|
||||
openHelp();
|
||||
return;
|
||||
}
|
||||
// Backquote by code, so the shortcut survives keyboard layouts where ` is
|
||||
// a dead key.
|
||||
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.code === "Backquote") {
|
||||
event.preventDefault();
|
||||
if (!event.repeat && workspaceActive) toggleTerminal();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape" && commandPaletteOpen) {
|
||||
commandPaletteOpen = false;
|
||||
return;
|
||||
@@ -6671,6 +6741,41 @@
|
||||
}}
|
||||
/>
|
||||
</aside>
|
||||
{#if terminalOpen}
|
||||
<section class="terminal-dock" style="--terminal-height: {terminalHeight}px" aria-label={t("terminal.title")}>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="terminal-resize-handle"
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label={t("terminal.resize")}
|
||||
aria-valuenow={terminalHeight}
|
||||
aria-valuemin={TERMINAL_MIN_HEIGHT}
|
||||
aria-valuemax={TERMINAL_MAX_HEIGHT}
|
||||
tabindex="0"
|
||||
onpointerdown={startTerminalResize}
|
||||
onpointermove={onTerminalResizeMove}
|
||||
onpointerup={endTerminalResize}
|
||||
onpointercancel={endTerminalResize}
|
||||
onkeydown={onTerminalResizeKeydown}
|
||||
></div>
|
||||
<header class="terminal-dock-head">
|
||||
<span class="terminal-dock-title"><SquareTerminal size={13} aria-hidden="true" />{t("terminal.title")}</span>
|
||||
<span class="terminal-dock-path" title={activeRepoPath}>{activeRepoPath}</span>
|
||||
<button class="terminal-dock-close" type="button" onclick={toggleTerminal} title={t("terminal.hide")} aria-label={t("terminal.hide")}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="terminal-dock-body">
|
||||
{#await import("./lib/components/TerminalPanel.svelte") then module}
|
||||
{#each repoTabs as tab (tab.path)}
|
||||
<module.default sessionId={`repo:${tab.path}`} repoPath={tab.path} active={sameRepoPath(tab.path, activeRepoPath)} />
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
<footer class="workspace-statusbar" aria-label="Application status summary">
|
||||
@@ -6690,6 +6795,11 @@
|
||||
{/if}
|
||||
<span class:active={autoRefreshEnabled} class="workspace-auto">Auto <i aria-hidden="true"></i></span>
|
||||
{/if}
|
||||
{#if workspaceActive}
|
||||
<button class:active={terminalOpen} class="workspace-terminal-toggle" type="button" onclick={toggleTerminal} aria-pressed={terminalOpen} title={terminalOpen ? t("terminal.hide") : t("terminal.show")}>
|
||||
<SquareTerminal size={12} aria-hidden="true" />{t("terminal.title")}
|
||||
</button>
|
||||
{/if}
|
||||
{#if appVersion}<span class="app-version" title={`Gitty version ${appVersion}`}>Gitty v{appVersion}</span>{/if}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+137
@@ -6453,6 +6453,7 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
|
||||
.workspace {
|
||||
grid-template-columns: minmax(220px, var(--left-sidebar-width, 280px)) 7px minmax(360px, 1fr) 7px minmax(var(--history-aside-min-width, 420px), var(--history-aside-width, 620px));
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
flex: 1 1 0;
|
||||
padding: 0;
|
||||
background: var(--color-border-subtle);
|
||||
@@ -6460,6 +6461,11 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
}
|
||||
.left-sidebar { background: var(--color-surface); }
|
||||
.main-panel { border: 0; border-radius: 0; background: var(--color-surface-solid); }
|
||||
.left-sidebar,
|
||||
.left-sidebar-resize-handle,
|
||||
.history-resize-handle,
|
||||
.history-aside { grid-row: 1 / -1; }
|
||||
.main-panel { grid-row: 1; }
|
||||
.history-aside { background: var(--color-border-subtle); row-gap: 0; }
|
||||
|
||||
.panel {
|
||||
@@ -9301,3 +9307,134 @@ section > header.page-header.page-header {
|
||||
|
||||
/* Tab strips keep their hidden scrollbars. */
|
||||
.repo-tabs-scroll { scrollbar-width: none; }
|
||||
|
||||
/* ── Embedded terminal ─────────────────────────────────────────────────────── */
|
||||
.terminal-dock {
|
||||
/* Center column only: the gutters beside it belong to the resize handles,
|
||||
which now run the full height. */
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
display: grid;
|
||||
grid-template-rows: 5px auto minmax(0, 1fr);
|
||||
height: var(--terminal-height, 260px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-surface-solid);
|
||||
}
|
||||
|
||||
.terminal-resize-handle {
|
||||
cursor: ns-resize;
|
||||
background: var(--color-border-subtle);
|
||||
touch-action: none;
|
||||
}
|
||||
.terminal-resize-handle:hover,
|
||||
.terminal-resize-handle:focus-visible {
|
||||
outline: none;
|
||||
background: color-mix(in srgb, var(--color-primary) 45%, var(--color-border));
|
||||
}
|
||||
|
||||
.terminal-dock-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 30px;
|
||||
padding: 0 8px 0 12px;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background: var(--color-surface-dim);
|
||||
}
|
||||
.terminal-dock-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--color-ink);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.terminal-dock-path {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.terminal-dock-close {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--color-ink-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.terminal-dock-close:hover {
|
||||
background: color-mix(in srgb, var(--color-danger) 16%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.terminal-dock-body {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-surface {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 6px 4px 6px 10px;
|
||||
}
|
||||
.terminal-surface.hidden { display: none; }
|
||||
.terminal-host { width: 100%; height: 100%; }
|
||||
|
||||
/* xterm draws its own scrollbar; keep it in the app's visual language. */
|
||||
.terminal-surface .xterm-viewport { background: transparent !important; }
|
||||
.terminal-surface .xterm-viewport::-webkit-scrollbar { width: 9px; }
|
||||
.terminal-surface .xterm-viewport::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-ink-muted) 34%, transparent);
|
||||
}
|
||||
|
||||
.terminal-error {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 8px;
|
||||
margin: 0;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--color-danger) 16%, var(--color-surface-solid));
|
||||
color: var(--color-danger);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.workspace-terminal-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 8px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.workspace-terminal-toggle:hover { background: color-mix(in srgb, var(--color-primary) 16%, transparent); }
|
||||
.workspace-terminal-toggle.active { color: var(--color-primary); }
|
||||
|
||||
/* Stacked layout: one column, so nothing spans rows. */
|
||||
@media (max-width: 760px) {
|
||||
.terminal-dock { grid-column: 1; }
|
||||
.left-sidebar,
|
||||
.left-sidebar-resize-handle,
|
||||
.history-resize-handle,
|
||||
.history-aside { grid-row: auto; }
|
||||
}
|
||||
|
||||
@@ -249,6 +249,7 @@
|
||||
commands: [
|
||||
{ command: "Ctrl + /", description: "Diese Hilfe öffnen" },
|
||||
{ command: "Ctrl + 1 … 4", description: "Zwischen Dashboard, Repositories, Pull Requests und Issues & Boards wechseln" },
|
||||
{ command: "Ctrl + ^", description: "Terminal im Repository ein- und ausblenden" },
|
||||
{ command: "Ctrl + A", description: "Alle Dateien in der aktiven Statusliste („Ungestaged“ oder „Gestaged“) auswählen" },
|
||||
{ command: "Escape", description: "Aktuelles Overlay oder Dialogfenster schließen – in der Statusliste die aktuelle Auswahl aufheben" },
|
||||
{ command: "Tab / Shift + Tab", description: "Zwischen Bedienelementen wechseln" },
|
||||
@@ -461,6 +462,7 @@
|
||||
commands: [
|
||||
{ command: "Ctrl + /", description: "Open this help center" },
|
||||
{ command: "Ctrl + 1 … 4", description: "Switch between Dashboard, Repositories, Pull Requests and Issues & Boards" },
|
||||
{ command: "Ctrl + `", description: "Show or hide the terminal inside the repository view" },
|
||||
{ command: "Ctrl + A", description: "Select every file in the focused status list (Unstaged or Staged)" },
|
||||
{ command: "Escape", description: "Close the current overlay or dialog – in the status list, clear the current selection" },
|
||||
{ command: "Tab / Shift + Tab", description: "Move between controls" },
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { closeTerminal, openTerminal, resizeTerminal, writeTerminal } from "../git";
|
||||
import { t } from "../i18n.svelte";
|
||||
|
||||
interface Props {
|
||||
/** Stable per repository tab, so every tab keeps its own shell. */
|
||||
sessionId: string;
|
||||
repoPath: string;
|
||||
/** Only the active tab's terminal is visible; the others stay alive. */
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
let { sessionId, repoPath, active = false }: Props = $props();
|
||||
|
||||
let host = $state<HTMLDivElement | null>(null);
|
||||
let terminal: Terminal | null = null;
|
||||
let fitAddon: FitAddon | null = null;
|
||||
let observer: ResizeObserver | null = null;
|
||||
let unlisteners: UnlistenFn[] = [];
|
||||
let started = false;
|
||||
let exited = $state(false);
|
||||
let error = $state("");
|
||||
|
||||
/** xterm needs concrete colors, so the theme tokens are resolved once here. */
|
||||
function readTheme() {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const read = (name: string, fallback: string) => styles.getPropertyValue(name).trim() || fallback;
|
||||
const foreground = read("--color-ink", "#e6e9f0");
|
||||
return {
|
||||
background: read("--color-surface-solid", "#12141a"),
|
||||
foreground,
|
||||
cursor: read("--color-primary", "#4f8cff"),
|
||||
cursorAccent: read("--color-surface-solid", "#12141a"),
|
||||
selectionBackground: read("--color-selection", "rgba(79, 140, 255, 0.32)"),
|
||||
red: read("--code-delete-strong", "#e86060"),
|
||||
green: read("--code-add-strong", "#4eca76"),
|
||||
brightRed: read("--code-delete-text", "#e86060"),
|
||||
brightGreen: read("--code-add-text", "#5dd88a"),
|
||||
blue: read("--color-primary", "#4f8cff"),
|
||||
brightBlue: read("--color-accent", "#7aa2ff"),
|
||||
};
|
||||
}
|
||||
|
||||
function fit() {
|
||||
if (!fitAddon || !terminal || !active) return;
|
||||
try {
|
||||
fitAddon.fit();
|
||||
void resizeTerminal(sessionId, terminal.cols, terminal.rows).catch(() => {});
|
||||
} catch {
|
||||
// A hidden or zero-sized host throws; the next visible fit corrects it.
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
if (started || !host) return;
|
||||
started = true;
|
||||
|
||||
terminal = new Terminal({
|
||||
fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--font-mono").trim() || "monospace",
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.25,
|
||||
cursorBlink: true,
|
||||
scrollback: 5000,
|
||||
allowProposedApi: true,
|
||||
theme: readTheme(),
|
||||
});
|
||||
fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(host);
|
||||
await tick();
|
||||
try { fitAddon.fit(); } catch { /* see fit() */ }
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (exited) return;
|
||||
void writeTerminal(sessionId, data).catch((cause) => { error = String(cause); });
|
||||
});
|
||||
terminal.onResize(({ cols, rows }) => {
|
||||
if (exited) return;
|
||||
void resizeTerminal(sessionId, cols, rows).catch(() => {});
|
||||
});
|
||||
|
||||
unlisteners.push(await listen<{ id: string; data: string }>("terminal:data", (event) => {
|
||||
if (event.payload.id !== sessionId) return;
|
||||
terminal?.write(event.payload.data);
|
||||
}));
|
||||
unlisteners.push(await listen<{ id: string; message: string }>("terminal:exit", (event) => {
|
||||
if (event.payload.id !== sessionId) return;
|
||||
exited = true;
|
||||
if (event.payload.message) error = event.payload.message;
|
||||
}));
|
||||
|
||||
try {
|
||||
await openTerminal(sessionId, repoPath, terminal.cols, terminal.rows);
|
||||
terminal.focus();
|
||||
} catch (cause) {
|
||||
error = String(cause);
|
||||
exited = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void start();
|
||||
if (host) {
|
||||
observer = new ResizeObserver(() => fit());
|
||||
observer.observe(host);
|
||||
}
|
||||
return () => {};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer?.disconnect();
|
||||
for (const unlisten of unlisteners) unlisten();
|
||||
unlisteners = [];
|
||||
terminal?.dispose();
|
||||
terminal = null;
|
||||
void closeTerminal(sessionId).catch(() => {});
|
||||
});
|
||||
|
||||
// Becoming visible again needs a fresh measurement: xterm cannot size itself
|
||||
// while its host is display:none.
|
||||
$effect(() => {
|
||||
if (!active) return;
|
||||
void tick().then(() => {
|
||||
fit();
|
||||
terminal?.focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="terminal-surface" class:hidden={!active} aria-hidden={!active}>
|
||||
<div bind:this={host} class="terminal-host"></div>
|
||||
{#if error}
|
||||
<p class="terminal-error" role="status">{error}</p>
|
||||
{:else if exited}
|
||||
<p class="terminal-error" role="status">{t("terminal.exited")}</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -777,3 +777,22 @@ export function getIntegrationIssueLabels(provider: GitIntegrationProvider, base
|
||||
export function setIntegrationIssueLabels(provider: GitIntegrationProvider, baseUrl: string, username: string, token: string, repository: string, number: number, labels: import("./types").IntegrationLabel[], expected: string[] | null = null): Promise<import("./types").IntegrationLabel[]> {
|
||||
return invoke("set_integration_issue_labels", { provider, baseUrl, username, token, repository, number, labels, expected });
|
||||
}
|
||||
|
||||
// ── Embedded terminal ────────────────────────────────────────────────────────
|
||||
// Session ids are owned by the frontend: one per repository tab.
|
||||
|
||||
export function openTerminal(id: string, cwd: string, cols: number, rows: number): Promise<void> {
|
||||
return invoke<void>("terminal_open", { id, cwd, cols, rows });
|
||||
}
|
||||
|
||||
export function writeTerminal(id: string, data: string): Promise<void> {
|
||||
return invoke<void>("terminal_write", { id, data });
|
||||
}
|
||||
|
||||
export function resizeTerminal(id: string, cols: number, rows: number): Promise<void> {
|
||||
return invoke<void>("terminal_resize", { id, cols, rows });
|
||||
}
|
||||
|
||||
export function closeTerminal(id: string): Promise<void> {
|
||||
return invoke<void>("terminal_close", { id });
|
||||
}
|
||||
|
||||
@@ -185,6 +185,15 @@ export const messages = {
|
||||
"stashes.drop": { en: "Drop", de: "Löschen" },
|
||||
|
||||
// ── Status panel ───────────────────────────────────────────────────────────
|
||||
// ── Embedded terminal ──────────────────────────────────────────────────────
|
||||
"terminal.title": { en: "Terminal", de: "Terminal" },
|
||||
"terminal.show": { en: "Show terminal", de: "Terminal anzeigen" },
|
||||
"terminal.hide": { en: "Hide terminal", de: "Terminal ausblenden" },
|
||||
"terminal.close": { en: "Close terminal", de: "Terminal schließen" },
|
||||
"terminal.resize": { en: "Resize terminal", de: "Terminalhöhe ändern" },
|
||||
"terminal.exited": { en: "The shell has ended. Close and reopen the terminal to start a new one.", de: "Die Shell wurde beendet. Terminal schließen und erneut öffnen startet eine neue." },
|
||||
"terminal.hint": { en: "Runs in the repository directory", de: "Läuft im Repository-Verzeichnis" },
|
||||
|
||||
"status.panelLabel": { en: "Working tree status", de: "Status des Arbeitsverzeichnisses" },
|
||||
"status.eyebrow": { en: "Workspace", de: "Arbeitsbereich" },
|
||||
"status.title": { en: "Changes", de: "Änderungen" },
|
||||
|
||||
Reference in New Issue
Block a user