mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 669762a9c9 | |||
| f975ecce48 | |||
| a121bd4bb5 | |||
| dcc7b2943e | |||
| bfa549ef22 | |||
| 376e339dfe | |||
| 646e260488 | |||
| d3c48e3a3e | |||
| c0c53edb84 | |||
| 923cf71e0a | |||
| f705a4df0a | |||
| 75a82de5bf |
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
@@ -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 = [];
|
||||
|
||||
@@ -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
@@ -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
@@ -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()}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 = "";
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user