global shortcut for wave (#287)

* working on easy global shortcut for wave

* globalshortcut setting working

* cmd for macos, alt for others

* re-remove types.ts (was added back during merge)

* rename DDItem to DropdownItem, put into custom.d.ts

* make some consts
This commit is contained in:
Mike Sawka 2024-02-13 20:43:02 -05:00 committed by GitHub
parent 18fe3f3296
commit 3e4bd458b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 147 additions and 28 deletions

View File

@ -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 (
<div className={cn("view clientsettings-view")}>
@ -207,6 +229,17 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Global Hotkey</div>
<div className="settings-input">
<Dropdown
className="hotkey-dropdown"
options={this.getFKeys()}
defaultValue={this.getCurrentShortcut()}
onChange={this.handleChangeShortcut}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
</div>

View File

@ -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;

View File

@ -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 () => {

View File

@ -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),

View File

@ -431,6 +431,10 @@ class CommandRunner {
}
GlobalModel.submitCommand("sidebar", "open", null, kwargs, false);
}
setGlobalShortcut(shortcut: string): Promise<CommandRtnType> {
return GlobalModel.submitCommand("client", "setglobalshortcut", [shortcut], { nohist: "1" }, false);
}
}
export { CommandRunner };

View File

@ -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);
})();
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<CommandRtnType> {
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);
}

View File

@ -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 = {

View File

@ -1,6 +1,5 @@
{
"include": ["src/**/*", "types/**/*"],
// "exclude": ["src/electron/emain.ts"],
"compilerOptions": {
"target": "es5",
"module": "commonjs",

View File

@ -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 {

View File

@ -288,6 +288,8 @@ type ClientOptsType struct {
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 {