diff --git a/src/app/clientsettings/clientsettings.tsx b/src/app/clientsettings/clientsettings.tsx
index 48c7b0ea..dd772bc5 100644
--- a/src/app/clientsettings/clientsettings.tsx
+++ b/src/app/clientsettings/clientsettings.tsx
@@ -29,12 +29,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
@boundMethod
handleChangeFontSize(fontSize: string): void {
- let newFontSize = Number(fontSize);
+ const newFontSize = Number(fontSize);
this.fontSizeDropdownActive.set(false);
if (GlobalModel.termFontSize.get() == newFontSize) {
return;
}
- let prtn = GlobalCommandRunner.setTermFontSize(newFontSize, false);
+ const prtn = GlobalCommandRunner.setTermFontSize(newFontSize, false);
commandRtnHandler(prtn, this.errorMessage);
}
@@ -67,29 +67,29 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
commandRtnHandler(prtn, this.errorMessage);
}
- getFontSizes(): any {
- let availableFontSizes: { label: string; value: number }[] = [];
+ getFontSizes(): DropdownItem[] {
+ const availableFontSizes: DropdownItem[] = [];
for (let s = appconst.MinFontSize; s <= appconst.MaxFontSize; s++) {
- availableFontSizes.push({ label: s + "px", value: s });
+ availableFontSizes.push({ label: s + "px", value: String(s) });
}
return availableFontSizes;
}
@boundMethod
inlineUpdateOpenAIModel(newModel: string): void {
- let prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
+ const prtn = GlobalCommandRunner.setClientOpenAISettings({ model: newModel });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIToken(newToken: string): void {
- let prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
+ const prtn = GlobalCommandRunner.setClientOpenAISettings({ apitoken: newToken });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIMaxTokens(newMaxTokensStr: string): void {
- let prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
+ const prtn = GlobalCommandRunner.setClientOpenAISettings({ maxtokens: newMaxTokensStr });
commandRtnHandler(prtn, this.errorMessage);
}
@@ -105,19 +105,41 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
GlobalModel.clientSettingsViewModel.closeView();
}
+ @boundMethod
+ handleChangeShortcut(newShortcut: string): void {
+ const prtn = GlobalCommandRunner.setGlobalShortcut(newShortcut);
+ commandRtnHandler(prtn, this.errorMessage);
+ }
+
+ getFKeys(): DropdownItem[] {
+ const opts: DropdownItem[] = [];
+ opts.push({ label: "Disabled", value: "" });
+ const platform = GlobalModel.getPlatform();
+ for (let i = 1; i <= 12; i++) {
+ const shortcut = (platform == "darwin" ? "Cmd" : "Alt") + "+F" + String(i);
+ opts.push({ label: shortcut, value: shortcut });
+ }
+ return opts;
+ }
+
+ getCurrentShortcut(): string {
+ const clientData = GlobalModel.clientData.get();
+ return clientData?.clientopts?.globalshortcut ?? "";
+ }
+
render() {
- let isHidden = GlobalModel.activeMainView.get() != "clientsettings";
+ const isHidden = GlobalModel.activeMainView.get() != "clientsettings";
if (isHidden) {
return null;
}
- let cdata: ClientDataType = GlobalModel.clientData.get();
- let openAIOpts = cdata.openaiopts ?? {};
- let apiTokenStr = isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
- let maxTokensStr = String(
+ const cdata: ClientDataType = GlobalModel.clientData.get();
+ const openAIOpts = cdata.openaiopts ?? {};
+ const apiTokenStr = isBlank(openAIOpts.apitoken) ? "(not set)" : "********";
+ const maxTokensStr = String(
openAIOpts.maxtokens == null || openAIOpts.maxtokens == 0 ? 1000 : openAIOpts.maxtokens
);
- let curFontSize = GlobalModel.termFontSize.get();
+ const curFontSize = GlobalModel.termFontSize.get();
return (
@@ -207,6 +229,17 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
+
diff --git a/src/app/common/elements/dropdown.tsx b/src/app/common/elements/dropdown.tsx
index 0b9ea4c8..f7fef4f9 100644
--- a/src/app/common/elements/dropdown.tsx
+++ b/src/app/common/elements/dropdown.tsx
@@ -17,7 +17,7 @@ interface DropdownDecorationProps {
interface DropdownProps {
label?: string;
- options: { value: string; label: string }[];
+ options: DropdownItem[];
value?: string;
className?: string;
onChange: (value: string) => void;
diff --git a/src/electron/emain.ts b/src/electron/emain.ts
index a9ce352b..faf702d5 100644
--- a/src/electron/emain.ts
+++ b/src/electron/emain.ts
@@ -8,8 +8,9 @@ import fetch from "node-fetch";
import * as child_process from "node:child_process";
import { debounce } from "throttle-debounce";
import * as winston from "winston";
-import { sprintf } from "sprintf-js";
import * as util from "util";
+import * as waveutil from "../util/util";
+import { sprintf } from "sprintf-js";
import { handleJsonFetchResponse } from "@/util/util";
import { v4 as uuidv4 } from "uuid";
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "@/util/keyutil";
@@ -29,6 +30,7 @@ let instanceId = uuidv4();
let oldConsoleLog = console.log;
let wasActive = true;
let wasInFg = true;
+let currentGlobalShortcut: string | null = null;
checkPromptMigrate();
ensureDir(waveHome);
@@ -412,7 +414,7 @@ function mainResizeHandler(e, win) {
});
}
-function calcBounds(clientData) {
+function calcBounds(clientData: ClientDataType) {
let primaryDisplay = electron.screen.getPrimaryDisplay();
let pdBounds = primaryDisplay.bounds;
let size = { x: 100, y: 100, width: pdBounds.width - 200, height: pdBounds.height - 200 };
@@ -509,6 +511,12 @@ electron.ipcMain.on("open-external-link", async (_, url) => {
}
});
+electron.ipcMain.on("reregister-global-shortcut", (event, shortcut: string) => {
+ reregisterGlobalShortcut(shortcut);
+ event.returnValue = true;
+ return;
+});
+
electron.ipcMain.on("get-last-logs", async (event, numberOfLines) => {
try {
const logPath = path.join(getWaveHomeDir(), "wavesrv.log");
@@ -698,6 +706,34 @@ function runActiveTimer() {
setTimeout(runActiveTimer, 60000);
}
+function reregisterGlobalShortcut(shortcut: string) {
+ if (shortcut == "") {
+ shortcut = null;
+ }
+ if (currentGlobalShortcut == shortcut) {
+ return;
+ }
+ if (!waveutil.isBlank(currentGlobalShortcut)) {
+ if (electron.globalShortcut.isRegistered(currentGlobalShortcut)) {
+ electron.globalShortcut.unregister(currentGlobalShortcut);
+ }
+ }
+ if (waveutil.isBlank(shortcut)) {
+ currentGlobalShortcut = null;
+ return;
+ }
+ let ok = electron.globalShortcut.register(shortcut, () => {
+ console.log("global shortcut triggered, showing window");
+ MainWindow?.show();
+ });
+ console.log("registered global shortcut", shortcut, ok ? "ok" : "failed");
+ if (!ok) {
+ currentGlobalShortcut = null;
+ console.log("failed to register global shortcut", shortcut);
+ }
+ currentGlobalShortcut = shortcut;
+}
+
// ====== MAIN ====== //
(async () => {
diff --git a/src/electron/preload.js b/src/electron/preload.js
index 229a76e0..e7be2ec5 100644
--- a/src/electron/preload.js
+++ b/src/electron/preload.js
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld("api", {
},
restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
+ reregisterGlobalShortcut: (shortcut) => ipcRenderer.sendSync("reregister-global-shortcut", shortcut),
openExternalLink: (url) => ipcRenderer.send("open-external-link", url),
onTCmd: (callback) => ipcRenderer.on("t-cmd", callback),
onICmd: (callback) => ipcRenderer.on("i-cmd", callback),
diff --git a/src/models/commandrunner.ts b/src/models/commandrunner.ts
index eaf516af..19bb95f3 100644
--- a/src/models/commandrunner.ts
+++ b/src/models/commandrunner.ts
@@ -431,6 +431,10 @@ class CommandRunner {
}
GlobalModel.submitCommand("sidebar", "open", null, kwargs, false);
}
+
+ setGlobalShortcut(shortcut: string): Promise {
+ return GlobalModel.submitCommand("client", "setglobalshortcut", [shortcut], { nohist: "1" }, false);
+ }
}
export { CommandRunner };
diff --git a/src/models/model.ts b/src/models/model.ts
index 3c65a658..26e7a567 100644
--- a/src/models/model.ts
+++ b/src/models/model.ts
@@ -55,6 +55,7 @@ type ElectronApi = {
restartWaveSrv: () => boolean;
reloadWindow: () => void;
openExternalLink: (url: string) => void;
+ reregisterGlobalShortcut: (shortcut: string) => void;
onTCmd: (callback: (mods: KeyModsType) => void) => void;
onICmd: (callback: (mods: KeyModsType) => void) => void;
onLCmd: (callback: (mods: KeyModsType) => void) => void;
@@ -887,7 +888,7 @@ class Model {
this.bookmarksModel.mergeBookmarks(update.bookmarks.bookmarks);
}
} else if (update.clientdata != null) {
- this.clientData.set(update.clientdata);
+ this.setClientData(update.clientdata);
} else if (update.cmdline != null) {
this.inputModel.updateCmdLine(update.cmdline);
} else if (update.openaicmdinfochat != null) {
@@ -1095,16 +1096,25 @@ class Model {
fetch(url, { method: "post", body: null, headers: fetchHeaders })
.then((resp) => handleJsonFetchResponse(url, resp))
.then((data) => {
- mobx.action(() => {
- const clientData: ClientDataType = data.data;
- this.clientData.set(clientData);
- })();
+ const clientData: ClientDataType = data.data;
+ this.setClientData(clientData);
})
.catch((err) => {
this.errorHandler("calling get-client-data", err, true);
});
}
+ setClientData(clientData: ClientDataType) {
+ mobx.action(() => {
+ this.clientData.set(clientData);
+ })();
+ let shortcut = null;
+ if (clientData?.clientopts?.globalshortcutenabled) {
+ shortcut = clientData?.clientopts?.globalshortcut;
+ }
+ getApi().reregisterGlobalShortcut(shortcut);
+ }
+
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise {
if (this.debugCmds > 0) {
console.log("[cmd]", cmdPacketString(cmdPk));
@@ -1328,6 +1338,10 @@ class Model {
}
}
+ sendUserInput(userInputResponsePacket: UserInputResponsePacket) {
+ this.ws.pushMessage(userInputResponsePacket);
+ }
+
sendInputPacket(inputPacket: any) {
this.ws.pushMessage(inputPacket);
}
diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts
index f8b46bd0..5d5922a4 100644
--- a/src/types/custom.d.ts
+++ b/src/types/custom.d.ts
@@ -292,6 +292,11 @@ declare global {
userquery?: string;
};
+ type DropdownItem = {
+ label: string;
+ value: string;
+ };
+
/**
* Levels for the screen status indicator
*/
@@ -554,6 +559,8 @@ declare global {
collapsed: boolean;
width: number;
};
+ globalshortcut: string;
+ globalshortcutenabled: boolean;
};
type ReleaseInfoType = {
diff --git a/tsconfig.json b/tsconfig.json
index f7fd46eb..a3904cf6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,5 @@
{
"include": ["src/**/*", "types/**/*"],
- // "exclude": ["src/electron/emain.ts"],
"compilerOptions": {
"target": "es5",
"module": "commonjs",
diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go
index 7bf42891..39449e26 100644
--- a/wavesrv/pkg/cmdrunner/cmdrunner.go
+++ b/wavesrv/pkg/cmdrunner/cmdrunner.go
@@ -224,6 +224,7 @@ func init() {
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
+ registerCmdFn("client:setglobalshortcut", ClientSetGlobalShortcut)
registerCmdFn("sidebar:open", SidebarOpenCommand)
registerCmdFn("sidebar:close", SidebarCloseCommand)
@@ -4990,6 +4991,28 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
return update, nil
}
+func ClientSetGlobalShortcut(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
+ clientData, err := sstore.EnsureClientData(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("cannot retrieve client data: %v", err)
+ }
+ newShortcut := firstArg(pk)
+ if len(newShortcut) > 50 {
+ return nil, fmt.Errorf("invalid shortcut (maxlen = 50)")
+ }
+ clientOpts := clientData.ClientOpts
+ clientOpts.GlobalShortcut = newShortcut
+ clientOpts.GlobalShortcutEnabled = (newShortcut != "")
+ err = sstore.SetClientOpts(ctx, clientOpts)
+ if err != nil {
+ return nil, fmt.Errorf("error updating client data: %v", err)
+ }
+ clientData.ClientOpts = clientOpts
+ update := &sstore.ModelUpdate{}
+ sstore.AddUpdate(update, *clientData)
+ return update, nil
+}
+
func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go
index e9df8e73..309140e9 100644
--- a/wavesrv/pkg/sstore/sstore.go
+++ b/wavesrv/pkg/sstore/sstore.go
@@ -283,11 +283,13 @@ type SidebarValueType struct {
}
type ClientOptsType struct {
- NoTelemetry bool `json:"notelemetry,omitempty"`
- NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
- AcceptedTos int64 `json:"acceptedtos,omitempty"`
- ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
- MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
+ NoTelemetry bool `json:"notelemetry,omitempty"`
+ NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
+ AcceptedTos int64 `json:"acceptedtos,omitempty"`
+ ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
+ MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
+ GlobalShortcut string `json:"globalshortcut,omitempty"`
+ GlobalShortcutEnabled bool `json:"globalshortcutenabled,omitempty"`
}
type FeOptsType struct {