mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import {
|
|
atoms,
|
|
createBlock,
|
|
createTab,
|
|
getApi,
|
|
getBlockComponentModel,
|
|
globalStore,
|
|
refocusNode,
|
|
WOS,
|
|
} from "@/app/store/global";
|
|
import {
|
|
deleteLayoutModelForTab,
|
|
getLayoutModelForTab,
|
|
getLayoutModelForTabById,
|
|
NavigateDirection,
|
|
} from "@/layout/index";
|
|
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
|
|
import * as keyutil from "@/util/keyutil";
|
|
import { fireAndForget } from "@/util/util";
|
|
import * as jotai from "jotai";
|
|
|
|
const simpleControlShiftAtom = jotai.atom(false);
|
|
const globalKeyMap = new Map<string, (waveEvent: WaveKeyboardEvent) => boolean>();
|
|
|
|
function getFocusedBlockInStaticTab() {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
const layoutModel = getLayoutModelForTabById(tabId);
|
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
|
return focusedNode.data?.blockId;
|
|
}
|
|
|
|
function getSimpleControlShiftAtom() {
|
|
return simpleControlShiftAtom;
|
|
}
|
|
|
|
function setControlShift() {
|
|
globalStore.set(simpleControlShiftAtom, true);
|
|
setTimeout(() => {
|
|
const simpleState = globalStore.get(simpleControlShiftAtom);
|
|
if (simpleState) {
|
|
globalStore.set(atoms.controlShiftDelayAtom, true);
|
|
}
|
|
}, 400);
|
|
}
|
|
|
|
function unsetControlShift() {
|
|
globalStore.set(simpleControlShiftAtom, false);
|
|
globalStore.set(atoms.controlShiftDelayAtom, false);
|
|
}
|
|
|
|
function shouldDispatchToBlock(e: WaveKeyboardEvent): boolean {
|
|
if (globalStore.get(atoms.modalOpen)) {
|
|
return false;
|
|
}
|
|
const activeElem = document.activeElement;
|
|
if (activeElem != null && activeElem instanceof HTMLElement) {
|
|
if (activeElem.tagName == "INPUT" || activeElem.tagName == "TEXTAREA" || activeElem.contentEditable == "true") {
|
|
if (activeElem.classList.contains("dummy-focus")) {
|
|
return true;
|
|
}
|
|
if (keyutil.isInputEvent(e)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function genericClose(tabId: string) {
|
|
const ws = globalStore.get(atoms.workspace);
|
|
const tabORef = WOS.makeORef("tab", tabId);
|
|
const tabAtom = WOS.getWaveObjectAtom<Tab>(tabORef);
|
|
const tabData = globalStore.get(tabAtom);
|
|
if (tabData == null) {
|
|
return;
|
|
}
|
|
if (ws.pinnedtabids?.includes(tabId) && tabData.blockids?.length == 1) {
|
|
// don't allow closing the last block in a pinned tab
|
|
return;
|
|
}
|
|
if (tabData.blockids == null || tabData.blockids.length == 0) {
|
|
// close tab
|
|
getApi().closeTab(ws.oid, tabId);
|
|
deleteLayoutModelForTab(tabId);
|
|
return;
|
|
}
|
|
const layoutModel = getLayoutModelForTab(tabAtom);
|
|
fireAndForget(layoutModel.closeFocusedNode.bind(layoutModel));
|
|
}
|
|
|
|
function switchBlockByBlockNum(index: number) {
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
if (!layoutModel) {
|
|
return;
|
|
}
|
|
layoutModel.switchNodeFocusByBlockNum(index);
|
|
}
|
|
|
|
function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
|
|
const layoutModel = getLayoutModelForTabById(tabId);
|
|
layoutModel.switchNodeFocusInDirection(direction);
|
|
}
|
|
|
|
function getAllTabs(ws: Workspace): string[] {
|
|
return [...(ws.pinnedtabids ?? []), ...(ws.tabids ?? [])];
|
|
}
|
|
|
|
function switchTabAbs(index: number) {
|
|
console.log("switchTabAbs", index);
|
|
const ws = globalStore.get(atoms.workspace);
|
|
const newTabIdx = index - 1;
|
|
const tabids = getAllTabs(ws);
|
|
if (newTabIdx < 0 || newTabIdx >= tabids.length) {
|
|
return;
|
|
}
|
|
const newActiveTabId = tabids[newTabIdx];
|
|
getApi().setActiveTab(newActiveTabId);
|
|
}
|
|
|
|
function switchTab(offset: number) {
|
|
console.log("switchTab", offset);
|
|
const ws = globalStore.get(atoms.workspace);
|
|
const curTabId = globalStore.get(atoms.staticTabId);
|
|
let tabIdx = -1;
|
|
const tabids = getAllTabs(ws);
|
|
for (let i = 0; i < tabids.length; i++) {
|
|
if (tabids[i] == curTabId) {
|
|
tabIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
if (tabIdx == -1) {
|
|
return;
|
|
}
|
|
const newTabIdx = (tabIdx + offset + tabids.length) % tabids.length;
|
|
const newActiveTabId = tabids[newTabIdx];
|
|
getApi().setActiveTab(newActiveTabId);
|
|
}
|
|
|
|
function handleCmdI() {
|
|
globalRefocus();
|
|
}
|
|
|
|
function globalRefocus() {
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
|
if (focusedNode == null) {
|
|
// focus a node
|
|
layoutModel.focusFirstNode();
|
|
return;
|
|
}
|
|
const blockId = focusedNode?.data?.blockId;
|
|
if (blockId == null) {
|
|
return;
|
|
}
|
|
refocusNode(blockId);
|
|
}
|
|
|
|
async function handleCmdN() {
|
|
const termBlockDef: BlockDef = {
|
|
meta: {
|
|
view: "term",
|
|
controller: "shell",
|
|
},
|
|
};
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
|
if (focusedNode != null) {
|
|
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", focusedNode.data?.blockId));
|
|
const blockData = globalStore.get(blockAtom);
|
|
if (blockData?.meta?.view == "term") {
|
|
if (blockData?.meta?.["cmd:cwd"] != null) {
|
|
termBlockDef.meta["cmd:cwd"] = blockData.meta["cmd:cwd"];
|
|
}
|
|
}
|
|
if (blockData?.meta?.connection != null) {
|
|
termBlockDef.meta.connection = blockData.meta.connection;
|
|
}
|
|
}
|
|
await createBlock(termBlockDef);
|
|
}
|
|
|
|
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
|
const handled = handleGlobalWaveKeyboardEvents(waveEvent);
|
|
if (handled) {
|
|
return true;
|
|
}
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
|
const blockId = focusedNode?.data?.blockId;
|
|
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
|
|
const bcm = getBlockComponentModel(blockId);
|
|
const viewModel = bcm?.viewModel;
|
|
if (viewModel?.keyDownHandler) {
|
|
const handledByBlock = viewModel.keyDownHandler(waveEvent);
|
|
if (handledByBlock) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function registerControlShiftStateUpdateHandler() {
|
|
getApi().onControlShiftStateUpdate((state: boolean) => {
|
|
if (state) {
|
|
setControlShift();
|
|
} else {
|
|
unsetControlShift();
|
|
}
|
|
});
|
|
}
|
|
|
|
function registerElectronReinjectKeyHandler() {
|
|
getApi().onReinjectKey((event: WaveKeyboardEvent) => {
|
|
appHandleKeyDown(event);
|
|
});
|
|
}
|
|
|
|
function tryReinjectKey(event: WaveKeyboardEvent): boolean {
|
|
return appHandleKeyDown(event);
|
|
}
|
|
|
|
function registerGlobalKeys() {
|
|
globalKeyMap.set("Cmd:]", () => {
|
|
switchTab(1);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Shift:Cmd:]", () => {
|
|
switchTab(1);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:[", () => {
|
|
switchTab(-1);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Shift:Cmd:[", () => {
|
|
switchTab(-1);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:n", () => {
|
|
handleCmdN();
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:i", () => {
|
|
handleCmdI();
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:t", () => {
|
|
createTab();
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:w", () => {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
genericClose(tabId);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:Shift:w", () => {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
const ws = globalStore.get(atoms.workspace);
|
|
if (ws.pinnedtabids?.includes(tabId)) {
|
|
// switch to first unpinned tab if it exists (for close spamming)
|
|
if (ws.tabids != null && ws.tabids.length > 0) {
|
|
getApi().setActiveTab(ws.tabids[0]);
|
|
}
|
|
return true;
|
|
}
|
|
getApi().closeTab(ws.oid, tabId);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:m", () => {
|
|
const layoutModel = getLayoutModelForStaticTab();
|
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
|
if (focusedNode != null) {
|
|
layoutModel.magnifyNodeToggle(focusedNode.id);
|
|
}
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Ctrl:Shift:ArrowUp", () => {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
switchBlockInDirection(tabId, NavigateDirection.Up);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Ctrl:Shift:ArrowDown", () => {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
switchBlockInDirection(tabId, NavigateDirection.Down);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Ctrl:Shift:ArrowLeft", () => {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
switchBlockInDirection(tabId, NavigateDirection.Left);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Ctrl:Shift:ArrowRight", () => {
|
|
const tabId = globalStore.get(atoms.staticTabId);
|
|
switchBlockInDirection(tabId, NavigateDirection.Right);
|
|
return true;
|
|
});
|
|
globalKeyMap.set("Cmd:g", () => {
|
|
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
|
|
if (bcm.openSwitchConnection != null) {
|
|
bcm.openSwitchConnection();
|
|
return true;
|
|
}
|
|
});
|
|
for (let idx = 1; idx <= 9; idx++) {
|
|
globalKeyMap.set(`Cmd:${idx}`, () => {
|
|
switchTabAbs(idx);
|
|
return true;
|
|
});
|
|
globalKeyMap.set(`Ctrl:Shift:c{Digit${idx}}`, () => {
|
|
switchBlockByBlockNum(idx);
|
|
return true;
|
|
});
|
|
globalKeyMap.set(`Ctrl:Shift:c{Numpad${idx}}`, () => {
|
|
switchBlockByBlockNum(idx);
|
|
return true;
|
|
});
|
|
}
|
|
const allKeys = Array.from(globalKeyMap.keys());
|
|
// special case keys, handled by web view
|
|
allKeys.push("Cmd:l", "Cmd:r", "Cmd:ArrowRight", "Cmd:ArrowLeft");
|
|
getApi().registerGlobalWebviewKeys(allKeys);
|
|
}
|
|
|
|
function getAllGlobalKeyBindings(): string[] {
|
|
const allKeys = Array.from(globalKeyMap.keys());
|
|
return allKeys;
|
|
}
|
|
|
|
// these keyboard events happen *anywhere*, even if you have focus in an input or somewhere else.
|
|
function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean {
|
|
for (const key of globalKeyMap.keys()) {
|
|
if (keyutil.checkKeyPressed(waveEvent, key)) {
|
|
const handler = globalKeyMap.get(key);
|
|
if (handler == null) {
|
|
return false;
|
|
}
|
|
return handler(waveEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
export {
|
|
appHandleKeyDown,
|
|
getAllGlobalKeyBindings,
|
|
getSimpleControlShiftAtom,
|
|
globalRefocus,
|
|
registerControlShiftStateUpdateHandler,
|
|
registerElectronReinjectKeyHandler,
|
|
registerGlobalKeys,
|
|
tryReinjectKey,
|
|
unsetControlShift,
|
|
};
|