Draw the window's own frame: no menu bar, no title bar
The File/Edit/View/Window menu is a browser's menu, and it brought accelerators that fought the app's own shortcuts. The OS title bar is gone too, with minimize/maximize/close drawn in the header beside the language toggle. Two things a frameless window has to be told explicitly, both of which fail as a window that cannot be moved rather than as an error: the header has to declare itself a drag region, and interactive children have to opt out of it. Maximise state is read from the window rather than tracked from the last button press, because the window manager can maximise on its own — a double-click on the drag region, a shortcut, or snapping to an edge.
This commit is contained in:
@@ -343,6 +343,21 @@ Bytes cross as `Uint8Array`, never as a string. TCP splits UTF-8 characters
|
|||||||
across chunks routinely, and decoding either half alone corrupts them; the bug
|
across chunks routinely, and decoding either half alone corrupts them; the bug
|
||||||
appears only with CJK or emoji output, which makes it easy to miss.
|
appears only with CJK or emoji output, which makes it easy to miss.
|
||||||
|
|
||||||
|
### The window frame
|
||||||
|
|
||||||
|
There is no OS title bar and no application menu: the app draws its own
|
||||||
|
controls in the header and nothing else. Two consequences that are easy to miss:
|
||||||
|
|
||||||
|
- **The header is the only drag region.** A window manager will not move a
|
||||||
|
window whose client area never claims `-webkit-app-region: drag`, and the
|
||||||
|
symptom is a window that simply cannot be moved — not an error.
|
||||||
|
- **Interactive children must opt out** with `no-drag`, or they become
|
||||||
|
undraggable and, on some platforms, unclickable.
|
||||||
|
|
||||||
|
Maximise state is read from the window rather than tracked from the last button
|
||||||
|
press, because the window manager can maximise on its own — double-clicking the
|
||||||
|
drag region, a keyboard shortcut, or snapping to a screen edge.
|
||||||
|
|
||||||
### Terminal font
|
### Terminal font
|
||||||
|
|
||||||
The terminal uses **the desktop's own monospace font**, read from
|
The terminal uses **the desktop's own monospace font**, read from
|
||||||
|
|||||||
@@ -241,6 +241,19 @@ preload contextBridge,窄白名单 —— 这个文件**就是**
|
|||||||
|
|
||||||
字节以 `Uint8Array` 跨界,绝不用字符串。TCP 经常把 UTF-8 字符切断在 chunk 之间,单独解码任一半都会损坏它;这个 bug 只在中文或 emoji 输出时出现,因此极易漏掉。
|
字节以 `Uint8Array` 跨界,绝不用字符串。TCP 经常把 UTF-8 字符切断在 chunk 之间,单独解码任一半都会损坏它;这个 bug 只在中文或 emoji 输出时出现,因此极易漏掉。
|
||||||
|
|
||||||
|
### 窗口边框
|
||||||
|
|
||||||
|
没有系统标题栏,也没有应用菜单:应用在头部自绘窗口按钮,除此之外什么都没有。
|
||||||
|
两个容易漏掉的连带后果:
|
||||||
|
|
||||||
|
- **头部是唯一的拖动区域。** 如果客户区从不声明 `-webkit-app-region: drag`,
|
||||||
|
窗口管理器就不会移动它——症状是窗口**完全拖不动**,而不是报错。
|
||||||
|
- **可交互的子元素必须用 `no-drag` 显式退出**,否则它们既拖不动、在某些平台上
|
||||||
|
也点不动。
|
||||||
|
|
||||||
|
最大化状态是从窗口**读取**的,而不是根据上一次按钮点击推断的——因为窗口管理器
|
||||||
|
可以自己最大化:双击拖动区、键盘快捷键、或贴边吸附。
|
||||||
|
|
||||||
### 终端字体
|
### 终端字体
|
||||||
|
|
||||||
终端使用**桌面自己的等宽字体**——启动时从 `org.gnome.desktop.interface
|
终端使用**桌面自己的等宽字体**——启动时从 `org.gnome.desktop.interface
|
||||||
|
|||||||
+20
-1
@@ -1,5 +1,6 @@
|
|||||||
import { app, BrowserWindow, session, shell } from 'electron'
|
import { app, BrowserWindow, Menu, session, shell } from 'electron'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
|
import { CH } from '@shared/ipc-contract'
|
||||||
import { createAppContext, type AppContext } from './context'
|
import { createAppContext, type AppContext } from './context'
|
||||||
import { registerIpcHandlers } from './ipc/registry'
|
import { registerIpcHandlers } from './ipc/registry'
|
||||||
|
|
||||||
@@ -42,6 +43,10 @@ function createWindow(): BrowserWindow {
|
|||||||
show: false,
|
show: false,
|
||||||
backgroundColor: '#14161a',
|
backgroundColor: '#14161a',
|
||||||
title: 'NexusTerm',
|
title: 'NexusTerm',
|
||||||
|
// No OS title bar. The app draws its own controls in the header, so the
|
||||||
|
// header also has to declare itself a drag region in CSS — without that
|
||||||
|
// the window cannot be moved at all.
|
||||||
|
frame: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
// These three are the security boundary. The renderer runs untrusted
|
// These three are the security boundary. The renderer runs untrusted
|
||||||
@@ -57,6 +62,16 @@ function createWindow(): BrowserWindow {
|
|||||||
// Avoid the white flash before React has painted.
|
// Avoid the white flash before React has painted.
|
||||||
win.once('ready-to-show', () => win.show())
|
win.once('ready-to-show', () => win.show())
|
||||||
|
|
||||||
|
// The maximise button has to render differently when the window is already
|
||||||
|
// maximised, and the window can also be maximised by the window manager —
|
||||||
|
// double-clicking the drag region, a keyboard shortcut, snapping — so the
|
||||||
|
// state is pushed rather than inferred from the last button press.
|
||||||
|
const pushMaximized = (maximized: boolean): void => {
|
||||||
|
if (!win.isDestroyed()) win.webContents.send(CH.WINDOW_STATE, { maximized })
|
||||||
|
}
|
||||||
|
win.on('maximize', () => pushMaximized(true))
|
||||||
|
win.on('unmaximize', () => pushMaximized(false))
|
||||||
|
|
||||||
// Anything that tries to open a new window or navigate away goes to the
|
// Anything that tries to open a new window or navigate away goes to the
|
||||||
// system browser instead. A terminal emulator has no business hosting
|
// system browser instead. A terminal emulator has no business hosting
|
||||||
// arbitrary web content in-process.
|
// arbitrary web content in-process.
|
||||||
@@ -114,6 +129,10 @@ if (!gotTheLock) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
void app.whenReady().then(() => {
|
void app.whenReady().then(() => {
|
||||||
|
// File/Edit/View/Window is a browser's menu, not a terminal's. It also
|
||||||
|
// brought its own accelerators that fought with the app's own shortcuts.
|
||||||
|
Menu.setApplicationMenu(null)
|
||||||
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
callback({
|
callback({
|
||||||
responseHeaders: {
|
responseHeaders: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { app, ipcMain } from 'electron'
|
import { app, BrowserWindow, ipcMain, type IpcMainInvokeEvent } from 'electron'
|
||||||
import { CH } from '@shared/ipc-contract'
|
import { CH } from '@shared/ipc-contract'
|
||||||
import {
|
import {
|
||||||
appInfoSchema,
|
appInfoSchema,
|
||||||
@@ -137,4 +137,26 @@ export function registerIpcHandlers(context: AppContext): void {
|
|||||||
const { hostId, kind } = credentialRefInputSchema.parse(raw)
|
const { hostId, kind } = credentialRefInputSchema.parse(raw)
|
||||||
return hasCredential(db, hostId, kind)
|
return hasCredential(db, hostId, kind)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- window
|
||||||
|
//
|
||||||
|
// The window is frameless, so these stand in for the title bar. Each one
|
||||||
|
// resolves the window from the sender rather than holding a reference, so a
|
||||||
|
// second window (if one ever exists) cannot be closed by the first.
|
||||||
|
|
||||||
|
const windowOf = (event: IpcMainInvokeEvent): BrowserWindow | null =>
|
||||||
|
BrowserWindow.fromWebContents(event.sender)
|
||||||
|
|
||||||
|
ipcMain.handle(CH.WINDOW_MINIMIZE, (event) => windowOf(event)?.minimize())
|
||||||
|
|
||||||
|
ipcMain.handle(CH.WINDOW_TOGGLE_MAXIMIZE, (event) => {
|
||||||
|
const win = windowOf(event)
|
||||||
|
if (!win) return
|
||||||
|
if (win.isMaximized()) win.unmaximize()
|
||||||
|
else win.maximize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(CH.WINDOW_CLOSE, (event) => windowOf(event)?.close())
|
||||||
|
|
||||||
|
ipcMain.handle(CH.WINDOW_IS_MAXIMIZED, (event) => windowOf(event)?.isMaximized() ?? false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
import { CH } from '@shared/ipc-contract'
|
import { CH } from '@shared/ipc-contract'
|
||||||
import { SESSION_PORT_CHANNEL, type PortHandshake } from '@shared/session-protocol'
|
import { SESSION_PORT_CHANNEL, type PortHandshake } from '@shared/session-protocol'
|
||||||
|
import type { WindowState } from '@shared/ipc-schemas'
|
||||||
import type { NexusTermApi } from '@shared/api'
|
import type { NexusTermApi } from '@shared/api'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +45,24 @@ const api: NexusTermApi = {
|
|||||||
remove: (input) => ipcRenderer.invoke(CH.CREDENTIAL_DELETE, input),
|
remove: (input) => ipcRenderer.invoke(CH.CREDENTIAL_DELETE, input),
|
||||||
has: (hostId, kind) => ipcRenderer.invoke(CH.CREDENTIAL_HAS, { hostId, kind }),
|
has: (hostId, kind) => ipcRenderer.invoke(CH.CREDENTIAL_HAS, { hostId, kind }),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
window: {
|
||||||
|
minimize: () => ipcRenderer.invoke(CH.WINDOW_MINIMIZE),
|
||||||
|
toggleMaximize: () => ipcRenderer.invoke(CH.WINDOW_TOGGLE_MAXIMIZE),
|
||||||
|
close: () => ipcRenderer.invoke(CH.WINDOW_CLOSE),
|
||||||
|
isMaximized: () => ipcRenderer.invoke(CH.WINDOW_IS_MAXIMIZED),
|
||||||
|
|
||||||
|
onMaximizedChange: (listener) => {
|
||||||
|
const handler = (_event: unknown, state: WindowState): void => listener(state.maximized)
|
||||||
|
ipcRenderer.on(CH.WINDOW_STATE, handler)
|
||||||
|
// Returned rather than left implicit: React's development-mode effects
|
||||||
|
// run twice, and without a way to unsubscribe each of those would add
|
||||||
|
// another listener for the lifetime of the window.
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener(CH.WINDOW_STATE, handler)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import TabBar from './components/Tabs/TabBar'
|
|||||||
import TerminalHost from './components/Terminal/TerminalHost'
|
import TerminalHost from './components/Terminal/TerminalHost'
|
||||||
import HostKeyDialog from './components/Session/HostKeyDialog'
|
import HostKeyDialog from './components/Session/HostKeyDialog'
|
||||||
import PasswordDialog from './components/Session/PasswordDialog'
|
import PasswordDialog from './components/Session/PasswordDialog'
|
||||||
|
import WindowControls from './components/WindowControls'
|
||||||
import { useSessionStore } from './stores/sessionStore'
|
import { useSessionStore } from './stores/sessionStore'
|
||||||
import { useSettingsStore } from './stores/settingsStore'
|
import { useSettingsStore } from './stores/settingsStore'
|
||||||
import { SUPPORTED_LOCALES, type Locale } from './i18n'
|
import { SUPPORTED_LOCALES, type Locale } from './i18n'
|
||||||
@@ -54,7 +55,16 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<header className="header">
|
<header
|
||||||
|
className="header"
|
||||||
|
// Double-clicking a title bar maximises the window everywhere else;
|
||||||
|
// a frameless one has to be told to. Buttons opt out, or double-clicking
|
||||||
|
// close would maximise and then close.
|
||||||
|
onDoubleClick={(e) => {
|
||||||
|
if ((e.target as HTMLElement).closest('button')) return
|
||||||
|
void window.nexusterm.window.toggleMaximize()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span className="header__mark">❯_</span>
|
<span className="header__mark">❯_</span>
|
||||||
<span className="header__name">{t('app.name')}</span>
|
<span className="header__name">{t('app.name')}</span>
|
||||||
<span className="header__tagline">{t('app.tagline')}</span>
|
<span className="header__tagline">{t('app.tagline')}</span>
|
||||||
@@ -69,6 +79,8 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
<Languages size={14} />
|
<Languages size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<WindowControls />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="body">
|
<div className="body">
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Copy, Minus, Square, X } from 'lucide-react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimise / maximise / close, drawn by the app.
|
||||||
|
*
|
||||||
|
* The window is frameless, so the title bar that normally carries these is
|
||||||
|
* gone. Two consequences worth knowing:
|
||||||
|
*
|
||||||
|
* - The header has to declare itself a drag region (see `.header` in the
|
||||||
|
* stylesheet), or there is nothing left to drag the window by.
|
||||||
|
* - Maximise state is *read* rather than tracked from the last button press,
|
||||||
|
* because the window manager can maximise on its own — double-clicking the
|
||||||
|
* drag region, a keyboard shortcut, snapping to an edge.
|
||||||
|
*/
|
||||||
|
export default function WindowControls() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [maximized, setMaximized] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
void window.nexusterm.window.isMaximized().then((value) => {
|
||||||
|
if (!cancelled) setMaximized(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsubscribe = window.nexusterm.window.onMaximizedChange(setMaximized)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wincontrols">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="wincontrols__button"
|
||||||
|
title={t('window.minimize')}
|
||||||
|
aria-label={t('window.minimize')}
|
||||||
|
onClick={() => void window.nexusterm.window.minimize()}
|
||||||
|
>
|
||||||
|
<Minus size={13} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="wincontrols__button"
|
||||||
|
title={maximized ? t('window.restore') : t('window.maximize')}
|
||||||
|
aria-label={maximized ? t('window.restore') : t('window.maximize')}
|
||||||
|
onClick={() => void window.nexusterm.window.toggleMaximize()}
|
||||||
|
>
|
||||||
|
{maximized ? <Copy size={12} /> : <Square size={11} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="wincontrols__button wincontrols__button--close"
|
||||||
|
title={t('window.close')}
|
||||||
|
aria-label={t('window.close')}
|
||||||
|
onClick={() => void window.nexusterm.window.close()}
|
||||||
|
>
|
||||||
|
<X size={13} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -113,5 +113,11 @@
|
|||||||
"reconnecting": "--- connection lost; reconnecting (attempt {{attempt}}, in {{seconds}}s) ---",
|
"reconnecting": "--- connection lost; reconnecting (attempt {{attempt}}, in {{seconds}}s) ---",
|
||||||
"reconnected": "--- reconnected on attempt {{attempt}} ---\n--- note: this is a new shell; the previous session ended and its working directory is not restored ---",
|
"reconnected": "--- reconnected on attempt {{attempt}} ---\n--- note: this is a new shell; the previous session ended and its working directory is not restored ---",
|
||||||
"disconnected": "--- disconnected: {{message}} ---"
|
"disconnected": "--- disconnected: {{message}} ---"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"minimize": "Minimize",
|
||||||
|
"maximize": "Maximize",
|
||||||
|
"restore": "Restore",
|
||||||
|
"close": "Close window"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,5 +113,11 @@
|
|||||||
"reconnecting": "--- 连接断开,正在重连(第 {{attempt}} 次,{{seconds}} 秒后)---",
|
"reconnecting": "--- 连接断开,正在重连(第 {{attempt}} 次,{{seconds}} 秒后)---",
|
||||||
"reconnected": "--- 已重连(第 {{attempt}} 次尝试成功)---\n--- 注意:这是一个新的 shell,上一个会话已结束,工作目录不会恢复 ---",
|
"reconnected": "--- 已重连(第 {{attempt}} 次尝试成功)---\n--- 注意:这是一个新的 shell,上一个会话已结束,工作目录不会恢复 ---",
|
||||||
"disconnected": "--- 连接已断开:{{message}} ---"
|
"disconnected": "--- 连接已断开:{{message}} ---"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"minimize": "最小化",
|
||||||
|
"maximize": "最大化",
|
||||||
|
"restore": "还原",
|
||||||
|
"close": "关闭窗口"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,22 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0 12px;
|
/* No left padding on the right edge: the window controls run to the corner,
|
||||||
|
as they do on every other window. */
|
||||||
|
padding: 0 0 0 12px;
|
||||||
background: var(--bg-raised);
|
background: var(--bg-raised);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
/* The window is frameless, so this is the only thing left to drag it by.
|
||||||
|
Interactive children opt out below, or they would be undraggable and
|
||||||
|
unclickable. */
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header button,
|
||||||
|
.header input,
|
||||||
|
.header select,
|
||||||
|
.header .banner {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header__mark {
|
.header__mark {
|
||||||
@@ -97,6 +110,38 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- window controls ---------- */
|
||||||
|
|
||||||
|
.wincontrols {
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
margin-left: 2px;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wincontrols__button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wincontrols__button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Red, as every window's close button is — the one control that should need
|
||||||
|
no reading to identify. */
|
||||||
|
.wincontrols__button--close:hover {
|
||||||
|
background: #c42b1c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- security banner ---------- */
|
/* ---------- security banner ---------- */
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
|
|||||||
@@ -82,4 +82,20 @@ export interface NexusTermApi {
|
|||||||
remove(input: CredentialDeleteInput): Promise<void>
|
remove(input: CredentialDeleteInput): Promise<void>
|
||||||
has(hostId: number, kind: CredentialKind): Promise<boolean>
|
has(hostId: number, kind: CredentialKind): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window controls for a frameless window.
|
||||||
|
*
|
||||||
|
* The title bar that normally carries these is gone, so the app draws its
|
||||||
|
* own — which also means the drag region has to be declared in CSS, since
|
||||||
|
* there is no longer a title bar to drag.
|
||||||
|
*/
|
||||||
|
window: {
|
||||||
|
minimize(): Promise<void>
|
||||||
|
toggleMaximize(): Promise<void>
|
||||||
|
close(): Promise<void>
|
||||||
|
isMaximized(): Promise<boolean>
|
||||||
|
/** Returns a function that removes the listener. */
|
||||||
|
onMaximizedChange(listener: (maximized: boolean) => void): () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ export const CH = {
|
|||||||
CREDENTIAL_SET: 'credential:set',
|
CREDENTIAL_SET: 'credential:set',
|
||||||
CREDENTIAL_DELETE: 'credential:delete',
|
CREDENTIAL_DELETE: 'credential:delete',
|
||||||
CREDENTIAL_HAS: 'credential:has',
|
CREDENTIAL_HAS: 'credential:has',
|
||||||
|
|
||||||
|
// The window is frameless, so these replace what the title bar used to do.
|
||||||
|
WINDOW_MINIMIZE: 'window:minimize',
|
||||||
|
WINDOW_TOGGLE_MAXIMIZE: 'window:toggleMaximize',
|
||||||
|
WINDOW_CLOSE: 'window:close',
|
||||||
|
WINDOW_IS_MAXIMIZED: 'window:isMaximized',
|
||||||
|
/** Pushed from main when the window is maximised or restored. */
|
||||||
|
WINDOW_STATE: 'window:state',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Channel = (typeof CH)[keyof typeof CH]
|
export type Channel = (typeof CH)[keyof typeof CH]
|
||||||
|
|||||||
@@ -187,6 +187,10 @@ export const sessionCloseInputSchema = z.object({
|
|||||||
sessionId: z.string().min(1).max(80),
|
sessionId: z.string().min(1).max(80),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Pushed from main when the window is maximised or restored. */
|
||||||
|
export const windowStateSchema = z.object({ maximized: z.boolean() })
|
||||||
|
export type WindowState = z.infer<typeof windowStateSchema>
|
||||||
|
|
||||||
/** A local shell either starts or it does not; there is no prompt to answer. */
|
/** A local shell either starts or it does not; there is no prompt to answer. */
|
||||||
export const localSessionResultSchema = z.discriminatedUnion('ok', [
|
export const localSessionResultSchema = z.discriminatedUnion('ok', [
|
||||||
z.object({ ok: z.literal(true), sessionId: z.string() }),
|
z.object({ ok: z.literal(true), sessionId: z.string() }),
|
||||||
|
|||||||
Reference in New Issue
Block a user