Compare commits

...

12 Commits

Author SHA1 Message Date
Evan Simkowitz 669762a9c9 save work 2024-03-26 12:44:29 -07:00
Evan Simkowitz f975ecce48 Moving screen and session into workspaces package 2024-03-26 12:27:34 -07:00
Evan Simkowitz a121bd4bb5 Move bookmarks, history, playbook, and telemetry code out of sstore (#493)
* break out telemetry and playbook

* break out bookmarks

* add license disclaimers
2024-03-25 20:20:52 -07:00
dependabot[bot] dcc7b2943e Bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#490)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 09:55:15 -07:00
Cole Lashley bfa549ef22 Term keybindings (#482)
* added term keybindings

* removed logs and added a dump logs flag in keyutil

* un capture keys that we aren't using

* moved keybindings to inside terminal
2024-03-22 17:59:29 -07:00
Cole Lashley 376e339dfe Keybind UI utils (#487)
* added utils to get all of the keybinds and pretty print them, for display purposes

* added info section

* clarifying meta and alt behavior

* changed keybinduidescription to keybind config
2024-03-22 17:58:06 -07:00
Cole Lashley 646e260488 Code edit keybindings (#483)
* added codeedit keybindings

* removed logs

* added computed property to codeedit
2024-03-22 17:55:45 -07:00
Cole Lashley d3c48e3a3e Control keybindings (#489)
* fixed inline settings textedit and added datepicker keybindings

* added dropdown keybindings

* added observable to make sure that keybindings aren't double registered

* added enter and escape keybindings for datepicker

* dropdown closure fix
2024-03-22 17:53:27 -07:00
Cole Lashley c0c53edb84 Modal keybindings (#484)
* first draft at modal keybindings

* added modal keybindings and inline settings text edit keybindings

* added switch for keybindings in modal footer

* removed logs

* remove another console.log

* fix userinput keybindings -- should be generic:cancel for 2nd one
2024-03-21 23:10:02 -07:00
Cole Lashley 923cf71e0a Ai chat keybindings (#481)
* added aichat keybindings

* remove console.log
2024-03-21 18:30:05 -07:00
Cole Lashley f705a4df0a Textarea keybindings (#470)
* first commit for textarea keybindings

* added empty onkeydown to get around default behavior for now

* added history keybindings

* removed tab special case

* fix two small issues with keybindings
2024-03-21 18:26:21 -07:00
Cole Lashley 75a82de5bf Adding engineered query to chat command (#488)
* added engineered query to chat command

* let ai know responses are rendered in markdown
2024-03-21 18:00:55 -07:00
45 changed files with 3204 additions and 2559 deletions
+64 -2
View File
@@ -1,7 +1,8 @@
[
{
"command": "system:toggleDeveloperTools",
"keys": ["Cmd:Option:i"]
"keys": ["Cmd:Option:i"],
"info": "Opens the chrome developer tool menu"
},
{
"command": "system:hideWindow",
@@ -15,10 +16,62 @@
"command": "generic:confirm",
"keys": ["Enter"]
},
{
"command": "generic:expandTextInput",
"keys": ["Shift:Enter", "Ctrl:Enter"]
},
{
"command": "generic:deleteItem",
"keys": ["Backspace", "Delete"]
},
{
"command": "generic:space",
"keys": ["Space"]
},
{
"command": "generic:tab",
"keys": ["Tab"]
},
{
"command": "generic:numpad-0",
"keys": ["0"]
},
{
"command": "generic:numpad-1",
"keys": ["1"]
},
{
"command": "generic:numpad-2",
"keys": ["2"]
},
{
"command": "generic:numpad-3",
"keys": ["3"]
},
{
"command": "generic:numpad-4",
"keys": ["4"]
},
{
"command": "generic:numpad-5",
"keys": ["5"]
},
{
"command": "generic:numpad-6",
"keys": ["6"]
},
{
"command": "generic:numpad-7",
"keys": ["7"]
},
{
"command": "generic:numpad-8",
"keys": ["8"]
},
{
"command": "generic:numpad-9",
"keys": ["9"]
},
{
"command": "generic:selectAbove",
"keys": ["ArrowUp"]
@@ -27,6 +80,14 @@
"command": "generic:selectBelow",
"keys": ["ArrowDown"]
},
{
"command": "generic:selectLeft",
"keys": ["ArrowLeft"]
},
{
"command": "generic:selectRight",
"keys": ["ArrowRight"]
},
{
"command": "generic:selectPageAbove",
"keys": ["PageUp"]
@@ -67,7 +128,8 @@
},
{
"command": "app:restartCommand",
"keys": ["Cmd:r"]
"keys": ["Cmd:r"],
"info": "Restarts the command running in the current selected line"
},
{
"command": "app:restartLastCommand",
+89 -36
View File
@@ -1,9 +1,12 @@
import React, { useState, useEffect, useRef, createRef } from "react";
import * as mobx from "mobx";
import ReactDOM from "react-dom";
import dayjs from "dayjs";
import cs from "classnames";
import { Button } from "@/elements";
import { If } from "tsx-control-statements/components";
import { GlobalModel } from "@/models";
import { v4 as uuidv4 } from "uuid";
import "./datepicker.less";
@@ -36,6 +39,10 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
MM: selDate.format("MM"),
DD: selDate.format("DD"),
});
let curUuid = uuidv4();
let keybindsRegistered = mobx.observable.box(false, {
name: "datepicker-keybinds-registered",
});
useEffect(() => {
inputRefs.current = {
@@ -273,6 +280,11 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
setShowYearAccordion(false);
};
const closeModal = () => {
setIsOpen(false);
setShowYearAccordion(false);
};
const dayPickerModal = isOpen
? ReactDOM.createPortal(
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
@@ -300,13 +312,13 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
}
};
const handleArrowNavigation = (event, currentPart) => {
const handleArrowNavigation = (key, currentPart) => {
const currentIndex = formatParts.indexOf(currentPart);
let targetInput;
if (event.key === "ArrowLeft" && currentIndex > 0) {
if (key == "ArrowLeft" && currentIndex > 0) {
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
} else if (event.key === "ArrowRight" && currentIndex < formatParts.length - 1) {
} else if (key == "ArrowRight" && currentIndex < formatParts.length - 1) {
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
}
@@ -315,51 +327,91 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
}
};
const handleKeyDown = (event, currentPart) => {
const key = event.key;
const handleKeyDown = (event, currentPart) => {};
if (key === "ArrowLeft" || key === "ArrowRight") {
// Handle arrow navigation without selecting text
handleArrowNavigation(event, currentPart);
const handleFocus = (event, part) => {
event.target.select();
registerKeybindings(event, part);
};
const registerKeybindings = (event: any, part: string) => {
if (keybindsRegistered.get() == true) {
return;
}
if (key === " ") {
// Handle spacebar press to toggle the modal
mobx.action(() => {
keybindsRegistered.set(true);
})();
let keybindManager = GlobalModel.keybindManager;
let domain = "datepicker-" + curUuid + "-" + part;
keybindManager.registerKeybinding("control", domain, "generic:selectLeft", (waveEvent) => {
handleArrowNavigation("ArrowLeft", part);
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:selectRight", (waveEvent) => {
handleArrowNavigation("ArrowRight", part);
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:space", (waveEvent) => {
toggleModal();
return;
}
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
toggleModal();
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
closeModal();
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:tab", (waveEvent) => {
handleArrowNavigation("ArrowRight", part);
return true;
});
for (let numpadKey = 0; numpadKey <= 9; numpadKey++) {
keybindManager.registerKeybinding(
"control",
domain,
"generic:numpad-" + numpadKey.toString(),
(waveEvent) => {
let currentPart = part;
const maxLength = currentPart === "YYYY" ? 4 : 2;
const newValue = event.target.value.length < maxLength ? event.target.value + numpadKey : numpadKey;
let selectionTimeoutId = null;
handleDatePartChange(currentPart, newValue);
if (key.match(/[0-9]/)) {
// Handle numeric keys
event.preventDefault();
const maxLength = currentPart === "YYYY" ? 4 : 2;
const newValue = event.target.value.length < maxLength ? event.target.value + key : key;
let selectionTimeoutId = null;
handleDatePartChange(currentPart, newValue);
// Clear any existing timeout
if (selectionTimeoutId !== null) {
clearTimeout(selectionTimeoutId);
}
// Clear any existing timeout
if (selectionTimeoutId !== null) {
clearTimeout(selectionTimeoutId);
}
// Re-focus and select the input after state update
selectionTimeoutId = setTimeout(() => {
event.target.focus();
event.target.select();
}, 0);
// Re-focus and select the input after state update
selectionTimeoutId = setTimeout(() => {
event.target.focus();
event.target.select();
}, 0);
return true;
}
);
}
};
const handleFocus = (event) => {
event.target.select();
const handleBlur = (event, part) => {
unregisterKeybindings(part);
};
const unregisterKeybindings = (part) => {
mobx.action(() => {
keybindsRegistered.set(false);
})();
let domain = "datepicker-" + curUuid + "-" + part;
GlobalModel.keybindManager.unregisterDomain(domain);
};
// Prevent use from selecting text in the input
const handleMouseDown = (event) => {
const handleMouseDown = (event, part) => {
event.preventDefault();
handleFocus(event);
handleFocus(event, part);
};
const handleIconKeyDown = (event) => {
@@ -413,8 +465,9 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
value={dateParts[part]}
onChange={(e) => handleDatePartChange(part, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, part)}
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onMouseDown={(e) => handleMouseDown(e, part)}
onFocus={(e) => handleFocus(e, part)}
onBlur={(e) => handleBlur(e, part)}
maxLength={part === "YYYY" ? 4 : 2}
className="date-input"
placeholder={part}
+73 -41
View File
@@ -7,6 +7,8 @@ import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import { v4 as uuidv4 } from "uuid";
import { GlobalModel } from "@/models";
import "./dropdown.less";
@@ -39,6 +41,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
wrapperRef: React.RefObject<HTMLDivElement>;
menuRef: React.RefObject<HTMLDivElement>;
timeoutId: any;
curUuid: string;
constructor(props: DropdownProps) {
super(props);
@@ -50,6 +53,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
};
this.wrapperRef = React.createRef();
this.menuRef = React.createRef();
this.curUuid == uuidv4();
}
componentDidMount() {
@@ -102,49 +106,76 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
@boundMethod
handleFocus() {
this.setState({ isTouched: true });
this.registerKeybindings();
}
registerKeybindings() {
let keybindManager = GlobalModel.keybindManager;
let domain = "dropdown-" + this.curUuid;
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
this.handleConfirm();
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:space", (waveEvent) => {
this.handleConfirm();
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
this.setState({ isOpen: false });
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:selectAbove", (waveEvent) => {
const { isOpen } = this.state;
const { options } = this.props;
if (isOpen) {
this.setState((prevState) => ({
highlightedIndex:
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
}));
}
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:selectBelow", (waveEvent) => {
const { isOpen } = this.state;
const { options } = this.props;
if (isOpen) {
this.setState((prevState) => ({
highlightedIndex:
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
}));
}
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:tab", (waveEvent) => {
this.setState({ isOpen: false });
return true;
});
}
handleConfirm() {
const { options } = this.props;
const { isOpen, highlightedIndex } = this.state;
if (isOpen) {
const option = options[highlightedIndex];
if (option) {
this.handleSelect(option.value, undefined);
}
} else {
this.toggleDropdown();
}
}
handleBlur() {
this.unregisterKeybindings();
}
unregisterKeybindings() {
let domain = "dropdown-" + this.curUuid;
GlobalModel.keybindManager.unregisterDomain(domain);
}
@boundMethod
handleKeyDown(event: React.KeyboardEvent) {
const { options } = this.props;
const { isOpen, highlightedIndex } = this.state;
switch (event.key) {
case "Enter":
case " ":
if (isOpen) {
const option = options[highlightedIndex];
if (option) {
this.handleSelect(option.value, undefined);
}
} else {
this.toggleDropdown();
}
break;
case "Escape":
this.setState({ isOpen: false });
break;
case "ArrowUp":
if (isOpen) {
this.setState((prevState) => ({
highlightedIndex:
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
}));
}
break;
case "ArrowDown":
if (isOpen) {
this.setState((prevState) => ({
highlightedIndex:
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
}));
}
break;
case "Tab":
this.setState({ isOpen: false });
break;
}
}
handleKeyDown(event: React.KeyboardEvent) {}
@boundMethod
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
@@ -228,7 +259,8 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
tabIndex={0}
onKeyDown={this.handleKeyDown}
onClick={this.handleClick}
onFocus={this.handleFocus}
onFocus={this.handleFocus.bind(this)}
onBlur={this.handleBlur.bind(this)}
>
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
<If condition={label}>
@@ -8,6 +8,8 @@ import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { GlobalModel } from "@/models";
import { v4 as uuidv4 } from "uuid";
import "./inlinesettingstextedit.less";
@@ -27,6 +29,11 @@ class InlineSettingsTextEdit extends React.Component<
tempText: OV<string>;
shouldFocus: boolean = false;
inputRef: React.RefObject<any> = React.createRef();
curId: string;
componentDidMount(): void {
this.curId = uuidv4();
}
componentDidUpdate(): void {
if (this.shouldFocus) {
@@ -52,6 +59,7 @@ class InlineSettingsTextEdit extends React.Component<
this.tempText = null;
this.props.onChange(newText);
})();
this.unregisterKeybindings();
}
@boundMethod
@@ -60,24 +68,38 @@ class InlineSettingsTextEdit extends React.Component<
this.isEditing.set(false);
this.tempText = null;
})();
this.unregisterKeybindings();
}
@boundMethod
handleKeyDown(e: any): void {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
e.stopPropagation();
handleFocus() {
this.registerKeybindings();
}
registerKeybindings() {
let keybindManager = GlobalModel.keybindManager;
let domain = "inline-settings" + this.curId;
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
this.confirmChange();
return;
}
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
e.stopPropagation();
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
this.cancelChange();
return;
}
return;
return true;
});
}
unregisterKeybindings() {
let domain = "inline-settings" + this.curId;
GlobalModel.keybindManager.unregisterDomain(domain);
}
handleBlur() {
this.unregisterKeybindings();
this.cancelChange();
}
componentWillUnmount(): void {
this.unregisterKeybindings();
}
@boundMethod
@@ -99,7 +121,8 @@ class InlineSettingsTextEdit extends React.Component<
ref={this.inputRef}
className="input"
type="text"
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus.bind(this)}
onBlur={this.handleBlur.bind(this)}
placeholder={this.props.placeholder}
onChange={this.handleChangeText}
value={this.tempText.get()}
+47 -2
View File
@@ -6,8 +6,11 @@ import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import ReactDOM from "react-dom";
import { Button } from "./button";
import { v4 as uuidv4 } from "uuid";
import { GlobalModel } from "@/models";
import "./modal.less";
import { boundMethod } from "autobind-decorator";
interface ModalHeaderProps {
onClose?: () => void;
@@ -30,10 +33,52 @@ interface ModalFooterProps {
onOk?: () => void;
cancelLabel?: string;
okLabel?: string;
keybindings?: boolean;
}
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
class ModalKeybindings extends React.Component<{ onOk; onCancel }, {}> {
curId: string;
@boundMethod
componentDidMount(): void {
this.curId = uuidv4();
let domain = "modal-" + this.curId;
let keybindManager = GlobalModel.keybindManager;
if (this.props.onOk) {
keybindManager.registerKeybinding("modal", domain, "generic:confirm", (waveEvent) => {
this.props.onOk();
return true;
});
}
if (this.props.onCancel) {
keybindManager.registerKeybinding("modal", domain, "generic:cancel", (waveEvent) => {
this.props.onCancel();
return true;
});
}
}
@boundMethod
componentWillUnmount(): void {
GlobalModel.keybindManager.unregisterDomain("modal-" + this.curId);
}
render(): React.ReactNode {
return null;
}
}
const ModalFooter: React.FC<ModalFooterProps> = ({
onCancel,
onOk,
cancelLabel = "Cancel",
okLabel = "Ok",
keybindings = true,
}) => (
<div className="wave-modal-footer">
<If condition={keybindings}>
<ModalKeybindings onOk={onOk} onCancel={onCancel}></ModalKeybindings>
</If>
{onCancel && (
<Button className="secondary" onClick={onCancel}>
{cancelLabel}
@@ -79,4 +124,4 @@ class Modal extends React.Component<ModalProps> {
}
}
export { Modal };
export { Modal, ModalKeybindings };
+8
View File
@@ -18,6 +18,8 @@ interface TextFieldProps {
className?: string;
onChange?: (value: string) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
onFocus?: () => void;
onBlur?: () => void;
placeholder?: string;
defaultValue?: string;
decoration?: TextFieldDecorationProps;
@@ -78,6 +80,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
@boundMethod
handleFocus() {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus();
}
}
@boundMethod
@@ -91,6 +96,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
this.setState({ error: false, focused: false });
}
}
if (this.props.onBlur) {
this.props.onBlur();
}
}
@boundMethod
+3
View File
@@ -9,6 +9,7 @@ import { Markdown, Modal, Button, Checkbox } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import "./alert.less";
import { ModalKeybindings } from "../elements/modal";
@mobxReact.observer
class AlertModal extends React.Component<{}, {}> {
@@ -54,6 +55,7 @@ class AlertModal extends React.Component<{}, {}> {
</div>
<div className="wave-modal-footer">
<If condition={isConfirm}>
<ModalKeybindings onOk={this.handleOK} onCancel={this.closeModal}></ModalKeybindings>
<Button className="secondary" onClick={this.closeModal}>
Cancel
</Button>
@@ -62,6 +64,7 @@ class AlertModal extends React.Component<{}, {}> {
</Button>
</If>
<If condition={!isConfirm}>
<ModalKeybindings onOk={this.handleOK} onCancel={null}></ModalKeybindings>
<Button autoFocus={true} onClick={this.handleOK}>
Ok
</Button>
+17 -12
View File
@@ -2,7 +2,7 @@ import * as React from "react";
import { GlobalModel } from "@/models";
import { Choose, When, If } from "tsx-control-statements/components";
import { Modal, PasswordField, TextField, Markdown, Checkbox } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, KeybindManager } from "@/util/keyutil";
import "./userinput.less";
@@ -44,17 +44,20 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
[userInputRequest]
);
function handleTextKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
e.stopPropagation();
function handleTextFocus() {
let keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("modal", "userinput", "generic:confirm", (waveEvent) => {
handleSendText();
} else if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
e.stopPropagation();
return true;
});
keybindManager.registerKeybinding("modal", "userinput", "generic:cancel", (waveEvent) => {
handleSendCancel();
}
return true;
});
}
function handleTextBlur() {
GlobalModel.keybindManager.unregisterDomain("userinput");
}
React.useEffect(() => {
@@ -89,7 +92,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
value={responseText}
maxLength={400}
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
onFocus={() => handleTextFocus()}
onBlur={() => handleTextBlur()}
/>
</If>
<If condition={!userInputRequest.publictext}>
@@ -98,7 +102,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
value={responseText}
maxLength={400}
autoFocus={true}
onKeyDown={(e) => handleTextKeyDown(e)}
onFocus={() => handleTextFocus()}
onBlur={() => handleTextBlur()}
/>
</If>
</If>
@@ -14,6 +14,7 @@ import * as textmeasure from "@/util/textmeasure";
import * as appconst from "@/app/appconst";
import "./viewremoteconndetail.less";
import { ModalKeybindings } from "../elements/modal";
@mobxReact.observer
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
@@ -382,6 +383,20 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
</div>
</div>
<div className="wave-modal-footer">
<ModalKeybindings
onOk={() => {
if (selectedRemoteStatus == "connecting") {
return;
}
this.handleClose();
}}
onCancel={() => {
if (selectedRemoteStatus == "connecting") {
return;
}
this.handleClose();
}}
></ModalKeybindings>
<Button
className="secondary"
disabled={selectedRemoteStatus == "connecting"}
+123 -58
View File
@@ -8,21 +8,64 @@ import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { For } from "tsx-control-statements/components";
import { If, For } from "tsx-control-statements/components";
import { Markdown } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
componentDidMount(): void {
let AIChatObject = this.props.AIChatObject;
let keybindManager = GlobalModel.keybindManager;
let inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
AIChatObject.onEnterKeyPressed();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:expandTextInput", (waveEvent) => {
AIChatObject.onExpandInputPressed();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
inputModel.closeAIAssistantChat(true);
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
inputModel.clearAIAssistantChat();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
return AIChatObject.onArrowUpPressed();
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
return AIChatObject.onArrowDownPressed();
});
}
componentWillUnmount(): void {
GlobalModel.keybindManager.unregisterDomain("aichat");
}
render() {
return null;
}
}
@mobxReact.observer
class AIChat extends React.Component<{}, {}> {
chatListKeyCount: number = 0;
textAreaNumLines: mobx.IObservableValue<number> = mobx.observable.box(1, { name: "textAreaNumLines" });
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
isFocused: OV<boolean>;
constructor(props: any) {
super(props);
this.chatWindowScrollRef = React.createRef();
this.textAreaRef = React.createRef();
this.isFocused = mobx.observable.box(false, {
name: "aichat-isfocused",
});
}
componentDidMount() {
@@ -66,67 +109,82 @@ class AIChat extends React.Component<{}, {}> {
return { numLines, linePos };
}
@mobx.action
@boundMethod
onKeyDown(e: any) {
onTextAreaFocused(e: any) {
mobx.action(() => {
let model = GlobalModel;
let inputModel = model.inputModel;
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
let resetCodeSelect = !ctrlMod;
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
if (!ctrlMod) {
if (inputModel.getCodeSelectSelectedIndex() == -1) {
let messageStr = e.target.value;
this.submitChatMessage(messageStr);
e.target.value = "";
} else {
inputModel.grabCodeSelectSelection();
}
} else {
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
}
}
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
e.stopPropagation();
inputModel.closeAIAssistantChat(true);
}
if (checkKeyPressed(waveEvent, "Ctrl:l")) {
e.preventDefault();
e.stopPropagation();
inputModel.clearAIAssistantChat();
}
if (checkKeyPressed(waveEvent, "ArrowUp")) {
if (this.getLinePos(e.target).linePos > 1) {
// normal up arrow
return;
}
e.preventDefault();
inputModel.codeSelectSelectNextOldestCodeBlock();
resetCodeSelect = false;
}
if (checkKeyPressed(waveEvent, "ArrowDown")) {
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
return;
}
e.preventDefault();
inputModel.codeSelectSelectNextNewestCodeBlock();
resetCodeSelect = false;
}
if (resetCodeSelect) {
inputModel.codeSelectDeselectAll();
}
// set height of textarea based on number of newlines
this.textAreaNumLines.set(e.target.value.split(/\n/).length);
this.isFocused.set(true);
})();
}
onTextAreaBlur(e: any) {
mobx.action(() => {
this.isFocused.set(false);
})();
}
onTextAreaChange(e: any) {
// set height of textarea based on number of newlines
mobx.action(() => {
this.textAreaNumLines.set(e.target.value.split(/\n/).length);
GlobalModel.inputModel.codeSelectDeselectAll();
})();
}
onEnterKeyPressed() {
let inputModel = GlobalModel.inputModel;
let currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
if (inputModel.getCodeSelectSelectedIndex() == -1) {
let messageStr = currentRef.value;
this.submitChatMessage(messageStr);
currentRef.value = "";
} else {
inputModel.grabCodeSelectSelection();
}
}
onExpandInputPressed() {
let currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
GlobalModel.inputModel.codeSelectDeselectAll();
}
onArrowUpPressed(): boolean {
let currentRef = this.textAreaRef.current;
if (currentRef == null) {
return false;
}
if (this.getLinePos(currentRef).linePos > 1) {
// normal up arrow
GlobalModel.inputModel.codeSelectDeselectAll();
return false;
}
GlobalModel.inputModel.codeSelectSelectNextOldestCodeBlock();
return true;
}
onArrowDownPressed(): boolean {
let currentRef = this.textAreaRef.current;
let inputModel = GlobalModel.inputModel;
if (currentRef == null) {
return false;
}
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
GlobalModel.inputModel.codeSelectDeselectAll();
return false;
}
inputModel.codeSelectSelectNextNewestCodeBlock();
return true;
}
@mobx.action
@boundMethod
onKeyDown(e: any) {}
renderError(err: string): any {
return <div className="chat-msg-error">{err}</div>;
}
@@ -192,9 +250,13 @@ class AIChat extends React.Component<{}, {}> {
const textAreaPadding = 2 * 0.5 * termFontSize;
let textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines + textAreaPadding;
let textAreaInnerHeight = this.textAreaNumLines.get() * textAreaLineHeight + textAreaPadding;
let isFocused = this.isFocused.get();
return (
<div className="cmd-aichat">
<If condition={isFocused}>
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
</If>
<div className="cmdinput-titlebar">
<div className="title-icon">
<i className="fa-sharp fa-solid fa-sparkles" />
@@ -217,6 +279,9 @@ class AIChat extends React.Component<{}, {}> {
autoComplete="off"
autoCorrect="off"
id="chat-cmd-input"
onFocus={this.onTextAreaFocused.bind(this)}
onBlur={this.onTextAreaBlur.bind(this)}
onChange={this.onTextAreaChange.bind(this)}
onKeyDown={this.onKeyDown}
style={{ height: textAreaInnerHeight, maxHeight: textAreaMaxHeight, fontSize: termFontSize }}
className={cn("chat-textarea")}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -630,6 +630,9 @@ class InputModel {
}
codeSelectDeselectAll(direction: number = this.codeSelectBottom) {
if (this.codeSelectSelectedIndex.get() == direction) {
return;
}
mobx.action(() => {
this.codeSelectSelectedIndex.set(direction);
this.codeSelectBlockRefArray = [];
-17
View File
@@ -476,23 +476,6 @@ class Model {
if (isModKeyPress(e)) {
return;
}
if (this.alertMessage.get() != null) {
if (checkKeyPressed(waveEvent, "Escape")) {
e.preventDefault();
this.modalsModel.popModal(() => this.cancelAlert());
return;
}
if (checkKeyPressed(waveEvent, "Enter")) {
e.preventDefault();
this.confirmAlert();
return;
}
return;
}
if (checkKeyPressed(waveEvent, "Escape") && this.modalsModel.store.length > 0) {
this.modalsModel.popModal();
return;
}
if (this.keybindManager.processKeyEvent(e, waveEvent)) {
return;
}
+4 -67
View File
@@ -12,7 +12,7 @@ import { MagicLayout } from "@/app/magiclayout";
import * as appconst from "@/app/appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { Model } from "./model";
import { GlobalCommandRunner } from "./global";
import { GlobalCommandRunner, GlobalModel } from "./global";
import { Cmd } from "./cmd";
import { ScreenLines } from "./screenlines";
import { getTermPtyData } from "@/util/modelutil";
@@ -469,7 +469,7 @@ class Screen {
this.renderers[lineId] = renderer;
}
setLineFocus(lineNum: number, focus: boolean): void {
setLineFocus(lineNum: number, lineid: string, focus: boolean): void {
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
if (focus && this.selectedLine.get() != lineNum) {
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
@@ -498,71 +498,8 @@ class Screen {
})();
}
termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (checkKeyPressed(waveEvent, "ArrowUp")) {
termWrap.terminal.scrollLines(-1);
return;
}
if (checkKeyPressed(waveEvent, "ArrowDown")) {
termWrap.terminal.scrollLines(1);
return;
}
if (checkKeyPressed(waveEvent, "PageUp")) {
termWrap.terminal.scrollPages(-1);
return;
}
if (checkKeyPressed(waveEvent, "PageDown")) {
termWrap.terminal.scrollPages(1);
return;
}
}
isTermCapturedKey(e: any): boolean {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (
checkKeyPressed(waveEvent, "ArrowUp") ||
checkKeyPressed(waveEvent, "ArrowDown") ||
checkKeyPressed(waveEvent, "PageUp") ||
checkKeyPressed(waveEvent, "PageDown")
) {
return true;
}
return false;
}
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
e.stopPropagation();
e.preventDefault();
let sel = termWrap.terminal.getSelection();
navigator.clipboard.writeText(sel);
return false;
}
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
e.stopPropagation();
e.preventDefault();
let p = navigator.clipboard.readText();
p.then((text) => {
termWrap.dataHandler?.(text, termWrap);
});
return false;
}
if (termWrap.isRunning) {
return true;
}
let isCaptured = this.isTermCapturedKey(e);
if (!isCaptured) {
return true;
}
if (e.type != "keydown" || isModKeyPress(e)) {
return false;
}
e.stopPropagation();
e.preventDefault();
this.termCustomKeyHandlerInternal(e, termWrap);
return false;
return true;
}
loadTerminalRenderer(elem: Element, line: LineType, cmd: Cmd, width: number) {
@@ -588,7 +525,7 @@ class Screen {
termOpts: cmd.getTermOpts(),
winSize: { height: 0, width: width },
dataHandler: cmd.handleData.bind(cmd),
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, focus),
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, line.lineid, focus),
isRunning: cmd.isRunning(),
customKeyHandler: this.termCustomKeyHandler.bind(this),
fontSize: this.globalModel.getTermFontSize(),
+68 -15
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import Editor, { Monaco } from "@monaco-editor/react";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import cn from "classnames";
@@ -10,7 +11,12 @@ import { Markdown } from "@/elements";
import { GlobalModel, GlobalCommandRunner } from "@/models";
import Split from "react-split-it";
import loader from "@monaco-editor/loader";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import {
checkKeyPressed,
adaptFromReactOrNativeKeyEvent,
KeybindManager,
adaptFromElectronKeyEvent,
} from "@/util/keyutil";
import { Button, Dropdown } from "@/elements";
import "./code.less";
@@ -46,6 +52,18 @@ function renderCmdText(text: string): any {
// there is a global monaco variable (TODO get the correct TS type)
declare var monaco: any;
class CodeKeybindings extends React.Component<{ codeObject: SourceCodeRenderer }, {}> {
componentDidMount(): void {
this.props.codeObject.registerKeybindings();
}
componentWillUnmount(): void {
this.props.codeObject.unregisterKeybindings();
}
render() {
return null;
}
}
class SourceCodeRenderer extends React.Component<
{
data: ExtBlob;
@@ -131,6 +149,10 @@ class SourceCodeRenderer extends React.Component<
}
}
componentWillUnmount() {
this.unregisterKeybindings();
}
componentDidUpdate(prevProps: any): void {
if (!prevProps.shouldFocus && this.props.shouldFocus) {
if (this.monacoEditor) {
@@ -174,6 +196,30 @@ class SourceCodeRenderer extends React.Component<
}
};
registerKeybindings() {
const { lineId } = this.props.context;
let domain = "code-" + lineId;
let keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("plugin", domain, "codeedit:save", (waveEvent) => {
this.doSave();
return true;
});
keybindManager.registerKeybinding("plugin", domain, "codeedit:close", (waveEvent) => {
this.doClose();
return true;
});
keybindManager.registerKeybinding("plugin", domain, "codeedit:togglePreview", (waveEvent) => {
this.togglePreview();
return true;
});
}
unregisterKeybindings() {
const { lineId } = this.props.context;
let domain = "code-" + lineId;
GlobalModel.keybindManager.unregisterDomain(domain);
}
handleEditorDidMount = (editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
this.monacoEditor = editor;
this.setInitialLanguage(editor);
@@ -184,20 +230,15 @@ class SourceCodeRenderer extends React.Component<
}, 2000);
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
let waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
if (checkKeyPressed(waveEvent, "Cmd:s") && this.state.isSave) {
e.preventDefault();
e.stopPropagation();
this.doSave();
}
if (checkKeyPressed(waveEvent, "Cmd:d")) {
e.preventDefault();
e.stopPropagation();
this.doClose();
}
if (checkKeyPressed(waveEvent, "Cmd:p")) {
e.preventDefault();
e.stopPropagation();
this.togglePreview();
console.log("keydown?", waveEvent);
if (
GlobalModel.keybindManager.checkKeysPressed(waveEvent, [
"codeedit:save",
"codeedit:close",
"codeedit:togglePreview",
])
) {
GlobalModel.keybindManager.processKeyEvent(e.browserEvent, waveEvent);
}
});
editor.onDidScrollChange((e) => {
@@ -503,8 +544,20 @@ class SourceCodeRenderer extends React.Component<
</div>
);
}
let { lineNum } = this.props.context;
let screen = GlobalModel.getActiveScreen();
let lineIsSelected = mobx.computed(
() => screen.getSelectedLine() == lineNum && screen.getFocusType() == "cmd",
{
name: "code-lineisselected",
}
);
console.log("lineis selected:", lineIsSelected.get());
return (
<div className="code-renderer">
<If condition={lineIsSelected.get()}>
<CodeKeybindings codeObject={this}></CodeKeybindings>
</If>
<Split sizes={[editorFraction, 1 - editorFraction]} onSetSizes={this.setSizes}>
{this.getCodeEditor()}
{isPreviewerAvailable && showPreview && this.getPreviewer()}
+1 -1
View File
@@ -148,7 +148,7 @@ class TermWrap {
});
this.terminal.open(elem);
if (opts.keyHandler != null) {
this.terminal.onKey((e) => opts.keyHandler(e, this));
//this.terminal.onKey((e) => opts.keyHandler(e, this));
}
if (opts.dataHandler != null) {
this.dataHandler = opts.dataHandler;
+59
View File
@@ -17,6 +17,60 @@ import "./terminal.less";
dayjs.extend(localizedFormat);
class TerminalKeybindings extends React.Component<{ termWrap: any; lineid: string }, {}> {
componentDidMount(): void {
this.registerKeybindings();
}
registerKeybindings() {
let keybindManager = GlobalModel.keybindManager;
let domain = "line-" + this.props.lineid;
let termWrap = this.props.termWrap;
keybindManager.registerKeybinding("plugin", domain, "terminal:copy", (waveEvent) => {
let termWrap = this.props.termWrap;
let sel = termWrap.terminal.getSelection();
navigator.clipboard.writeText(sel);
return true;
});
keybindManager.registerKeybinding("plugin", domain, "terminal:paste", (waveEvent) => {
let p = navigator.clipboard.readText();
p.then((text) => {
termWrap.dataHandler?.(text, termWrap);
});
return true;
});
keybindManager.registerKeybinding("plugin", domain, "generic:selectAbove", (waveEvent) => {
termWrap.terminal.scrollLines(-1);
return true;
});
keybindManager.registerKeybinding("plugin", domain, "generic:selectBelow", (waveEvent) => {
termWrap.terminal.scrollLines(1);
return true;
});
keybindManager.registerKeybinding("plugin", domain, "generic:selectPageAbove", (waveEvent) => {
termWrap.terminal.scrollLines(-10);
return true;
});
keybindManager.registerKeybinding("plugin", domain, "generic:selectPageBelow", (waveEvent) => {
termWrap.terminal.scrollLines(10);
return true;
});
}
unregisterKeybindings() {
let domain = "line-" + this.props.lineid;
GlobalModel.keybindManager.unregisterDomain(domain);
}
componentWillUnmount(): void {
this.unregisterKeybindings();
}
render(): React.ReactNode {
return null;
}
}
@mobxReact.observer
class TerminalRenderer extends React.Component<
{
@@ -153,6 +207,8 @@ class TerminalRenderer extends React.Component<
termHeight = 0;
}
let termLoaded = this.termLoaded.get();
let lineid = line.lineid;
let termWrap = screen.getTermWrap(lineid);
return (
<div
ref={this.elemRef}
@@ -169,6 +225,9 @@ class TerminalRenderer extends React.Component<
<If condition={!isFocused}>
<div key="term-block" className="term-block" onClick={this.clickTermBlock}></div>
</If>
<If condition={isFocused}>
<TerminalKeybindings termWrap={termWrap} lineid={lineid}></TerminalKeybindings>
</If>
<div
key="term-connectelem"
className="terminal-connectelem"
+104 -8
View File
@@ -25,10 +25,11 @@ const KeyTypeCode = "code";
type KeybindCallback = (event: WaveKeyboardEvent) => boolean;
type KeybindConfigArray = Array<KeybindConfig>;
type KeybindConfig = { command: string; keys: Array<string>; commandStr?: string };
type KeybindConfig = { command: string; keys: Array<string>; commandStr?: string; info?: string };
const Callback = "callback";
const Command = "command";
const DumpLogs = false;
type Keybind = {
domain: string;
@@ -38,7 +39,7 @@ type Keybind = {
commandStr: string;
};
const KeybindLevels = ["system", "modal", "app", "mainview", "pane", "plugin"];
const KeybindLevels = ["system", "modal", "app", "mainview", "pane", "plugin", "control"];
class KeybindManager {
domainCallbacks: Map<string, KeybindCallback>;
@@ -93,6 +94,7 @@ class KeybindManager {
throw new Error("invalid keybind key");
}
}
// if user doesn't specify a command string or a description, we will revert to the old one
let defaultCmd = this.keyDescriptionsMap.get(curKeybind.command);
if (
defaultCmd != null &&
@@ -101,6 +103,13 @@ class KeybindManager {
) {
curKeybind.commandStr = this.keyDescriptionsMap.get(curKeybind.command).commandStr;
}
if (
defaultCmd != null &&
defaultCmd.info != null &&
(curKeybind.info == null || curKeybind.info == "")
) {
curKeybind.info = this.keyDescriptionsMap.get(curKeybind.command).info;
}
newKeyDescriptions.set(curKeybind.command, curKeybind);
}
} catch (e) {
@@ -114,6 +123,83 @@ class KeybindManager {
this.keyDescriptionsMap = newKeyDescriptions;
}
prettyPrintKeybind(keyDescription: string): string {
let keyPress = parseKeyDescription(keyDescription);
let returnString = "";
if (keyPress.mods.Cmd) {
returnString += "⌘";
}
if (keyPress.mods.Ctrl) {
returnString += "⌃";
}
if (keyPress.mods.Option) {
returnString += "⌥";
}
if (keyPress.mods.Shift) {
returnString += "⇧";
}
if (keyPress.mods.Meta) {
returnString += "M";
}
if (keyPress.mods.Alt) {
returnString += "⌥";
}
returnString += keyPress.key;
return returnString;
}
getUIDescription(keyDescription: string, prettyPrint: boolean = true): KeybindConfig {
let keybinds = this.getKeybindsFromDescription(keyDescription, prettyPrint);
if (!this.keyDescriptionsMap.has(keyDescription)) {
return { keys: keybinds, info: "", command: keyDescription, commandStr: "" };
}
let curKeybindConfig = this.keyDescriptionsMap.get(keyDescription);
let curInfo = "";
if (curKeybindConfig.info) {
curInfo = curKeybindConfig.info;
}
let curCommandStr = "";
if (curKeybindConfig.commandStr) {
curCommandStr = curKeybindConfig.commandStr;
}
return { keys: keybinds, info: curInfo, commandStr: curCommandStr, command: keyDescription };
}
getKeybindsFromDescription(keyDescription: string, prettyPrint: boolean = true): Array<string> {
if (!this.keyDescriptionsMap.has(keyDescription)) {
return [];
}
let keyBinds = this.keyDescriptionsMap.get(keyDescription).keys;
if (!prettyPrint) {
return keyBinds;
}
let keybindsArray = [];
for (let index = 0; index < keyBinds.length; index++) {
let curKeybind = keyBinds[index];
let curPrettyPrintString = this.prettyPrintKeybind(curKeybind);
keybindsArray.push(curPrettyPrintString);
}
return keybindsArray;
}
getAllKeybindUIDescriptions(prettyPrint: boolean = true): KeybindConfigArray {
let keybindsList = [];
let keybindDescriptions = this.keyDescriptionsMap.keys();
for (let keyDesc of keybindDescriptions) {
keybindsList.push(this.getUIDescription(keyDesc, prettyPrint));
}
return keybindsList;
}
getAllKeybinds(prettyPrint: boolean = true): Array<Array<string>> {
let keybindsList = [];
let keybindDescriptions = this.keyDescriptionsMap.keys();
for (let keyDesc of keybindDescriptions) {
keybindsList.push(this.getKeybindsFromDescription(keyDesc, prettyPrint));
}
return keybindsList;
}
runSlashCommand(curKeybind: Keybind): boolean {
let curConfigKeybind = this.keyDescriptionsMap.get(curKeybind.keybinding);
if (curConfigKeybind == null || curConfigKeybind.commandStr == null || curKeybind.commandStr == "") {
@@ -147,6 +233,9 @@ class KeybindManager {
for (let index = keybindsArray.length - 1; index >= 0; index--) {
let curKeybind = keybindsArray[index];
if (this.checkKeyPressed(event, curKeybind.keybinding)) {
if (DumpLogs) {
console.log("keybind found", curKeybind);
}
let shouldReturn = false;
let shouldRunCommand = true;
if (curKeybind.callback != null) {
@@ -178,13 +267,21 @@ class KeybindManager {
if (modalLevel.length != 0) {
// console.log("processing modal");
// special case when modal keybindings are present
let shouldReturn = this.processLevel(nativeEvent, event, modalLevel);
let controlLevel = this.levelMap.get("control");
let shouldReturn = this.processLevel(nativeEvent, event, controlLevel);
if (shouldReturn) {
return true;
}
shouldReturn = this.processLevel(nativeEvent, event, modalLevel);
if (shouldReturn) {
return true;
}
let systemLevel = this.levelMap.get("system");
return this.processLevel(nativeEvent, event, systemLevel);
}
if (DumpLogs) {
console.log("levels:", this.levelMap, "event:", event);
}
for (let index = this.levelArray.length - 1; index >= 0; index--) {
let curLevel = this.levelArray[index];
let curKeybindsArray;
@@ -357,7 +454,6 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl {
for (let key of keys) {
if (key == "Cmd") {
rtn.mods.Cmd = true;
rtn.mods.Meta = true;
} else if (key == "Shift") {
rtn.mods.Shift = true;
} else if (key == "Ctrl") {
@@ -407,10 +503,10 @@ function notMod(keyPressMod, eventMod) {
function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean {
let keyPress = parseKeyDescription(keyDescription);
if (notMod(keyPress.mods.Option, event.option)) {
if (!keyPress.mods.Alt && notMod(keyPress.mods.Option, event.option)) {
return false;
}
if (notMod(keyPress.mods.Cmd, event.cmd)) {
if (!keyPress.mods.Meta && notMod(keyPress.mods.Cmd, event.cmd)) {
return false;
}
if (notMod(keyPress.mods.Shift, event.shift)) {
@@ -419,10 +515,10 @@ function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): bool
if (notMod(keyPress.mods.Ctrl, event.control)) {
return false;
}
if (notMod(keyPress.mods.Alt, event.alt)) {
if (keyPress.mods.Alt && !event.alt) {
return false;
}
if (notMod(keyPress.mods.Meta, event.meta)) {
if (keyPress.mods.Meta && !event.meta) {
return false;
}
let eventKey = "";
+4 -3
View File
@@ -45,6 +45,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scws"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
"github.com/wavetermdev/waveterm/wavesrv/pkg/wsshell"
)
@@ -211,7 +212,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
return
}
activity := sstore.ActivityUpdate{}
activity := telemetry.ActivityUpdate{}
if activeState.Fg {
activity.FgMinutes = 1
}
@@ -222,7 +223,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
activity.OpenMinutes = 1
}
activity.NumConns = remote.NumRemotes()
err = sstore.UpdateCurrentActivity(r.Context(), activity)
err = telemetry.UpdateCurrentActivity(r.Context(), activity)
if err != nil {
WriteJsonError(w, fmt.Errorf("error updating activity: %w", err))
return
@@ -998,7 +999,7 @@ func main() {
}
log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint())
sstore.UpdateActivityWrap(context.Background(), sstore.ActivityUpdate{NumConns: remote.NumRemotes()}, "numconns") // set at least one record into activity
telemetry.UpdateActivityWrap(context.Background(), telemetry.ActivityUpdate{NumConns: remote.NumRemotes()}, "numconns") // set at least one record into activity
installSignalHandlers()
go telemetryLoop()
go stdinReadWatch()
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package bookmarks
import (
"context"
"fmt"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
)
type BookmarkType struct {
BookmarkId string `json:"bookmarkid"`
CreatedTs int64 `json:"createdts"`
CmdStr string `json:"cmdstr"`
Alias string `json:"alias,omitempty"`
Tags []string `json:"tags"`
Description string `json:"description"`
OrderIdx int64 `json:"orderidx"`
Remove bool `json:"remove,omitempty"`
}
func (bm *BookmarkType) GetSimpleKey() string {
return bm.BookmarkId
}
func (bm *BookmarkType) ToMap() map[string]interface{} {
rtn := make(map[string]interface{})
rtn["bookmarkid"] = bm.BookmarkId
rtn["createdts"] = bm.CreatedTs
rtn["cmdstr"] = bm.CmdStr
rtn["alias"] = bm.Alias
rtn["description"] = bm.Description
rtn["tags"] = dbutil.QuickJsonArr(bm.Tags)
return rtn
}
func (bm *BookmarkType) FromMap(m map[string]interface{}) bool {
dbutil.QuickSetStr(&bm.BookmarkId, m, "bookmarkid")
dbutil.QuickSetInt64(&bm.CreatedTs, m, "createdts")
dbutil.QuickSetStr(&bm.Alias, m, "alias")
dbutil.QuickSetStr(&bm.CmdStr, m, "cmdstr")
dbutil.QuickSetStr(&bm.Description, m, "description")
dbutil.QuickSetJsonArr(&bm.Tags, m, "tags")
return true
}
type bookmarkOrderType struct {
BookmarkId string
OrderIdx int64
}
func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) {
var bms []*BookmarkType
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
var query string
if tag == "" {
query = `SELECT * FROM bookmark`
bms = dbutil.SelectMapsGen[*BookmarkType](tx, query)
} else {
query = `SELECT * FROM bookmark WHERE EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)`
bms = dbutil.SelectMapsGen[*BookmarkType](tx, query, tag)
}
bmMap := dbutil.MakeGenMap(bms)
var orders []bookmarkOrderType
query = `SELECT bookmarkid, orderidx FROM bookmark_order WHERE tag = ?`
tx.Select(&orders, query, tag)
for _, bmOrder := range orders {
bm := bmMap[bmOrder.BookmarkId]
if bm != nil {
bm.OrderIdx = bmOrder.OrderIdx
}
}
return nil
})
if txErr != nil {
return nil, txErr
}
return bms, nil
}
func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*BookmarkType, error) {
var rtn *BookmarkType
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT * FROM bookmark WHERE bookmarkid = ?`
rtn = dbutil.GetMapGen[*BookmarkType](tx, query, bookmarkId)
if rtn == nil {
return nil
}
query = `SELECT orderidx FROM bookmark_order WHERE bookmarkid = ? AND tag = ?`
orderIdx := tx.GetInt(query, bookmarkId, tag)
rtn.OrderIdx = int64(orderIdx)
return nil
})
if txErr != nil {
return nil, txErr
}
return rtn, nil
}
func GetBookmarkIdByArg(ctx context.Context, bookmarkArg string) (string, error) {
var rtnId string
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
if len(bookmarkArg) == 8 {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid LIKE (? || '%')`
rtnId = tx.GetString(query, bookmarkArg)
return nil
}
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
rtnId = tx.GetString(query, bookmarkArg)
return nil
})
if txErr != nil {
return "", txErr
}
return rtnId, nil
}
func GetBookmarkIdsByCmdStr(ctx context.Context, cmdStr string) ([]string, error) {
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) ([]string, error) {
query := `SELECT bookmarkid FROM bookmark WHERE cmdstr = ?`
bmIds := tx.SelectStrings(query, cmdStr)
return bmIds, nil
})
}
// ignores OrderIdx field
func InsertBookmark(ctx context.Context, bm *BookmarkType) error {
if bm == nil || bm.BookmarkId == "" {
return fmt.Errorf("invalid empty bookmark id")
}
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
if tx.Exists(query, bm.BookmarkId) {
return fmt.Errorf("bookmarkid already exists")
}
query = `INSERT INTO bookmark ( bookmarkid, createdts, cmdstr, alias, tags, description)
VALUES (:bookmarkid,:createdts,:cmdstr,:alias,:tags,:description)`
tx.NamedExec(query, bm.ToMap())
for _, tag := range append(bm.Tags, "") {
query = `SELECT COALESCE(max(orderidx), 0) FROM bookmark_order WHERE tag = ?`
maxOrder := tx.GetInt(query, tag)
query = `INSERT INTO bookmark_order (tag, bookmarkid, orderidx) VALUES (?, ?, ?)`
tx.Exec(query, tag, bm.BookmarkId, maxOrder+1)
}
return nil
})
return txErr
}
const (
BookmarkField_Desc = "desc"
BookmarkField_CmdStr = "cmdstr"
)
func EditBookmark(ctx context.Context, bookmarkId string, editMap map[string]interface{}) error {
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
if !tx.Exists(query, bookmarkId) {
return fmt.Errorf("bookmark not found")
}
if desc, found := editMap[BookmarkField_Desc]; found {
query = `UPDATE bookmark SET description = ? WHERE bookmarkid = ?`
tx.Exec(query, desc, bookmarkId)
}
if cmdStr, found := editMap[BookmarkField_CmdStr]; found {
query = `UPDATE bookmark SET cmdstr = ? WHERE bookmarkid = ?`
tx.Exec(query, cmdStr, bookmarkId)
}
return nil
})
return txErr
}
func fixupBookmarkOrder(tx *sstore.TxWrap) {
query := `
WITH new_order AS (
SELECT tag, bookmarkid, row_number() OVER (PARTITION BY tag ORDER BY orderidx) AS newidx FROM bookmark_order
)
UPDATE bookmark_order
SET orderidx = new_order.newidx
FROM new_order
WHERE bookmark_order.tag = new_order.tag AND bookmark_order.bookmarkid = new_order.bookmarkid
`
tx.Exec(query)
}
func DeleteBookmark(ctx context.Context, bookmarkId string) error {
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
if !tx.Exists(query, bookmarkId) {
return fmt.Errorf("bookmark not found")
}
query = `DELETE FROM bookmark WHERE bookmarkid = ?`
tx.Exec(query, bookmarkId)
query = `DELETE FROM bookmark_order WHERE bookmarkid = ?`
tx.Exec(query, bookmarkId)
fixupBookmarkOrder(tx)
return nil
})
return txErr
}

Some files were not shown because too many files have changed in this diff Show More