mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-21 14:37:16 +00:00
Tab Indicators, Confirm on Quit, etc (#2811)
* Adds a Confirm on Quit dialog (and new config to disable it) * New MacOS keybinding for Cmd:ArrowLeft/Cmd:ArrowRight to send Ctrl-A and Ctrl-E respectively * Fix Ctrl-V regression on windows to allow config setting to override * Remove questionnaire in bug template * Full featured tab indicators -- icon, color, priority, clear features. Can be manipulated by new `wsh tabindicator` command * Hook up BEL to new tab indicator system. BEL can now play a sound and/or set an indicator (controlled by two new config options)
This commit is contained in:
parent
9d7a457c55
commit
73bb5beb3b
34 changed files with 611 additions and 27 deletions
11
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
|
@ -84,14 +84,3 @@ body:
|
|||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Questionnaire
|
||||
description: "If you feel up to the challenge, please check one of the boxes below:"
|
||||
options:
|
||||
- label: I'm interested in fixing this myself but don't know where to start
|
||||
required: false
|
||||
- label: I would like to fix and I have a solution
|
||||
required: false
|
||||
- label: I don't have time to fix this right now, but maybe later
|
||||
required: false
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ tasks:
|
|||
WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central"
|
||||
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
|
||||
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev"
|
||||
WAVETERM_NOCONFIRMQUIT: "1"
|
||||
|
||||
electron:start:
|
||||
desc: Run the Electron application directly.
|
||||
|
|
@ -55,6 +56,7 @@ tasks:
|
|||
WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central"
|
||||
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
|
||||
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/"
|
||||
WAVETERM_NOCONFIRMQUIT: "1"
|
||||
|
||||
electron:winquickdev:
|
||||
desc: Run the Electron application via the Vite dev server (quick dev - Windows amd64 only, no generate, no wsh).
|
||||
|
|
|
|||
|
|
@ -568,6 +568,7 @@ func main() {
|
|||
go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher()
|
||||
blocklogger.InitBlockLogger()
|
||||
jobcontroller.InitJobController()
|
||||
wcore.InitTabIndicatorStore()
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("GetSystemSummary", recover())
|
||||
|
|
|
|||
100
cmd/wsh/cmd/wshcmd-tabindicator.go
Normal file
100
cmd/wsh/cmd/wshcmd-tabindicator.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var tabIndicatorCmd = &cobra.Command{
|
||||
Use: "tabindicator [icon]",
|
||||
Short: "set or clear a tab indicator",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: tabIndicatorRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var (
|
||||
tabIndicatorTabId string
|
||||
tabIndicatorColor string
|
||||
tabIndicatorPriority float64
|
||||
tabIndicatorClear bool
|
||||
tabIndicatorPersistent bool
|
||||
tabIndicatorBeep bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(tabIndicatorCmd)
|
||||
tabIndicatorCmd.Flags().StringVar(&tabIndicatorTabId, "tabid", "", "tab id (defaults to WAVETERM_TABID)")
|
||||
tabIndicatorCmd.Flags().StringVar(&tabIndicatorColor, "color", "", "indicator color")
|
||||
tabIndicatorCmd.Flags().Float64Var(&tabIndicatorPriority, "priority", 0, "indicator priority")
|
||||
tabIndicatorCmd.Flags().BoolVar(&tabIndicatorClear, "clear", false, "clear the indicator")
|
||||
tabIndicatorCmd.Flags().BoolVar(&tabIndicatorPersistent, "persistent", false, "make indicator persistent (don't clear on focus)")
|
||||
tabIndicatorCmd.Flags().BoolVar(&tabIndicatorBeep, "beep", false, "play system bell sound")
|
||||
}
|
||||
|
||||
func tabIndicatorRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("tabindicator", rtnErr == nil)
|
||||
}()
|
||||
|
||||
tabId := tabIndicatorTabId
|
||||
if tabId == "" {
|
||||
tabId = os.Getenv("WAVETERM_TABID")
|
||||
}
|
||||
if tabId == "" {
|
||||
return fmt.Errorf("no tab id specified (use --tabid or set WAVETERM_TABID)")
|
||||
}
|
||||
|
||||
var indicator *wshrpc.TabIndicator
|
||||
if !tabIndicatorClear {
|
||||
icon := "bell"
|
||||
if len(args) > 0 {
|
||||
icon = args[0]
|
||||
}
|
||||
indicator = &wshrpc.TabIndicator{
|
||||
Icon: icon,
|
||||
Color: tabIndicatorColor,
|
||||
Priority: tabIndicatorPriority,
|
||||
ClearOnFocus: !tabIndicatorPersistent,
|
||||
}
|
||||
}
|
||||
|
||||
eventData := wshrpc.TabIndicatorEventData{
|
||||
TabId: tabId,
|
||||
Indicator: indicator,
|
||||
}
|
||||
|
||||
event := wps.WaveEvent{
|
||||
Event: wps.Event_TabIndicator,
|
||||
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, tabId).String()},
|
||||
Data: eventData,
|
||||
}
|
||||
|
||||
err := wshclient.EventPublishCommand(RpcClient, event, &wshrpc.RpcOpts{NoResponse: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("publishing tab indicator event: %v", err)
|
||||
}
|
||||
|
||||
if tabIndicatorBeep {
|
||||
err = wshclient.ElectronSystemBellCommand(RpcClient, &wshrpc.RpcOpts{Route: "electron"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("playing system bell: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tabIndicatorClear {
|
||||
fmt.Printf("tab indicator cleared\n")
|
||||
} else {
|
||||
fmt.Printf("tab indicator set\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ wsh editconfig
|
|||
| app:defaultnewblock | string | Sets the default new block (Cmd:n, Cmd:d). "term" for terminal block, "launcher" for launcher block (default = "term") |
|
||||
| app:showoverlayblocknums | bool | Set to false to disable the Ctrl+Shift block number overlay that appears when holding Ctrl+Shift (defaults to true) |
|
||||
| app:ctrlvpaste | bool | On Windows/Linux, when null (default) uses Control+V on Windows only. Set to true to force Control+V on all non-macOS platforms, false to disable the accelerator. macOS always uses Command+V regardless of this setting |
|
||||
| app:confirmquit | bool | Set to false to disable the quit confirmation dialog when closing Wave Terminal (defaults to true, requires app restart) |
|
||||
| ai:preset | string | the default AI preset to use |
|
||||
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
|
||||
| ai:apitoken | string | your AI api token |
|
||||
|
|
@ -62,6 +63,8 @@ wsh editconfig
|
|||
| term:allowbracketedpaste | bool | allow bracketed paste mode in terminal (default false) |
|
||||
| term:shiftenternewline | bool | when enabled, Shift+Enter sends escape sequence + newline (\u001b\n) instead of carriage return, useful for claude code and similar AI coding tools (default false) |
|
||||
| term:macoptionismeta | bool | on macOS, treat the Option key as Meta key for terminal keybindings (default false) |
|
||||
| term:bellsound | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
|
||||
| term:bellindicator | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default true) |
|
||||
| editor:minimapenabled | bool | set to false to disable editor minimap |
|
||||
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
|
||||
| editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) |
|
||||
|
|
@ -108,6 +111,7 @@ For reference, this is the current default configuration (v0.11.5):
|
|||
"ai:maxtokens": 4000,
|
||||
"ai:timeoutms": 60000,
|
||||
"app:defaultnewblock": "term",
|
||||
"app:confirmquit": true,
|
||||
"autoupdate:enabled": true,
|
||||
"autoupdate:installonquit": true,
|
||||
"autoupdate:intervalms": 3600000,
|
||||
|
|
@ -126,7 +130,11 @@ For reference, this is the current default configuration (v0.11.5):
|
|||
"window:confirmclose": true,
|
||||
"window:savelastwindow": true,
|
||||
"telemetry:enabled": true,
|
||||
"term:copyonselect": true
|
||||
"term:bellsound": false,
|
||||
"term:bellindicator": true,
|
||||
"term:copyonselect": true,
|
||||
"waveai:showcloudmodes": true,
|
||||
"waveai:defaultmode": "waveai@balanced"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
|
|||
| <Kbd k="Shift:End"/> | Scroll to bottom |
|
||||
| <Kbd k="Cmd:Home" windows="N/A" linux="N/A"/> | Scroll to top (macOS only) |
|
||||
| <Kbd k="Cmd:End" windows="N/A" linux="N/A"/> | Scroll to bottom (macOS only)|
|
||||
| <Kbd k="Cmd:ArrowLeft" windows="N/A" linux="N/A"/> | Move to beginning of line (macOS only) |
|
||||
| <Kbd k="Cmd:ArrowRight" windows="N/A" linux="N/A"/> | Move to end of line (macOS only) |
|
||||
| <Kbd k="Shift:PageUp"/> | Scroll up one page |
|
||||
| <Kbd k="Shift:PageDown"/>| Scroll down one page |
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ let globalIsQuitting = false;
|
|||
let globalIsStarting = true;
|
||||
let globalIsRelaunching = false;
|
||||
let forceQuit = false;
|
||||
let userConfirmedQuit = false;
|
||||
let termCommandsRun = 0;
|
||||
|
||||
export function setWasActive(val: boolean) {
|
||||
|
|
@ -54,6 +55,14 @@ export function getForceQuit(): boolean {
|
|||
return forceQuit;
|
||||
}
|
||||
|
||||
export function setUserConfirmedQuit(val: boolean) {
|
||||
userConfirmedQuit = val;
|
||||
}
|
||||
|
||||
export function getUserConfirmedQuit(): boolean {
|
||||
return userConfirmedQuit;
|
||||
}
|
||||
|
||||
export function incrementTermCommandsRun() {
|
||||
termCommandsRun++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ import {
|
|||
} from "./emain-util";
|
||||
import { ElectronWshClient } from "./emain-wsh";
|
||||
|
||||
function handleWindowsMenuAccelerators(waveEvent: WaveKeyboardEvent, tabView: WaveTabView): boolean {
|
||||
function handleWindowsMenuAccelerators(
|
||||
waveEvent: WaveKeyboardEvent,
|
||||
tabView: WaveTabView,
|
||||
fullConfig: FullConfigType
|
||||
): boolean {
|
||||
const waveWindow = getWaveWindowById(tabView.waveWindowId);
|
||||
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:Shift:n")) {
|
||||
|
|
@ -34,6 +38,11 @@ function handleWindowsMenuAccelerators(waveEvent: WaveKeyboardEvent, tabView: Wa
|
|||
}
|
||||
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:v")) {
|
||||
const ctrlVPaste = fullConfig?.settings?.["app:ctrlvpaste"];
|
||||
const shouldPaste = ctrlVPaste ?? true;
|
||||
if (!shouldPaste) {
|
||||
return false;
|
||||
}
|
||||
tabView.webContents.paste();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -324,7 +333,7 @@ export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: stri
|
|||
}
|
||||
|
||||
if (unamePlatform === "win32" && input.type == "keyDown") {
|
||||
if (handleWindowsMenuAccelerators(waveEvent, tabView)) {
|
||||
if (handleWindowsMenuAccelerators(waveEvent, tabView, fullConfig)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as child_process from "node:child_process";
|
|||
import * as readline from "readline";
|
||||
import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/util/endpoints";
|
||||
import { AuthKey, WaveAuthKeyEnv } from "./authkey";
|
||||
import { setForceQuit } from "./emain-activity";
|
||||
import { setForceQuit, setUserConfirmedQuit } from "./emain-activity";
|
||||
import {
|
||||
getElectronAppResourcesPath,
|
||||
getElectronAppUnpackedBasePath,
|
||||
|
|
@ -112,6 +112,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
|
|||
);
|
||||
if (startParams == null) {
|
||||
console.log("error parsing WAVESRV-ESTART line", line);
|
||||
setUserConfirmedQuit(true);
|
||||
electron.app.quit();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { WindowService } from "@/app/store/services";
|
||||
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { Notification, net, safeStorage } from "electron";
|
||||
import { Notification, net, safeStorage, shell } from "electron";
|
||||
import { getResolvedUpdateChannel } from "emain/updater";
|
||||
import { unamePlatform } from "./emain-platform";
|
||||
import { getWebContentsByBlockId, webGetSelector } from "./emain-web";
|
||||
|
|
@ -106,6 +106,10 @@ export class ElectronWshClientType extends WshClient {
|
|||
return net.isOnline();
|
||||
}
|
||||
|
||||
async handle_electronsystembell(rh: RpcResponseHelper): Promise<void> {
|
||||
shell.beep();
|
||||
}
|
||||
|
||||
// async handle_workspaceupdate(rh: RpcResponseHelper) {
|
||||
// console.log("workspaceupdate");
|
||||
// fireAndForget(async () => {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ import {
|
|||
getAndClearTermCommandsRun,
|
||||
getForceQuit,
|
||||
getGlobalIsRelaunching,
|
||||
getUserConfirmedQuit,
|
||||
setForceQuit,
|
||||
setGlobalIsQuitting,
|
||||
setGlobalIsStarting,
|
||||
setUserConfirmedQuit,
|
||||
setWasActive,
|
||||
setWasInFg,
|
||||
} from "./emain-activity";
|
||||
|
|
@ -53,6 +55,8 @@ import { configureAutoUpdater, updater } from "./updater";
|
|||
|
||||
const electronApp = electron.app;
|
||||
|
||||
let confirmQuit = true;
|
||||
|
||||
const waveDataDir = getWaveDataDir();
|
||||
const waveConfigDir = getWaveConfigDir();
|
||||
|
||||
|
|
@ -238,10 +242,37 @@ electronApp.on("window-all-closed", () => {
|
|||
return;
|
||||
}
|
||||
if (unamePlatform !== "darwin") {
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
}
|
||||
});
|
||||
electronApp.on("before-quit", (e) => {
|
||||
const allWindows = getAllWaveWindows();
|
||||
const allBuilders = getAllBuilderWindows();
|
||||
if (
|
||||
confirmQuit &&
|
||||
!getForceQuit() &&
|
||||
!getUserConfirmedQuit() &&
|
||||
(allWindows.length > 0 || allBuilders.length > 0) &&
|
||||
!getIsWaveSrvDead() &&
|
||||
!process.env.WAVETERM_NOCONFIRMQUIT
|
||||
) {
|
||||
e.preventDefault();
|
||||
const choice = electron.dialog.showMessageBoxSync(null, {
|
||||
type: "question",
|
||||
buttons: ["Cancel", "Quit"],
|
||||
title: "Confirm Quit",
|
||||
message: "Are you sure you want to quit Wave Terminal?",
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
});
|
||||
if (choice === 0) {
|
||||
return;
|
||||
}
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
return;
|
||||
}
|
||||
setGlobalIsQuitting(true);
|
||||
updater?.stop();
|
||||
if (unamePlatform == "win32") {
|
||||
|
|
@ -255,11 +286,9 @@ electronApp.on("before-quit", (e) => {
|
|||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const allWindows = getAllWaveWindows();
|
||||
for (const window of allWindows) {
|
||||
hideWindowWithCatch(window);
|
||||
}
|
||||
const allBuilders = getAllBuilderWindows();
|
||||
for (const builder of allBuilders) {
|
||||
builder.hide();
|
||||
}
|
||||
|
|
@ -277,14 +306,17 @@ electronApp.on("before-quit", (e) => {
|
|||
});
|
||||
process.on("SIGINT", () => {
|
||||
console.log("Caught SIGINT, shutting down");
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
});
|
||||
process.on("SIGHUP", () => {
|
||||
console.log("Caught SIGHUP, shutting down");
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("Caught SIGTERM, shutting down");
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
});
|
||||
let caughtException = false;
|
||||
|
|
@ -304,6 +336,7 @@ process.on("uncaughtException", (error) => {
|
|||
console.log("Uncaught Exception, shutting down: ", error);
|
||||
console.log("Stack Trace:", error.stack);
|
||||
// Optionally, handle cleanup or exit the app
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
});
|
||||
|
||||
|
|
@ -332,6 +365,7 @@ async function appMain() {
|
|||
const instanceLock = electronApp.requestSingleInstanceLock();
|
||||
if (!instanceLock) {
|
||||
console.log("waveterm-app could not get single-instance-lock, shutting down");
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
return;
|
||||
}
|
||||
|
|
@ -356,6 +390,9 @@ async function appMain() {
|
|||
}
|
||||
const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
|
||||
checkIfRunningUnderARM64Translation(fullConfig);
|
||||
if (fullConfig?.settings?.["app:confirmquit"] != null) {
|
||||
confirmQuit = fullConfig.settings["app:confirmquit"];
|
||||
}
|
||||
ensureHotSpareTab(fullConfig);
|
||||
await relaunchBrowserWindows();
|
||||
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
||||
|
|
@ -383,5 +420,6 @@ async function appMain() {
|
|||
|
||||
appMain().catch((e) => {
|
||||
console.log("appMain error", e);
|
||||
setUserConfirmedQuit(true);
|
||||
electronApp.quit();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@ import { GlobalModel } from "@/app/store/global-model";
|
|||
import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model";
|
||||
import { Workspace } from "@/app/workspace/workspace";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import { atoms, createBlock, getSettingsPrefixAtom, globalStore, isDev, removeFlashError } from "@/store/global";
|
||||
import {
|
||||
atoms,
|
||||
clearTabIndicatorFromFocus,
|
||||
createBlock,
|
||||
getSettingsPrefixAtom,
|
||||
getTabIndicatorAtom,
|
||||
globalStore,
|
||||
isDev,
|
||||
removeFlashError,
|
||||
} from "@/store/global";
|
||||
import { appHandleKeyDown, keyboardMouseDownHandler } from "@/store/keymodel";
|
||||
import { getElemAsStr } from "@/util/focusutil";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
|
|
@ -205,6 +214,29 @@ const AppKeyHandlers = () => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const TabIndicatorAutoClearing = () => {
|
||||
const tabId = useAtomValue(atoms.staticTabId);
|
||||
const indicator = useAtomValue(getTabIndicatorAtom(tabId));
|
||||
const documentHasFocus = useAtomValue(atoms.documentHasFocus);
|
||||
|
||||
useEffect(() => {
|
||||
if (!indicator || !documentHasFocus || !indicator.clearonfocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
const currentIndicator = globalStore.get(getTabIndicatorAtom(tabId));
|
||||
if (globalStore.get(atoms.documentHasFocus) && currentIndicator?.clearonfocus) {
|
||||
clearTabIndicatorFromFocus(tabId);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [tabId, indicator, documentHasFocus]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const FlashError = () => {
|
||||
const flashErrors = useAtomValue(atoms.flashErrors);
|
||||
const [hoveredId, setHoveredId] = useState<string>(null);
|
||||
|
|
@ -304,6 +336,7 @@ const AppInner = () => {
|
|||
<AppKeyHandlers />
|
||||
<AppFocusHandler />
|
||||
<AppSettingsUpdater />
|
||||
<TabIndicatorAutoClearing />
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Workspace />
|
||||
</DndProvider>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ let globalPrimaryTabStartup: boolean = false;
|
|||
const blockComponentModelMap = new Map<string, BlockComponentModel>();
|
||||
const Counters = new Map<string, number>();
|
||||
const ConnStatusMapAtom = atom(new Map<string, PrimitiveAtom<ConnStatus>>());
|
||||
const TabIndicatorMap = new Map<string, PrimitiveAtom<TabIndicator>>();
|
||||
const orefAtomCache = new Map<string, Map<string, Atom<any>>>();
|
||||
|
||||
function initGlobal(initOpts: GlobalInitOptions) {
|
||||
|
|
@ -146,6 +147,17 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|||
});
|
||||
}
|
||||
|
||||
const documentHasFocusAtom = atom(true) as PrimitiveAtom<boolean>;
|
||||
if (globalThis.window != null) {
|
||||
globalStore.set(documentHasFocusAtom, document.hasFocus());
|
||||
window.addEventListener("focus", () => {
|
||||
globalStore.set(documentHasFocusAtom, true);
|
||||
});
|
||||
window.addEventListener("blur", () => {
|
||||
globalStore.set(documentHasFocusAtom, false);
|
||||
});
|
||||
}
|
||||
|
||||
const modalOpen = atom(false);
|
||||
const allConnStatusAtom = atom<ConnStatus[]>((get) => {
|
||||
const connStatusMap = get(ConnStatusMapAtom);
|
||||
|
|
@ -174,6 +186,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|||
controlShiftDelayAtom,
|
||||
updaterStatusAtom,
|
||||
prefersReducedMotionAtom,
|
||||
documentHasFocus: documentHasFocusAtom,
|
||||
modalOpen,
|
||||
allConnStatus: allConnStatusAtom,
|
||||
flashErrors: flashErrorsAtom,
|
||||
|
|
@ -235,6 +248,13 @@ function initGlobalWaveEventSubs(initOpts: WaveInitOpts) {
|
|||
const rateLimitInfo: RateLimitInfo = event.data;
|
||||
globalStore.set(atoms.waveAIRateLimitInfoAtom, rateLimitInfo);
|
||||
},
|
||||
},
|
||||
{
|
||||
eventType: "tab:indicator",
|
||||
handler: (event) => {
|
||||
const data: TabIndicatorEventData = event.data;
|
||||
setTabIndicatorInternal(data.tabid, data.indicator);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -688,6 +708,17 @@ async function loadConnStatus() {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadTabIndicators() {
|
||||
const tabIndicators = await RpcApi.GetAllTabIndicatorsCommand(TabRpcClient);
|
||||
if (tabIndicators == null) {
|
||||
return;
|
||||
}
|
||||
for (const [tabId, indicator] of Object.entries(tabIndicators)) {
|
||||
const curAtom = getTabIndicatorAtom(tabId);
|
||||
globalStore.set(curAtom, indicator);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToConnEvents() {
|
||||
waveEventSubscribe({
|
||||
eventType: "connchange",
|
||||
|
|
@ -741,6 +772,76 @@ function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
|
|||
return rtn;
|
||||
}
|
||||
|
||||
function getTabIndicatorAtom(tabId: string): PrimitiveAtom<TabIndicator> {
|
||||
let rtn = TabIndicatorMap.get(tabId);
|
||||
if (rtn == null) {
|
||||
rtn = atom(null) as PrimitiveAtom<TabIndicator>;
|
||||
TabIndicatorMap.set(tabId, rtn);
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
function setTabIndicatorInternal(tabId: string, indicator: TabIndicator) {
|
||||
if (indicator == null) {
|
||||
const indicatorAtom = getTabIndicatorAtom(tabId);
|
||||
globalStore.set(indicatorAtom, null);
|
||||
return;
|
||||
}
|
||||
const indicatorAtom = getTabIndicatorAtom(tabId);
|
||||
const currentIndicator = globalStore.get(indicatorAtom);
|
||||
if (currentIndicator == null) {
|
||||
globalStore.set(indicatorAtom, indicator);
|
||||
return;
|
||||
}
|
||||
if (indicator.priority >= currentIndicator.priority) {
|
||||
if (indicator.clearonfocus && !currentIndicator.clearonfocus) {
|
||||
indicator.persistentindicator = currentIndicator;
|
||||
}
|
||||
globalStore.set(indicatorAtom, indicator);
|
||||
}
|
||||
}
|
||||
|
||||
function setTabIndicator(tabId: string, indicator: TabIndicator) {
|
||||
setTabIndicatorInternal(tabId, indicator);
|
||||
|
||||
const eventData: WaveEvent = {
|
||||
event: "tab:indicator",
|
||||
scopes: [WOS.makeORef("tab", tabId)],
|
||||
data: {
|
||||
tabid: tabId,
|
||||
indicator: indicator,
|
||||
} as TabIndicatorEventData,
|
||||
};
|
||||
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
|
||||
}
|
||||
|
||||
function clearTabIndicatorFromFocus(tabId: string) {
|
||||
const indicatorAtom = getTabIndicatorAtom(tabId);
|
||||
const currentIndicator = globalStore.get(indicatorAtom);
|
||||
if (currentIndicator == null) {
|
||||
return;
|
||||
}
|
||||
const persistentIndicator = currentIndicator.persistentindicator;
|
||||
const eventData: WaveEvent = {
|
||||
event: "tab:indicator",
|
||||
scopes: [WOS.makeORef("tab", tabId)],
|
||||
data: {
|
||||
tabid: tabId,
|
||||
indicator: persistentIndicator ?? null,
|
||||
} as TabIndicatorEventData,
|
||||
};
|
||||
fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData));
|
||||
}
|
||||
|
||||
function clearAllTabIndicators() {
|
||||
for (const [tabId, indicatorAtom] of TabIndicatorMap.entries()) {
|
||||
const indicator = globalStore.get(indicatorAtom);
|
||||
if (indicator != null) {
|
||||
setTabIndicator(tabId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pushFlashError(ferr: FlashErrorType) {
|
||||
if (ferr.expiration == null) {
|
||||
ferr.expiration = Date.now() + 5000;
|
||||
|
|
@ -803,6 +904,8 @@ function recordTEvent(event: string, props?: TEventProps) {
|
|||
|
||||
export {
|
||||
atoms,
|
||||
clearAllTabIndicators,
|
||||
clearTabIndicatorFromFocus,
|
||||
counterInc,
|
||||
countersClear,
|
||||
countersPrint,
|
||||
|
|
@ -823,17 +926,19 @@ export {
|
|||
getOverrideConfigAtom,
|
||||
getSettingsKeyAtom,
|
||||
getSettingsPrefixAtom,
|
||||
getTabIndicatorAtom,
|
||||
getUserName,
|
||||
globalPrimaryTabStartup,
|
||||
globalStore,
|
||||
readAtom,
|
||||
initGlobal,
|
||||
initGlobalWaveEventSubs,
|
||||
isDev,
|
||||
loadConnStatus,
|
||||
loadTabIndicators,
|
||||
openLink,
|
||||
pushFlashError,
|
||||
pushNotification,
|
||||
readAtom,
|
||||
recordTEvent,
|
||||
refocusNode,
|
||||
registerBlockComponentModel,
|
||||
|
|
@ -844,6 +949,7 @@ export {
|
|||
setActiveTab,
|
||||
setNodeFocus,
|
||||
setPlatform,
|
||||
setTabIndicator,
|
||||
subscribeToConnEvents,
|
||||
unregisterBlockComponentModel,
|
||||
useBlockAtom,
|
||||
|
|
|
|||
|
|
@ -187,6 +187,11 @@ class RpcApiType {
|
|||
return client.wshRpcCall("electronencrypt", data, opts);
|
||||
}
|
||||
|
||||
// command "electronsystembell" [call]
|
||||
ElectronSystemBellCommand(client: WshClient, opts?: RpcOpts): Promise<void> {
|
||||
return client.wshRpcCall("electronsystembell", null, opts);
|
||||
}
|
||||
|
||||
// command "eventpublish" [call]
|
||||
EventPublishCommand(client: WshClient, data: WaveEvent, opts?: RpcOpts): Promise<void> {
|
||||
return client.wshRpcCall("eventpublish", data, opts);
|
||||
|
|
@ -302,6 +307,11 @@ class RpcApiType {
|
|||
return client.wshRpcCall("focuswindow", data, opts);
|
||||
}
|
||||
|
||||
// command "getalltabindicators" [call]
|
||||
GetAllTabIndicatorsCommand(client: WshClient, opts?: RpcOpts): Promise<{[key: string]: TabIndicator}> {
|
||||
return client.wshRpcCall("getalltabindicators", null, opts);
|
||||
}
|
||||
|
||||
// command "getallvars" [call]
|
||||
GetAllVarsCommand(client: WshClient, data: CommandVarData, opts?: RpcOpts): Promise<CommandVarResponseData[]> {
|
||||
return client.wshRpcCall("getallvars", data, opts);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 4px;
|
||||
transform: translate3d(0, -50%, 0);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--zindex-tab-name);
|
||||
padding: 1px 2px;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.wave-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { atoms, globalStore, recordTEvent, refocusNode } from "@/app/store/global";
|
||||
import {
|
||||
atoms,
|
||||
clearAllTabIndicators,
|
||||
clearTabIndicatorFromFocus,
|
||||
getTabIndicatorAtom,
|
||||
globalStore,
|
||||
recordTEvent,
|
||||
refocusNode,
|
||||
setTabIndicator,
|
||||
} from "@/app/store/global";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { Button } from "@/element/button";
|
||||
import { ContextMenuModel } from "@/store/contextmenu";
|
||||
import { fireAndForget } from "@/util/util";
|
||||
import { fireAndForget, makeIconClass } from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { ObjectService } from "../store/services";
|
||||
import { makeORef, useWaveObjectValue } from "../store/wos";
|
||||
|
|
@ -36,6 +46,7 @@ const Tab = memo(
|
|||
const [tabData, _] = useWaveObjectValue<Tab>(makeORef("tab", id));
|
||||
const [originalName, setOriginalName] = useState("");
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const indicator = useAtomValue(getTabIndicatorAtom(id));
|
||||
|
||||
const editableRef = useRef<HTMLDivElement>(null);
|
||||
const editableTimeoutRef = useRef<NodeJS.Timeout>(null);
|
||||
|
|
@ -133,17 +144,40 @@ const Tab = memo(
|
|||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const handleTabClick = () => {
|
||||
const currentIndicator = globalStore.get(getTabIndicatorAtom(id));
|
||||
if (currentIndicator?.clearonfocus) {
|
||||
clearTabIndicatorFromFocus(id);
|
||||
}
|
||||
onSelect();
|
||||
};
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
let menu: ContextMenuItem[] = [
|
||||
let menu: ContextMenuItem[] = [];
|
||||
const currentIndicator = globalStore.get(getTabIndicatorAtom(id));
|
||||
if (currentIndicator) {
|
||||
menu.push(
|
||||
{
|
||||
label: "Clear Tab Indicator",
|
||||
click: () => setTabIndicator(id, null),
|
||||
},
|
||||
{
|
||||
label: "Clear All Indicators",
|
||||
click: () => clearAllTabIndicators(),
|
||||
},
|
||||
{ type: "separator" }
|
||||
);
|
||||
}
|
||||
menu.push(
|
||||
{ label: "Rename Tab", click: () => handleRenameTab(null) },
|
||||
{
|
||||
label: "Copy TabId",
|
||||
click: () => fireAndForget(() => navigator.clipboard.writeText(id)),
|
||||
},
|
||||
{ type: "separator" },
|
||||
];
|
||||
{ type: "separator" }
|
||||
);
|
||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||
const bgPresets: string[] = [];
|
||||
for (const key in fullConfig?.presets ?? {}) {
|
||||
|
|
@ -192,7 +226,7 @@ const Tab = memo(
|
|||
"new-tab": isNew,
|
||||
})}
|
||||
onMouseDown={onDragStart}
|
||||
onClick={onSelect}
|
||||
onClick={handleTabClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
data-tab-id={id}
|
||||
>
|
||||
|
|
@ -208,6 +242,15 @@ const Tab = memo(
|
|||
>
|
||||
{tabData?.name}
|
||||
</div>
|
||||
{indicator && (
|
||||
<div
|
||||
className="tab-indicator pointer-events-none"
|
||||
style={{ color: indicator.color || "#fbbf24" }}
|
||||
title="Activity notification"
|
||||
>
|
||||
<i className={makeIconClass(indicator.icon, true, { defaultIcon: "bell" })} />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="ghost grey close"
|
||||
onClick={onClose}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,21 @@ export class TermViewModel implements ViewModel {
|
|||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isMacOS()) {
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowLeft")) {
|
||||
this.sendDataToController("\x01"); // Ctrl-A (beginning of line)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowRight")) {
|
||||
this.sendDataToController("\x05"); // Ctrl-E (end of line)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Shift:Enter")) {
|
||||
const shiftEnterNewlineAtom = getOverrideConfigAtom(this.blockId, "term:shiftenternewline");
|
||||
const shiftEnterNewlineEnabled = globalStore.get(shiftEnterNewlineAtom) ?? true;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,18 @@ import type { BlockNodeModel } from "@/app/block/blocktypes";
|
|||
import { getFileSubject } from "@/app/store/wps";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { WOS, fetchWaveFile, getApi, getSettingsKeyAtom, globalStore, openLink, recordTEvent } from "@/store/global";
|
||||
import {
|
||||
atoms,
|
||||
fetchWaveFile,
|
||||
getApi,
|
||||
getOverrideConfigAtom,
|
||||
getSettingsKeyAtom,
|
||||
globalStore,
|
||||
openLink,
|
||||
recordTEvent,
|
||||
setTabIndicator,
|
||||
WOS,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
|
||||
import { base64ToArray, base64ToString, fireAndForget } from "@/util/util";
|
||||
|
|
@ -478,6 +489,26 @@ export class TermWrap {
|
|||
this.terminal.parser.registerOscHandler(16162, (data: string) => {
|
||||
return handleOsc16162Command(data, this.blockId, this.loaded, this);
|
||||
});
|
||||
this.toDispose.push(
|
||||
this.terminal.onBell(() => {
|
||||
if (!this.loaded) {
|
||||
return true;
|
||||
}
|
||||
console.log("BEL received in terminal", this.blockId);
|
||||
const bellSoundEnabled =
|
||||
globalStore.get(getOverrideConfigAtom(this.blockId, "term:bellsound")) ?? false;
|
||||
if (bellSoundEnabled) {
|
||||
fireAndForget(() => RpcApi.ElectronSystemBellCommand(TabRpcClient, { route: "electron" }));
|
||||
}
|
||||
const bellIndicatorEnabled =
|
||||
globalStore.get(getOverrideConfigAtom(this.blockId, "term:bellindicator")) ?? false;
|
||||
if (bellIndicatorEnabled) {
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
setTabIndicator(tabId, { icon: "bell", color: "#fbbf24", clearonfocus: true, priority: 1 });
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler);
|
||||
this.connectElem = connectElem;
|
||||
this.mainFileSubject = null;
|
||||
|
|
|
|||
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
|
|
@ -21,6 +21,7 @@ declare global {
|
|||
zoomFactorAtom: jotai.PrimitiveAtom<number>;
|
||||
controlShiftDelayAtom: jotai.PrimitiveAtom<boolean>;
|
||||
prefersReducedMotionAtom: jotai.Atom<boolean>;
|
||||
documentHasFocus: jotai.PrimitiveAtom<boolean>;
|
||||
updaterStatusAtom: jotai.PrimitiveAtom<UpdaterStatus>;
|
||||
modalOpen: jotai.PrimitiveAtom<boolean>;
|
||||
allConnStatus: jotai.Atom<ConnStatus[]>;
|
||||
|
|
|
|||
20
frontend/types/gotypes.d.ts
vendored
20
frontend/types/gotypes.d.ts
vendored
|
|
@ -1063,6 +1063,8 @@ declare global {
|
|||
"term:shiftenternewline"?: boolean;
|
||||
"term:macoptionismeta"?: boolean;
|
||||
"term:conndebug"?: string;
|
||||
"term:bellsound"?: boolean;
|
||||
"term:bellindicator"?: boolean;
|
||||
"web:zoom"?: number;
|
||||
"web:hidenav"?: boolean;
|
||||
"web:partition"?: string;
|
||||
|
|
@ -1214,6 +1216,7 @@ declare global {
|
|||
"app:defaultnewblock"?: string;
|
||||
"app:showoverlayblocknums"?: boolean;
|
||||
"app:ctrlvpaste"?: boolean;
|
||||
"app:confirmquit"?: boolean;
|
||||
"feature:waveappbuilder"?: boolean;
|
||||
"ai:*"?: boolean;
|
||||
"ai:preset"?: string;
|
||||
|
|
@ -1245,6 +1248,8 @@ declare global {
|
|||
"term:allowbracketedpaste"?: boolean;
|
||||
"term:shiftenternewline"?: boolean;
|
||||
"term:macoptionismeta"?: boolean;
|
||||
"term:bellsound"?: boolean;
|
||||
"term:bellindicator"?: boolean;
|
||||
"editor:minimapenabled"?: boolean;
|
||||
"editor:stickyscrollenabled"?: boolean;
|
||||
"editor:wordwrap"?: boolean;
|
||||
|
|
@ -1483,6 +1488,21 @@ declare global {
|
|||
blockids: string[];
|
||||
};
|
||||
|
||||
// wshrpc.TabIndicator
|
||||
type TabIndicator = {
|
||||
icon: string;
|
||||
color?: string;
|
||||
priority: number;
|
||||
clearonfocus?: boolean;
|
||||
persistentindicator?: TabIndicator;
|
||||
};
|
||||
|
||||
// wshrpc.TabIndicatorEventData
|
||||
type TabIndicatorEventData = {
|
||||
tabid: string;
|
||||
indicator: TabIndicator;
|
||||
};
|
||||
|
||||
// waveobj.TermSize
|
||||
type TermSize = {
|
||||
rows: number;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
initGlobal,
|
||||
initGlobalWaveEventSubs,
|
||||
loadConnStatus,
|
||||
loadTabIndicators,
|
||||
pushFlashError,
|
||||
pushNotification,
|
||||
removeNotificationById,
|
||||
|
|
@ -166,6 +167,7 @@ async function initWave(initOpts: WaveInitOpts) {
|
|||
(window as any).globalWS = globalWS;
|
||||
(window as any).TabRpcClient = TabRpcClient;
|
||||
await loadConnStatus();
|
||||
await loadTabIndicators();
|
||||
initGlobalWaveEventSubs(initOpts);
|
||||
subscribeToConnEvents();
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ var ExtraTypes = []any{
|
|||
waveobj.ObjRTInfo{},
|
||||
uctypes.RateLimitInfo{},
|
||||
wconfig.AIModeConfigUpdate{},
|
||||
wshrpc.TabIndicatorEventData{},
|
||||
}
|
||||
|
||||
// add extra type unions to generate here
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const (
|
|||
WaveDevVarName = "WAVETERM_DEV"
|
||||
WaveDevViteVarName = "WAVETERM_DEV_VITE"
|
||||
WaveWshForceUpdateVarName = "WAVETERM_WSHFORCEUPDATE"
|
||||
WaveNoConfirmQuitVarName = "WAVETERM_NOCONFIRMQUIT"
|
||||
|
||||
WaveJwtTokenVarName = "WAVETERM_JWT"
|
||||
WaveSwapTokenVarName = "WAVETERM_SWAPTOKEN"
|
||||
|
|
@ -106,6 +107,7 @@ func CacheAndRemoveEnvVars() error {
|
|||
Dev_VarCache = os.Getenv(WaveDevVarName)
|
||||
os.Unsetenv(WaveDevVarName)
|
||||
os.Unsetenv(WaveDevViteVarName)
|
||||
os.Unsetenv(WaveNoConfirmQuitVarName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ const (
|
|||
MetaKey_TermShiftEnterNewline = "term:shiftenternewline"
|
||||
MetaKey_TermMacOptionIsMeta = "term:macoptionismeta"
|
||||
MetaKey_TermConnDebug = "term:conndebug"
|
||||
MetaKey_TermBellSound = "term:bellsound"
|
||||
MetaKey_TermBellIndicator = "term:bellindicator"
|
||||
|
||||
MetaKey_WebZoom = "web:zoom"
|
||||
MetaKey_WebHideNav = "web:hidenav"
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ type MetaTSType struct {
|
|||
TermShiftEnterNewline *bool `json:"term:shiftenternewline,omitempty"`
|
||||
TermMacOptionIsMeta *bool `json:"term:macoptionismeta,omitempty"`
|
||||
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
|
||||
TermBellSound *bool `json:"term:bellsound,omitempty"`
|
||||
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
|
||||
|
||||
WebZoom float64 `json:"web:zoom,omitempty"`
|
||||
WebHideNav *bool `json:"web:hidenav,omitempty"`
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"ai:maxtokens": 4000,
|
||||
"ai:timeoutms": 60000,
|
||||
"app:defaultnewblock": "term",
|
||||
"app:confirmquit": true,
|
||||
"autoupdate:enabled": true,
|
||||
"autoupdate:installonquit": true,
|
||||
"autoupdate:intervalms": 3600000,
|
||||
|
|
@ -23,6 +24,8 @@
|
|||
"window:confirmclose": true,
|
||||
"window:savelastwindow": true,
|
||||
"telemetry:enabled": true,
|
||||
"term:bellsound": false,
|
||||
"term:bellindicator": true,
|
||||
"term:copyonselect": true,
|
||||
"waveai:showcloudmodes": true,
|
||||
"waveai:defaultmode": "waveai@balanced"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const (
|
|||
ConfigKey_AppDefaultNewBlock = "app:defaultnewblock"
|
||||
ConfigKey_AppShowOverlayBlockNums = "app:showoverlayblocknums"
|
||||
ConfigKey_AppCtrlVPaste = "app:ctrlvpaste"
|
||||
ConfigKey_AppConfirmQuit = "app:confirmquit"
|
||||
|
||||
ConfigKey_FeatureWaveAppBuilder = "feature:waveappbuilder"
|
||||
|
||||
|
|
@ -47,6 +48,8 @@ const (
|
|||
ConfigKey_TermAllowBracketedPaste = "term:allowbracketedpaste"
|
||||
ConfigKey_TermShiftEnterNewline = "term:shiftenternewline"
|
||||
ConfigKey_TermMacOptionIsMeta = "term:macoptionismeta"
|
||||
ConfigKey_TermBellSound = "term:bellsound"
|
||||
ConfigKey_TermBellIndicator = "term:bellindicator"
|
||||
|
||||
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
|
||||
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ type SettingsType struct {
|
|||
AppDefaultNewBlock string `json:"app:defaultnewblock,omitempty"`
|
||||
AppShowOverlayBlockNums *bool `json:"app:showoverlayblocknums,omitempty"`
|
||||
AppCtrlVPaste *bool `json:"app:ctrlvpaste,omitempty"`
|
||||
AppConfirmQuit *bool `json:"app:confirmquit,omitempty"`
|
||||
|
||||
FeatureWaveAppBuilder bool `json:"feature:waveappbuilder,omitempty"`
|
||||
|
||||
|
|
@ -94,6 +95,8 @@ type SettingsType struct {
|
|||
TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"`
|
||||
TermShiftEnterNewline *bool `json:"term:shiftenternewline,omitempty"`
|
||||
TermMacOptionIsMeta *bool `json:"term:macoptionismeta,omitempty"`
|
||||
TermBellSound *bool `json:"term:bellsound,omitempty"`
|
||||
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
|
||||
|
||||
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
||||
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
||||
|
|
|
|||
88
pkg/wcore/tabindicator.go
Normal file
88
pkg/wcore/tabindicator.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package wcore
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
type TabIndicatorStore struct {
|
||||
lock *sync.Mutex
|
||||
indicators map[string]*wshrpc.TabIndicator
|
||||
}
|
||||
|
||||
var globalTabIndicatorStore = &TabIndicatorStore{
|
||||
lock: &sync.Mutex{},
|
||||
indicators: make(map[string]*wshrpc.TabIndicator),
|
||||
}
|
||||
|
||||
func InitTabIndicatorStore() {
|
||||
log.Printf("initializing tab indicator store\n")
|
||||
rpcClient := wshclient.GetBareRpcClient()
|
||||
rpcClient.EventListener.On(wps.Event_TabIndicator, handleTabIndicatorEvent)
|
||||
wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
|
||||
Event: wps.Event_TabIndicator,
|
||||
AllScopes: true,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func handleTabIndicatorEvent(event *wps.WaveEvent) {
|
||||
if event.Event != wps.Event_TabIndicator {
|
||||
return
|
||||
}
|
||||
var data wshrpc.TabIndicatorEventData
|
||||
err := utilfn.ReUnmarshal(&data, event.Data)
|
||||
if err != nil {
|
||||
log.Printf("error unmarshaling TabIndicatorEventData: %v\n", err)
|
||||
return
|
||||
}
|
||||
setTabIndicator(data.TabId, data.Indicator)
|
||||
}
|
||||
|
||||
func setTabIndicator(tabId string, indicator *wshrpc.TabIndicator) {
|
||||
globalTabIndicatorStore.lock.Lock()
|
||||
defer globalTabIndicatorStore.lock.Unlock()
|
||||
if indicator == nil {
|
||||
delete(globalTabIndicatorStore.indicators, tabId)
|
||||
log.Printf("tab indicator cleared: tabId=%s\n", tabId)
|
||||
return
|
||||
}
|
||||
currentIndicator := globalTabIndicatorStore.indicators[tabId]
|
||||
if currentIndicator == nil {
|
||||
globalTabIndicatorStore.indicators[tabId] = indicator
|
||||
log.Printf("tab indicator set: tabId=%s indicator=%v\n", tabId, indicator)
|
||||
return
|
||||
}
|
||||
if indicator.Priority >= currentIndicator.Priority {
|
||||
if indicator.ClearOnFocus && !currentIndicator.ClearOnFocus {
|
||||
indicator.PersistentIndicator = currentIndicator
|
||||
}
|
||||
globalTabIndicatorStore.indicators[tabId] = indicator
|
||||
log.Printf("tab indicator updated: tabId=%s indicator=%v\n", tabId, indicator)
|
||||
} else {
|
||||
log.Printf("tab indicator not updated (lower priority): tabId=%s currentPriority=%v newPriority=%v\n", tabId, currentIndicator.Priority, indicator.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func GetTabIndicator(tabId string) *wshrpc.TabIndicator {
|
||||
globalTabIndicatorStore.lock.Lock()
|
||||
defer globalTabIndicatorStore.lock.Unlock()
|
||||
return globalTabIndicatorStore.indicators[tabId]
|
||||
}
|
||||
|
||||
func GetAllTabIndicators() map[string]*wshrpc.TabIndicator {
|
||||
globalTabIndicatorStore.lock.Lock()
|
||||
defer globalTabIndicatorStore.lock.Unlock()
|
||||
result := make(map[string]*wshrpc.TabIndicator)
|
||||
for tabId, indicator := range globalTabIndicatorStore.indicators {
|
||||
result[tabId] = indicator
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ const (
|
|||
Event_WaveAppAppGoUpdated = "waveapp:appgoupdated"
|
||||
Event_TsunamiUpdateMeta = "tsunami:updatemeta"
|
||||
Event_AIModeConfig = "waveai:modeconfig"
|
||||
Event_TabIndicator = "tab:indicator"
|
||||
)
|
||||
|
||||
type WaveEvent struct {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,12 @@ func ElectronEncryptCommand(w *wshutil.WshRpc, data wshrpc.CommandElectronEncryp
|
|||
return resp, err
|
||||
}
|
||||
|
||||
// command "electronsystembell", wshserver.ElectronSystemBellCommand
|
||||
func ElectronSystemBellCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "electronsystembell", nil, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "eventpublish", wshserver.EventPublishCommand
|
||||
func EventPublishCommand(w *wshutil.WshRpc, data wps.WaveEvent, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "eventpublish", data, opts)
|
||||
|
|
@ -368,6 +374,12 @@ func FocusWindowCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) er
|
|||
return err
|
||||
}
|
||||
|
||||
// command "getalltabindicators", wshserver.GetAllTabIndicatorsCommand
|
||||
func GetAllTabIndicatorsCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (map[string]*wshrpc.TabIndicator, error) {
|
||||
resp, err := sendRpcRequestCallHelper[map[string]*wshrpc.TabIndicator](w, "getalltabindicators", nil, opts)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// command "getallvars", wshserver.GetAllVarsCommand
|
||||
func GetAllVarsCommand(w *wshutil.WshRpc, data wshrpc.CommandVarData, opts *wshrpc.RpcOpts) ([]wshrpc.CommandVarResponseData, error) {
|
||||
resp, err := sendRpcRequestCallHelper[[]wshrpc.CommandVarResponseData](w, "getallvars", data, opts)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ type WshRpcInterface interface {
|
|||
FetchSuggestionsCommand(ctx context.Context, data FetchSuggestionsData) (*FetchSuggestionsResponse, error)
|
||||
DisposeSuggestionsCommand(ctx context.Context, widgetId string) error
|
||||
GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error)
|
||||
GetAllTabIndicatorsCommand(ctx context.Context) (map[string]*TabIndicator, error)
|
||||
|
||||
// connection functions
|
||||
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)
|
||||
|
|
@ -123,6 +124,7 @@ type WshRpcInterface interface {
|
|||
ElectronEncryptCommand(ctx context.Context, data CommandElectronEncryptData) (*CommandElectronEncryptRtnData, error)
|
||||
ElectronDecryptCommand(ctx context.Context, data CommandElectronDecryptData) (*CommandElectronDecryptRtnData, error)
|
||||
NetworkOnlineCommand(ctx context.Context) (bool, error)
|
||||
ElectronSystemBellCommand(ctx context.Context) error
|
||||
|
||||
// secrets
|
||||
GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error)
|
||||
|
|
@ -840,3 +842,16 @@ type WaveFileInfo struct {
|
|||
ModTs int64 `json:"modts"`
|
||||
Meta FileMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type TabIndicator struct {
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Priority float64 `json:"priority"`
|
||||
ClearOnFocus bool `json:"clearonfocus,omitempty"`
|
||||
PersistentIndicator *TabIndicator `json:"persistentindicator,omitempty"`
|
||||
}
|
||||
|
||||
type TabIndicatorEventData struct {
|
||||
TabId string `json:"tabid"`
|
||||
Indicator *TabIndicator `json:"indicator"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1402,6 +1402,10 @@ func (ws *WshServer) GetTabCommand(ctx context.Context, tabId string) (*waveobj.
|
|||
return tab, nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) GetAllTabIndicatorsCommand(ctx context.Context) (map[string]*wshrpc.TabIndicator, error) {
|
||||
return wcore.GetAllTabIndicators(), nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
for _, name := range names {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
"app:ctrlvpaste": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"app:confirmquit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"feature:waveappbuilder": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -119,6 +122,12 @@
|
|||
"term:macoptionismeta": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"term:bellsound": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"term:bellindicator": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"editor:minimapenabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue