Skip to content

Commit 73bb5be

Browse files
authored
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)
1 parent 9d7a457 commit 73bb5be

File tree

34 files changed

+611
-27
lines changed

34 files changed

+611
-27
lines changed

.github/ISSUE_TEMPLATE/bug-report.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,3 @@ body:
8484
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
8585
validations:
8686
required: false
87-
- type: checkboxes
88-
attributes:
89-
label: Questionnaire
90-
description: "If you feel up to the challenge, please check one of the boxes below:"
91-
options:
92-
- label: I'm interested in fixing this myself but don't know where to start
93-
required: false
94-
- label: I would like to fix and I have a solution
95-
required: false
96-
- label: I don't have time to fix this right now, but maybe later
97-
required: false

Taskfile.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ tasks:
2929
WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central"
3030
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
3131
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev"
32+
WAVETERM_NOCONFIRMQUIT: "1"
3233

3334
electron:start:
3435
desc: Run the Electron application directly.
@@ -55,6 +56,7 @@ tasks:
5556
WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central"
5657
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
5758
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/"
59+
WAVETERM_NOCONFIRMQUIT: "1"
5860

5961
electron:winquickdev:
6062
desc: Run the Electron application via the Vite dev server (quick dev - Windows amd64 only, no generate, no wsh).

cmd/server/main-server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ func main() {
568568
go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher()
569569
blocklogger.InitBlockLogger()
570570
jobcontroller.InitJobController()
571+
wcore.InitTabIndicatorStore()
571572
go func() {
572573
defer func() {
573574
panichandler.PanicHandler("GetSystemSummary", recover())

cmd/wsh/cmd/wshcmd-tabindicator.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"os"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/wavetermdev/waveterm/pkg/waveobj"
12+
"github.com/wavetermdev/waveterm/pkg/wps"
13+
"github.com/wavetermdev/waveterm/pkg/wshrpc"
14+
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
15+
)
16+
17+
var tabIndicatorCmd = &cobra.Command{
18+
Use: "tabindicator [icon]",
19+
Short: "set or clear a tab indicator",
20+
Args: cobra.MaximumNArgs(1),
21+
RunE: tabIndicatorRun,
22+
PreRunE: preRunSetupRpcClient,
23+
}
24+
25+
var (
26+
tabIndicatorTabId string
27+
tabIndicatorColor string
28+
tabIndicatorPriority float64
29+
tabIndicatorClear bool
30+
tabIndicatorPersistent bool
31+
tabIndicatorBeep bool
32+
)
33+
34+
func init() {
35+
rootCmd.AddCommand(tabIndicatorCmd)
36+
tabIndicatorCmd.Flags().StringVar(&tabIndicatorTabId, "tabid", "", "tab id (defaults to WAVETERM_TABID)")
37+
tabIndicatorCmd.Flags().StringVar(&tabIndicatorColor, "color", "", "indicator color")
38+
tabIndicatorCmd.Flags().Float64Var(&tabIndicatorPriority, "priority", 0, "indicator priority")
39+
tabIndicatorCmd.Flags().BoolVar(&tabIndicatorClear, "clear", false, "clear the indicator")
40+
tabIndicatorCmd.Flags().BoolVar(&tabIndicatorPersistent, "persistent", false, "make indicator persistent (don't clear on focus)")
41+
tabIndicatorCmd.Flags().BoolVar(&tabIndicatorBeep, "beep", false, "play system bell sound")
42+
}
43+
44+
func tabIndicatorRun(cmd *cobra.Command, args []string) (rtnErr error) {
45+
defer func() {
46+
sendActivity("tabindicator", rtnErr == nil)
47+
}()
48+
49+
tabId := tabIndicatorTabId
50+
if tabId == "" {
51+
tabId = os.Getenv("WAVETERM_TABID")
52+
}
53+
if tabId == "" {
54+
return fmt.Errorf("no tab id specified (use --tabid or set WAVETERM_TABID)")
55+
}
56+
57+
var indicator *wshrpc.TabIndicator
58+
if !tabIndicatorClear {
59+
icon := "bell"
60+
if len(args) > 0 {
61+
icon = args[0]
62+
}
63+
indicator = &wshrpc.TabIndicator{
64+
Icon: icon,
65+
Color: tabIndicatorColor,
66+
Priority: tabIndicatorPriority,
67+
ClearOnFocus: !tabIndicatorPersistent,
68+
}
69+
}
70+
71+
eventData := wshrpc.TabIndicatorEventData{
72+
TabId: tabId,
73+
Indicator: indicator,
74+
}
75+
76+
event := wps.WaveEvent{
77+
Event: wps.Event_TabIndicator,
78+
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, tabId).String()},
79+
Data: eventData,
80+
}
81+
82+
err := wshclient.EventPublishCommand(RpcClient, event, &wshrpc.RpcOpts{NoResponse: true})
83+
if err != nil {
84+
return fmt.Errorf("publishing tab indicator event: %v", err)
85+
}
86+
87+
if tabIndicatorBeep {
88+
err = wshclient.ElectronSystemBellCommand(RpcClient, &wshrpc.RpcOpts{Route: "electron"})
89+
if err != nil {
90+
return fmt.Errorf("playing system bell: %v", err)
91+
}
92+
}
93+
94+
if tabIndicatorClear {
95+
fmt.Printf("tab indicator cleared\n")
96+
} else {
97+
fmt.Printf("tab indicator set\n")
98+
}
99+
return nil
100+
}

docs/docs/config.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ wsh editconfig
3838
| app:defaultnewblock | string | Sets the default new block (Cmd:n, Cmd:d). "term" for terminal block, "launcher" for launcher block (default = "term") |
3939
| app:showoverlayblocknums | bool | Set to false to disable the Ctrl+Shift block number overlay that appears when holding Ctrl+Shift (defaults to true) |
4040
| 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 |
41+
| app:confirmquit | bool | Set to false to disable the quit confirmation dialog when closing Wave Terminal (defaults to true, requires app restart) |
4142
| ai:preset | string | the default AI preset to use |
4243
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
4344
| ai:apitoken | string | your AI api token |
@@ -62,6 +63,8 @@ wsh editconfig
6263
| term:allowbracketedpaste | bool | allow bracketed paste mode in terminal (default false) |
6364
| 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) |
6465
| term:macoptionismeta | bool | on macOS, treat the Option key as Meta key for terminal keybindings (default false) |
66+
| term:bellsound | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
67+
| term:bellindicator | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default true) |
6568
| editor:minimapenabled | bool | set to false to disable editor minimap |
6669
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
6770
| 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):
108111
"ai:maxtokens": 4000,
109112
"ai:timeoutms": 60000,
110113
"app:defaultnewblock": "term",
114+
"app:confirmquit": true,
111115
"autoupdate:enabled": true,
112116
"autoupdate:installonquit": true,
113117
"autoupdate:intervalms": 3600000,
@@ -126,7 +130,11 @@ For reference, this is the current default configuration (v0.11.5):
126130
"window:confirmclose": true,
127131
"window:savelastwindow": true,
128132
"telemetry:enabled": true,
129-
"term:copyonselect": true
133+
"term:bellsound": false,
134+
"term:bellindicator": true,
135+
"term:copyonselect": true,
136+
"waveai:showcloudmodes": true,
137+
"waveai:defaultmode": "waveai@balanced"
130138
}
131139
```
132140

docs/docs/keybindings.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
100100
| <Kbd k="Shift:End"/> | Scroll to bottom |
101101
| <Kbd k="Cmd:Home" windows="N/A" linux="N/A"/> | Scroll to top (macOS only) |
102102
| <Kbd k="Cmd:End" windows="N/A" linux="N/A"/> | Scroll to bottom (macOS only)|
103+
| <Kbd k="Cmd:ArrowLeft" windows="N/A" linux="N/A"/> | Move to beginning of line (macOS only) |
104+
| <Kbd k="Cmd:ArrowRight" windows="N/A" linux="N/A"/> | Move to end of line (macOS only) |
103105
| <Kbd k="Shift:PageUp"/> | Scroll up one page |
104106
| <Kbd k="Shift:PageDown"/>| Scroll down one page |
105107

emain/emain-activity.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let globalIsQuitting = false;
88
let globalIsStarting = true;
99
let globalIsRelaunching = false;
1010
let forceQuit = false;
11+
let userConfirmedQuit = false;
1112
let termCommandsRun = 0;
1213

1314
export function setWasActive(val: boolean) {
@@ -54,6 +55,14 @@ export function getForceQuit(): boolean {
5455
return forceQuit;
5556
}
5657

58+
export function setUserConfirmedQuit(val: boolean) {
59+
userConfirmedQuit = val;
60+
}
61+
62+
export function getUserConfirmedQuit(): boolean {
63+
return userConfirmedQuit;
64+
}
65+
5766
export function incrementTermCommandsRun() {
5867
termCommandsRun++;
5968
}

emain/emain-tabview.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import {
2020
} from "./emain-util";
2121
import { ElectronWshClient } from "./emain-wsh";
2222

23-
function handleWindowsMenuAccelerators(waveEvent: WaveKeyboardEvent, tabView: WaveTabView): boolean {
23+
function handleWindowsMenuAccelerators(
24+
waveEvent: WaveKeyboardEvent,
25+
tabView: WaveTabView,
26+
fullConfig: FullConfigType
27+
): boolean {
2428
const waveWindow = getWaveWindowById(tabView.waveWindowId);
2529

2630
if (checkKeyPressed(waveEvent, "Ctrl:Shift:n")) {
@@ -34,6 +38,11 @@ function handleWindowsMenuAccelerators(waveEvent: WaveKeyboardEvent, tabView: Wa
3438
}
3539

3640
if (checkKeyPressed(waveEvent, "Ctrl:v")) {
41+
const ctrlVPaste = fullConfig?.settings?.["app:ctrlvpaste"];
42+
const shouldPaste = ctrlVPaste ?? true;
43+
if (!shouldPaste) {
44+
return false;
45+
}
3746
tabView.webContents.paste();
3847
return true;
3948
}
@@ -324,7 +333,7 @@ export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: stri
324333
}
325334

326335
if (unamePlatform === "win32" && input.type == "keyDown") {
327-
if (handleWindowsMenuAccelerators(waveEvent, tabView)) {
336+
if (handleWindowsMenuAccelerators(waveEvent, tabView, fullConfig)) {
328337
e.preventDefault();
329338
return;
330339
}

emain/emain-wavesrv.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as child_process from "node:child_process";
66
import * as readline from "readline";
77
import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/util/endpoints";
88
import { AuthKey, WaveAuthKeyEnv } from "./authkey";
9-
import { setForceQuit } from "./emain-activity";
9+
import { setForceQuit, setUserConfirmedQuit } from "./emain-activity";
1010
import {
1111
getElectronAppResourcesPath,
1212
getElectronAppUnpackedBasePath,
@@ -112,6 +112,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
112112
);
113113
if (startParams == null) {
114114
console.log("error parsing WAVESRV-ESTART line", line);
115+
setUserConfirmedQuit(true);
115116
electron.app.quit();
116117
return;
117118
}

emain/emain-wsh.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { WindowService } from "@/app/store/services";
55
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
66
import { RpcApi } from "@/app/store/wshclientapi";
7-
import { Notification, net, safeStorage } from "electron";
7+
import { Notification, net, safeStorage, shell } from "electron";
88
import { getResolvedUpdateChannel } from "emain/updater";
99
import { unamePlatform } from "./emain-platform";
1010
import { getWebContentsByBlockId, webGetSelector } from "./emain-web";
@@ -106,6 +106,10 @@ export class ElectronWshClientType extends WshClient {
106106
return net.isOnline();
107107
}
108108

109+
async handle_electronsystembell(rh: RpcResponseHelper): Promise<void> {
110+
shell.beep();
111+
}
112+
109113
// async handle_workspaceupdate(rh: RpcResponseHelper) {
110114
// console.log("workspaceupdate");
111115
// fireAndForget(async () => {

0 commit comments

Comments
 (0)