mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
merge branch 'main' into use-ssh-library
This commit is contained in:
commit
d1bffb5a0d
14
.github/workflows/build-helper.yml
vendored
14
.github/workflows/build-helper.yml
vendored
@ -1,9 +1,9 @@
|
|||||||
name: "Build Helper"
|
name: "Build Helper"
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
env:
|
env:
|
||||||
WAVETERM_VERSION: 0.6.0
|
WAVETERM_VERSION: 0.6.1
|
||||||
GO_VERSION: '1.21.5'
|
GO_VERSION: "1.21.5"
|
||||||
NODE_VERSION: '21.5.0'
|
NODE_VERSION: "21.5.0"
|
||||||
jobs:
|
jobs:
|
||||||
runbuild-darwin-x64:
|
runbuild-darwin-x64:
|
||||||
name: "Build MacOS x64"
|
name: "Build MacOS x64"
|
||||||
@ -21,7 +21,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{env.NODE_VERSION}}
|
node-version: ${{env.NODE_VERSION}}
|
||||||
cache: 'yarn'
|
cache: "yarn"
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: scripthaus run build-package
|
- run: scripthaus run build-package
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@ -45,7 +45,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{env.NODE_VERSION}}
|
node-version: ${{env.NODE_VERSION}}
|
||||||
cache: 'yarn'
|
cache: "yarn"
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: scripthaus run build-package
|
- run: scripthaus run build-package
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@ -78,7 +78,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{env.NODE_VERSION}}
|
node-version: ${{env.NODE_VERSION}}
|
||||||
cache: 'yarn'
|
cache: "yarn"
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: ./scripthaus/scripthaus run build-package-linux
|
- run: ./scripthaus/scripthaus run build-package-linux
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
@ -98,5 +98,3 @@ jobs:
|
|||||||
name: waveterm-builds
|
name: waveterm-builds
|
||||||
path: buildtemp
|
path: buildtemp
|
||||||
retention-days: 2
|
retention-days: 2
|
||||||
|
|
||||||
|
|
||||||
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "golang.go", "dbaeumer.vscode-eslint"]
|
||||||
|
}
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# assumes we have Wave-darwin-x64-[version].zip and Wave-darwin-arm64-[version].zip in current directory
|
# assumes we have Wave-darwin-x64-[version].zip and Wave-darwin-arm64-[version].zip in current directory
|
||||||
VERSION=0.6.0
|
VERSION=0.6.1
|
||||||
rm -rf temp
|
rm -rf temp
|
||||||
rm -rf builds
|
rm -rf builds
|
||||||
mkdir temp
|
mkdir temp
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "waveterm",
|
"name": "waveterm",
|
||||||
"author": "Command Line Inc",
|
"author": "Command Line Inc",
|
||||||
"productName": "Wave",
|
"productName": "Wave",
|
||||||
"version": "0.6.0",
|
"version": "0.6.1",
|
||||||
"main": "dist/emain.js",
|
"main": "dist/emain.js",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -99,9 +99,6 @@ body a {
|
|||||||
|
|
||||||
body code {
|
body code {
|
||||||
font-family: @terminal-font;
|
font-family: @terminal-font;
|
||||||
}
|
|
||||||
|
|
||||||
body code {
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,11 +120,19 @@ svg.icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hideScrollbarUntillHover {
|
.hideScrollbarUntillHover {
|
||||||
overflow: hidden;
|
overflow: scroll;
|
||||||
&:hover,
|
|
||||||
&:focus,
|
&::-webkit-scrollbar-thumb,
|
||||||
&:focus-within {
|
&::-webkit-scrollbar-track {
|
||||||
overflow: auto;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +652,6 @@ a.a-block {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid rgba(241, 246, 243, 0.08);
|
border: 1px solid rgba(241, 246, 243, 0.08);
|
||||||
background: rgba(13, 13, 13, 0.85);
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
margin: 24px 18px;
|
margin: 24px 18px;
|
||||||
|
6
src/app/assets/icons/spinner-indicator.svg
Normal file
6
src/app/assets/icons/spinner-indicator.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg width="440" height="440" version="1.1" viewBox="0 0 116.42 116.42" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle id="indicator" cx="58.266" cy="58.266" r="28.311" stroke-width=".19016"/>
|
||||||
|
<path id="spinner" d="m107.2 58.21a48.988 48.988 0 0 1-48.988 48.988" fill="none" stroke="#000" stroke-width="16.856" style="paint-order:normal"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 443 B |
@ -609,7 +609,6 @@
|
|||||||
|
|
||||||
.wave-dropdown {
|
.wave-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: transparent;
|
|
||||||
height: 44px;
|
height: 44px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -715,9 +714,7 @@
|
|||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 0;
|
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
padding: 0;
|
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
@ -775,7 +772,6 @@
|
|||||||
min-width: 412px;
|
min-width: 412px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
|
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--element-hover-2, rgba(255, 255, 255, 0.06));
|
background: var(--element-hover-2, rgba(255, 255, 255, 0.06));
|
||||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
@ -931,7 +927,6 @@
|
|||||||
background: none;
|
background: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: inherit;
|
outline: inherit;
|
||||||
@ -1156,18 +1151,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
color: @term-red;
|
|
||||||
}
|
|
||||||
&.success {
|
|
||||||
color: @term-green;
|
|
||||||
}
|
|
||||||
&.output {
|
|
||||||
color: @term-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -13,6 +13,7 @@ import { RemoteType, StatusIndicatorLevel } from "../../types/types";
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { GlobalModel } from "../../model/model";
|
import { GlobalModel } from "../../model/model";
|
||||||
import * as appconst from "../appconst";
|
import * as appconst from "../appconst";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
||||||
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
||||||
@ -117,7 +118,7 @@ class Checkbox extends React.Component<
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
checkedInternal: this.props.checked !== undefined ? this.props.checked : Boolean(this.props.defaultChecked),
|
checkedInternal: this.props.checked ?? Boolean(this.props.defaultChecked),
|
||||||
};
|
};
|
||||||
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
||||||
}
|
}
|
||||||
@ -287,15 +288,16 @@ class Button extends React.Component<ButtonProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { leftIcon, rightIcon, theme, children, disabled, variant, color, style } = this.props;
|
const { leftIcon, rightIcon, theme, children, disabled, variant, color, style, autoFocus, className } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn("wave-button", theme, variant, color, { disabled: disabled })}
|
className={cn("wave-button", theme, variant, color, { disabled: disabled }, className)}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={style}
|
style={style}
|
||||||
autoFocus={this.props.autoFocus}
|
autoFocus={autoFocus}
|
||||||
>
|
>
|
||||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||||
{children}
|
{children}
|
||||||
@ -714,13 +716,14 @@ class InlineSettingsTextEdit extends React.Component<
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleKeyDown(e: any): void {
|
handleKeyDown(e: any): void {
|
||||||
if (e.code == "Enter") {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.confirmChange();
|
this.confirmChange();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Escape") {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.cancelChange();
|
this.cancelChange();
|
||||||
@ -868,7 +871,7 @@ class Markdown extends React.Component<
|
|||||||
if (codeSelect) {
|
if (codeSelect) {
|
||||||
return <CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex}>{props.children}</CodeBlockMarkdown>;
|
return <CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex}>{props.children}</CodeBlockMarkdown>;
|
||||||
} else {
|
} else {
|
||||||
let clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
let blockText = (e.target as HTMLElement).innerText;
|
let blockText = (e.target as HTMLElement).innerText;
|
||||||
if (blockText) {
|
if (blockText) {
|
||||||
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
|
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
|
||||||
@ -896,7 +899,9 @@ class Markdown extends React.Component<
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
|
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
|
||||||
<ReactMarkdown children={text} remarkPlugins={[remarkGfm]} components={markdownComponents} />
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
|
{text}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1239,38 +1244,6 @@ class Modal extends React.Component<ModalProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusIndicatorProps {
|
|
||||||
level: StatusIndicatorLevel;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusIndicator extends React.Component<StatusIndicatorProps> {
|
|
||||||
render() {
|
|
||||||
const statusIndicatorLevel = this.props.level;
|
|
||||||
let statusIndicator = null;
|
|
||||||
if (statusIndicatorLevel != StatusIndicatorLevel.None) {
|
|
||||||
let statusIndicatorClass = null;
|
|
||||||
switch (statusIndicatorLevel) {
|
|
||||||
case StatusIndicatorLevel.Output:
|
|
||||||
statusIndicatorClass = "output";
|
|
||||||
break;
|
|
||||||
case StatusIndicatorLevel.Success:
|
|
||||||
statusIndicatorClass = "success";
|
|
||||||
break;
|
|
||||||
case StatusIndicatorLevel.Error:
|
|
||||||
statusIndicatorClass = "error";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
statusIndicator = (
|
|
||||||
<div
|
|
||||||
className={`${this.props.className} fa-sharp fa-solid fa-circle-small status-indicator ${statusIndicatorClass}`}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return statusIndicator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShowWaveShellInstallPrompt(callbackFn: () => void) {
|
function ShowWaveShellInstallPrompt(callbackFn: () => void) {
|
||||||
let message: string = `
|
let message: string = `
|
||||||
In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell).
|
In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell).
|
||||||
@ -1313,6 +1286,5 @@ export {
|
|||||||
LinkButton,
|
LinkButton,
|
||||||
Status,
|
Status,
|
||||||
Modal,
|
Modal,
|
||||||
StatusIndicator,
|
|
||||||
ShowWaveShellInstallPrompt,
|
ShowWaveShellInstallPrompt,
|
||||||
};
|
};
|
||||||
|
72
src/app/common/icons/icons.less
Normal file
72
src/app/common/icons/icons.less
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.front-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positional-icon-visible {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positional-icon-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positional-icon {
|
||||||
|
display: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
.svg-icon svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positional-icon-inner {
|
||||||
|
display: flex;
|
||||||
|
width: inherit;
|
||||||
|
height: 100%;
|
||||||
|
line-height: inherit;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
& > div,
|
||||||
|
i,
|
||||||
|
.svg-icon,
|
||||||
|
span {
|
||||||
|
width: 20px;
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
.icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
#spinner,
|
||||||
|
#indicator {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.spin #spinner {
|
||||||
|
visibility: visible;
|
||||||
|
stroke: @term-white;
|
||||||
|
}
|
||||||
|
&.error #indicator {
|
||||||
|
visibility: visible;
|
||||||
|
fill: @term-red;
|
||||||
|
}
|
||||||
|
&.success #indicator {
|
||||||
|
visibility: visible;
|
||||||
|
fill: @term-green;
|
||||||
|
}
|
||||||
|
&.output #indicator {
|
||||||
|
visibility: visible;
|
||||||
|
fill: @term-white;
|
||||||
|
}
|
||||||
|
}
|
77
src/app/common/icons/icons.tsx
Normal file
77
src/app/common/icons/icons.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { StatusIndicatorLevel } from "../../../types/types";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
|
||||||
|
|
||||||
|
interface PositionalIconProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FrontIcon extends React.Component<PositionalIconProps> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={cn("front-icon", "positional-icon", this.props.className)}>
|
||||||
|
<div className="positional-icon-inner">{this.props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CenteredIcon extends React.Component<PositionalIconProps> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={cn("centered-icon", "positional-icon", this.props.className)} onClick={this.props.onClick}>
|
||||||
|
<div className="positional-icon-inner">{this.props.children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionsIconProps {
|
||||||
|
onClick: React.MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionsIcon extends React.Component<ActionsIconProps> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CenteredIcon className="actions" onClick={this.props.onClick}>
|
||||||
|
<div className="icon hoverEffect fa-sharp fa-solid fa-1x fa-ellipsis-vertical"></div>
|
||||||
|
</CenteredIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusIndicatorProps {
|
||||||
|
level: StatusIndicatorLevel;
|
||||||
|
className?: string;
|
||||||
|
runningCommands?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
|
||||||
|
render() {
|
||||||
|
const { level, className, runningCommands } = this.props;
|
||||||
|
let statusIndicator = null;
|
||||||
|
if (level != StatusIndicatorLevel.None || runningCommands) {
|
||||||
|
let levelClass = null;
|
||||||
|
switch (level) {
|
||||||
|
case StatusIndicatorLevel.Output:
|
||||||
|
levelClass = "output";
|
||||||
|
break;
|
||||||
|
case StatusIndicatorLevel.Success:
|
||||||
|
levelClass = "success";
|
||||||
|
break;
|
||||||
|
case StatusIndicatorLevel.Error:
|
||||||
|
levelClass = "error";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
statusIndicator = (
|
||||||
|
<CenteredIcon className={cn(className, levelClass, "status-indicator")}>
|
||||||
|
<SpinnerIndicator className={runningCommands ? "spin" : null} />
|
||||||
|
</CenteredIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return statusIndicator;
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
|||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import { Line } from "../line/linecomps";
|
import { Line } from "../line/linecomps";
|
||||||
import { CmdStrCode } from "../common/common";
|
import { CmdStrCode } from "../common/common";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||||
|
|
||||||
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
|
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
|
||||||
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||||
@ -95,7 +96,7 @@ function formatSessionName(snames: Record<string, string>, sessionId: string): s
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class HistoryCheckbox extends React.Component<{ checked: boolean, partialCheck?: boolean, onClick?: () => void }, {}> {
|
class HistoryCheckbox extends React.Component<{ checked: boolean; partialCheck?: boolean; onClick?: () => void }, {}> {
|
||||||
@boundMethod
|
@boundMethod
|
||||||
clickHandler(): void {
|
clickHandler(): void {
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
@ -111,13 +112,17 @@ class HistoryCheckbox extends React.Component<{ checked: boolean, partialCheck?:
|
|||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" fill="#D5FEAF" fill-opacity="0.026" />
|
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" fill="#D5FEAF" fill-opacity="0.026" />
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 8C4 6.89543 4.89543 6 6 6H10C11.1046 6 12 6.89543 12 8C12 9.10457 11.1046 10 10 10H6C4.89543 10 4 9.10457 4 8Z" fill="#58C142"/>
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M4 8C4 6.89543 4.89543 6 6 6H10C11.1046 6 12 6.89543 12 8C12 9.10457 11.1046 10 10 10H6C4.89543 10 4 9.10457 4 8Z"
|
||||||
|
fill="#58C142"
|
||||||
|
/>
|
||||||
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" stroke="#3B3F3A" />
|
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" stroke="#3B3F3A" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
return <div onClick={this.clickHandler} className="history-checkbox state-unchecked" />;
|
||||||
return <div onClick={this.clickHandler} className="history-checkbox state-unchecked"/>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,7 +212,8 @@ class HistoryView extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
searchKeyDown(e: any) {
|
searchKeyDown(e: any) {
|
||||||
if (e.code == "Enter") {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
GlobalModel.historyViewModel.submitSearch();
|
GlobalModel.historyViewModel.submitSearch();
|
||||||
return;
|
return;
|
||||||
@ -513,9 +519,7 @@ class HistoryView extends React.Component<{}, {}> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="fromts">
|
<div className="fromts">
|
||||||
<div className="fromts-text">
|
<div className="fromts-text">From: </div>
|
||||||
From:
|
|
||||||
</div>
|
|
||||||
<div className="hoverEffect">
|
<div className="hoverEffect">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
@ -550,7 +554,10 @@ class HistoryView extends React.Component<{}, {}> {
|
|||||||
</div>
|
</div>
|
||||||
<div className={cn("control-bar", "is-top", { "is-hidden": items.length == 0 })}>
|
<div className={cn("control-bar", "is-top", { "is-hidden": items.length == 0 })}>
|
||||||
<div className="control-checkbox" onClick={this.handleControlCheckbox} title="Toggle Selection">
|
<div className="control-checkbox" onClick={this.handleControlCheckbox} title="Toggle Selection">
|
||||||
<HistoryCheckbox checked={numSelected > 0 && numSelected == items.length} partialCheck={numSelected > 0}/>
|
<HistoryCheckbox
|
||||||
|
checked={numSelected > 0 && numSelected == items.length}
|
||||||
|
partialCheck={numSelected > 0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -608,13 +615,31 @@ class HistoryView extends React.Component<{}, {}> {
|
|||||||
<td className="ts text-standard">{getHistoryViewTs(nowDate, item.ts)}</td>
|
<td className="ts text-standard">{getHistoryViewTs(nowDate, item.ts)}</td>
|
||||||
<td className="downarrow" onClick={() => this.activateItem(item.historyid)}>
|
<td className="downarrow" onClick={() => this.activateItem(item.historyid)}>
|
||||||
<If condition={activeItemId != item.historyid}>
|
<If condition={activeItemId != item.historyid}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg
|
||||||
<path d="M12.1297 6.62492C12.3999 6.93881 12.3645 7.41237 12.0506 7.68263L8.48447 10.7531C8.20296 10.9955 7.78645 10.9952 7.50519 10.7526L3.94636 7.68213C3.63274 7.41155 3.59785 6.93796 3.86843 6.62434C4.13901 6.31072 4.6126 6.27583 4.92622 6.54641L7.99562 9.19459L11.0719 6.54591C11.3858 6.27565 11.8594 6.31102 12.1297 6.62492Z" fill="#C3C8C2"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.1297 6.62492C12.3999 6.93881 12.3645 7.41237 12.0506 7.68263L8.48447 10.7531C8.20296 10.9955 7.78645 10.9952 7.50519 10.7526L3.94636 7.68213C3.63274 7.41155 3.59785 6.93796 3.86843 6.62434C4.13901 6.31072 4.6126 6.27583 4.92622 6.54641L7.99562 9.19459L11.0719 6.54591C11.3858 6.27565 11.8594 6.31102 12.1297 6.62492Z"
|
||||||
|
fill="#C3C8C2"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</If>
|
</If>
|
||||||
<If condition={activeItemId == item.historyid}>
|
<If condition={activeItemId == item.historyid}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg
|
||||||
<path d="M3.87035 9.37508C3.60009 9.06119 3.63546 8.58763 3.94936 8.31737L7.51553 5.24692C7.79704 5.00455 8.21355 5.00476 8.49481 5.24742L12.0536 8.31787C12.3673 8.58845 12.4022 9.06204 12.1316 9.37566C11.861 9.68928 11.3874 9.72417 11.0738 9.45359L8.00438 6.80541L4.92806 9.45409C4.61416 9.72435 4.14061 9.68898 3.87035 9.37508Z" fill="#C3C8C2"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.87035 9.37508C3.60009 9.06119 3.63546 8.58763 3.94936 8.31737L7.51553 5.24692C7.79704 5.00455 8.21355 5.00476 8.49481 5.24742L12.0536 8.31787C12.3673 8.58845 12.4022 9.06204 12.1316 9.37566C11.861 9.68928 11.3874 9.72417 11.0738 9.45359L8.00438 6.80541L4.92806 9.45409C4.61416 9.72435 4.14061 9.68898 3.87035 9.37508Z"
|
||||||
|
fill="#C3C8C2"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</If>
|
</If>
|
||||||
</td>
|
</td>
|
||||||
|
@ -360,6 +360,12 @@ class LineCmd extends React.Component<
|
|||||||
GlobalCommandRunner.lineDelete(line.lineid, true);
|
GlobalCommandRunner.lineDelete(line.lineid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
clickRestart() {
|
||||||
|
let { line } = this.props;
|
||||||
|
GlobalCommandRunner.lineRestart(line.lineid, true);
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
clickMinimize() {
|
clickMinimize() {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
@ -467,12 +473,21 @@ class LineCmd extends React.Component<
|
|||||||
renderMeta1(cmd: Cmd) {
|
renderMeta1(cmd: Cmd) {
|
||||||
let { line } = this.props;
|
let { line } = this.props;
|
||||||
let termOpts = cmd.getTermOpts();
|
let termOpts = cmd.getTermOpts();
|
||||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
let formattedTime: string = "";
|
||||||
|
let restartTs = cmd.getRestartTs();
|
||||||
|
let timeTitle: string = null;
|
||||||
|
if (restartTs != null && restartTs > 0) {
|
||||||
|
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
|
||||||
|
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||||
|
}
|
||||||
let renderer = line.renderer;
|
let renderer = line.renderer;
|
||||||
return (
|
return (
|
||||||
<div key="meta1" className="meta meta-line1">
|
<div key="meta1" className="meta meta-line1">
|
||||||
<SmallLineAvatar line={line} cmd={cmd} />
|
<SmallLineAvatar line={line} cmd={cmd} />
|
||||||
<div className="ts">{formattedTime}</div>
|
<div title={timeTitle} className="ts">{formattedTime}</div>
|
||||||
<div> </div>
|
<div> </div>
|
||||||
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
||||||
<div className="renderer">
|
<div className="renderer">
|
||||||
@ -665,6 +680,9 @@ class LineCmd extends React.Component<
|
|||||||
{this.renderMeta1(cmd)}
|
{this.renderMeta1(cmd)}
|
||||||
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
|
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
|
||||||
</div>
|
</div>
|
||||||
|
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
|
||||||
|
<i className="fa-sharp fa-regular fa-arrows-rotate"/>
|
||||||
|
</div>
|
||||||
<div key="delete" title="Delete Line (⌘D)" className="line-icon" onClick={this.clickDelete}>
|
<div key="delete" title="Delete Line (⌘D)" className="line-icon" onClick={this.clickDelete}>
|
||||||
<i className="fa-sharp fa-regular fa-trash" />
|
<i className="fa-sharp fa-regular fa-trash" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import "../../app/common/themes/themes.less";
|
@import "../../app/common/themes/themes.less";
|
||||||
|
@import "../../app/common/icons/icons.less";
|
||||||
|
|
||||||
.main-sidebar {
|
.main-sidebar {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -23,14 +24,20 @@
|
|||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 6em;
|
width: 6em;
|
||||||
min-width: 6em;
|
min-width: 6em;
|
||||||
.arrow-container, .collapse-button {
|
|
||||||
|
.arrow-container,
|
||||||
|
.collapse-button {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.contents {
|
.contents {
|
||||||
margin-top: 26px;
|
margin-top: 26px;
|
||||||
|
|
||||||
.top, .workspaces-item, .middle, .bottom, .separator {
|
.top,
|
||||||
|
.workspaces-item,
|
||||||
|
.middle,
|
||||||
|
.bottom,
|
||||||
|
.separator {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@ -120,12 +127,9 @@
|
|||||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
}
|
}
|
||||||
.index {
|
.index {
|
||||||
margin: 0 1.5em 0 -0.5rem;
|
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
vertical-align: middle;
|
text-align: center;
|
||||||
text-align: right;
|
|
||||||
width: 2em;
|
width: 2em;
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
.hotkey {
|
.hotkey {
|
||||||
float: left !important;
|
float: left !important;
|
||||||
@ -145,58 +149,70 @@
|
|||||||
|
|
||||||
.item {
|
.item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin: 0 6px;
|
margin-left: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
|
||||||
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
|
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
|
||||||
.sessionName {
|
width: inherit;
|
||||||
width: 12rem;
|
max-width: inherit;
|
||||||
display: inline-block;
|
min-width: inherit;
|
||||||
vertical-align: middle;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.front-icon {
|
||||||
|
.positional-icon-visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-icons {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-contents {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.icon {
|
.icon {
|
||||||
margin: -2px 8px 0px 4px;
|
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.actions.icon {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
.hotkey {
|
.hotkey {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
visibility: hidden;
|
|
||||||
letter-spacing: 6px;
|
letter-spacing: 6px;
|
||||||
}
|
}
|
||||||
.disabled .hotkey {
|
&:hover {
|
||||||
display: none;
|
:not(.disabled) .hotkey {
|
||||||
}
|
.positional-icon-visible;
|
||||||
&:hover .hotkey {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
visibility: hidden;
|
.positional-icon-visible;
|
||||||
}
|
}
|
||||||
&:hover .actions {
|
}
|
||||||
visibility: visible;
|
|
||||||
|
&:not(:hover) .status-indicator {
|
||||||
|
.positional-icon-visible;
|
||||||
}
|
}
|
||||||
.add_workspace {
|
.add_workspace {
|
||||||
float: right;
|
float: right;
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
border-radius: 50%;
|
|
||||||
transition: transform 0.3s ease-in-out;
|
transition: transform 0.3s ease-in-out;
|
||||||
vertical-align: middle;
|
|
||||||
svg {
|
svg {
|
||||||
fill: @base-color;
|
fill: @base-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.front-icon {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-icons {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-discord {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
|
@ -12,27 +12,54 @@ import { If } from "tsx-control-statements/components";
|
|||||||
import { compareLoose } from "semver";
|
import { compareLoose } from "semver";
|
||||||
|
|
||||||
import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
|
import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
|
||||||
import { ReactComponent as HelpIcon } from "../assets/icons/help.svg";
|
|
||||||
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
|
|
||||||
import { ReactComponent as DiscordIcon } from "../assets/icons/discord.svg";
|
|
||||||
import { ReactComponent as HistoryIcon } from "../assets/icons/history.svg";
|
|
||||||
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
|
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
|
||||||
import { ReactComponent as ConnectionsIcon } from "../assets/icons/connections.svg";
|
|
||||||
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
|
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
|
||||||
import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
|
import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
|
||||||
import { ReactComponent as ActionsIcon } from "../assets/icons/tab/actions.svg";
|
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
|
||||||
|
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
||||||
import { sortAndFilterRemotes, isBlank, openLink } from "../../util/util";
|
import { isBlank, openLink } from "../../util/util";
|
||||||
import * as constants from "../appconst";
|
import * as constants from "../appconst";
|
||||||
|
|
||||||
import "./sidebar.less";
|
import "./sidebar.less";
|
||||||
|
import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common/icons/icons";
|
||||||
|
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
type OV<V> = mobx.IObservableValue<V>;
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
class SideBarItem extends React.Component<{
|
||||||
|
frontIcon: React.ReactNode;
|
||||||
|
contents: React.ReactNode | string;
|
||||||
|
endIcons?: React.ReactNode[];
|
||||||
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
|
}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("item", "unselectable", "hoverEffect", this.props.className)}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
<FrontIcon>{this.props.frontIcon}</FrontIcon>
|
||||||
|
<div className="item-contents truncate">{this.props.contents}</div>
|
||||||
|
<div className="end-icons">{this.props.endIcons}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HotKeyIcon extends React.Component<{ hotkey: string }> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CenteredIcon className="hotkey">
|
||||||
|
<span>⌘{this.props.hotkey}</span>
|
||||||
|
</CenteredIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class MainSideBar extends React.Component<{}, {}> {
|
class MainSideBar extends React.Component<{}, {}> {
|
||||||
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
||||||
@ -99,7 +126,6 @@ class MainSideBar extends React.Component<{}, {}> {
|
|||||||
@boundMethod
|
@boundMethod
|
||||||
handlePlaybookClick(): void {
|
handlePlaybookClick(): void {
|
||||||
console.log("playbook click");
|
console.log("playbook click");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
@ -159,42 +185,29 @@ class MainSideBar extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
return sessionList.map((session, index) => {
|
return sessionList.map((session, index) => {
|
||||||
const isActive = GlobalModel.activeMainView.get() == "session" && activeSessionId == session.sessionId;
|
const isActive = GlobalModel.activeMainView.get() == "session" && activeSessionId == session.sessionId;
|
||||||
|
const sessionScreens = GlobalModel.getSessionScreens(session.sessionId);
|
||||||
|
const sessionIndicator = Math.max(...sessionScreens.map((screen) => screen.statusIndicator.get()));
|
||||||
|
const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0);
|
||||||
return (
|
return (
|
||||||
<div
|
<SideBarItem
|
||||||
key={index}
|
className={`${isActive ? "active" : ""}`}
|
||||||
className={`item hoverEffect ${isActive ? "active" : ""}`}
|
frontIcon={<span className="index">{index + 1}</span>}
|
||||||
|
contents={session.name.get()}
|
||||||
|
endIcons={[
|
||||||
|
<StatusIndicator
|
||||||
|
key="statusindicator"
|
||||||
|
level={sessionIndicator}
|
||||||
|
runningCommands={sessionRunningCommands}
|
||||||
|
/>,
|
||||||
|
<ActionsIcon key="actions" onClick={(e) => this.openSessionSettings(e, session)} />,
|
||||||
|
]}
|
||||||
onClick={() => this.handleSessionClick(session.sessionId)}
|
onClick={() => this.handleSessionClick(session.sessionId)}
|
||||||
>
|
|
||||||
<span className="index">{index + 1}</span>
|
|
||||||
<span className="truncate sessionName">{session.name.get()}</span>
|
|
||||||
<ActionsIcon
|
|
||||||
className="icon hoverEffect actions"
|
|
||||||
onClick={(e) => this.openSessionSettings(e, session)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let model = GlobalModel;
|
|
||||||
let activeSessionId = model.activeSessionId.get();
|
|
||||||
let activeScreen = model.getActiveScreen();
|
|
||||||
let activeRemoteId: string = null;
|
|
||||||
if (activeScreen != null) {
|
|
||||||
let rptr = activeScreen.curRemote.get();
|
|
||||||
if (rptr != null && !isBlank(rptr.remoteid)) {
|
|
||||||
activeRemoteId = rptr.remoteid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let remotes = model.remotes ?? [];
|
|
||||||
remotes = sortAndFilterRemotes(remotes);
|
|
||||||
let sessionList = [];
|
|
||||||
for (let session of model.sessionList) {
|
|
||||||
if (!session.archived.get() || session.sessionId == activeSessionId) {
|
|
||||||
sessionList.push(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let isCollapsed = this.collapsed.get();
|
let isCollapsed = this.collapsed.get();
|
||||||
let clientData = GlobalModel.clientData.get();
|
let clientData = GlobalModel.clientData.get();
|
||||||
let needsUpdate = false;
|
let needsUpdate = false;
|
||||||
@ -223,65 +236,66 @@ class MainSideBar extends React.Component<{}, {}> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
<div className="top">
|
<div className="top">
|
||||||
<div className="item hoverEffect unselectable" onClick={this.handleHistoryClick}>
|
<SideBarItem
|
||||||
<HistoryIcon className="icon" />
|
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
|
||||||
History
|
contents="History"
|
||||||
<span className="hotkey">⌘H</span>
|
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
|
||||||
</div>
|
onClick={this.handleHistoryClick}
|
||||||
{/* <div className="item hoverEffect unselectable" onClick={this.handleBookmarksClick}>
|
/>
|
||||||
<FavoritesIcon className="icon" />
|
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">⌘B</span>} onClick={this.handleBookmarksClick}/> */}
|
||||||
Favorites
|
<SideBarItem
|
||||||
<span className="hotkey">⌘B</span>
|
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
|
||||||
</div> */}
|
contents="Connections"
|
||||||
<div className="item hoverEffect unselectable" onClick={this.handleConnectionsClick}>
|
onClick={this.handleConnectionsClick}
|
||||||
<ConnectionsIcon className="icon" />
|
/>
|
||||||
Connections
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
<div className="item workspaces-item unselectable">
|
<SideBarItem
|
||||||
<WorkspacesIcon className="icon" />
|
frontIcon={<WorkspacesIcon className="icon" />}
|
||||||
Workspaces
|
contents="Workspaces"
|
||||||
<div className="add_workspace hoverEffect" onClick={this.handleNewSession}>
|
endIcons={[
|
||||||
|
<div
|
||||||
|
key="add_workspace"
|
||||||
|
className="add_workspace hoverEffect"
|
||||||
|
onClick={this.handleNewSession}
|
||||||
|
>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</div>
|
</div>,
|
||||||
</div>
|
]}
|
||||||
|
/>
|
||||||
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
||||||
<div className="bottom">
|
<div className="bottom">
|
||||||
<If condition={needsUpdate}>
|
<If condition={needsUpdate}>
|
||||||
<div
|
<SideBarItem
|
||||||
className="item hoverEffect unselectable updateBanner"
|
className="updateBanner"
|
||||||
|
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
|
||||||
|
contents="Update Available"
|
||||||
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
|
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
|
||||||
>
|
/>
|
||||||
<i className="fa-sharp fa-regular fa-circle-up icon" />
|
|
||||||
Update Available
|
|
||||||
</div>
|
|
||||||
</If>
|
</If>
|
||||||
<If condition={GlobalModel.isDev}>
|
<If condition={GlobalModel.isDev}>
|
||||||
<div className="item hoverEffect unselectable" onClick={this.handlePluginsClick}>
|
<SideBarItem
|
||||||
<AppsIcon className="icon" />
|
frontIcon={<AppsIcon className="icon" />}
|
||||||
Apps
|
contents="Apps"
|
||||||
<span className="hotkey">⌘A</span>
|
onClick={this.handlePluginsClick}
|
||||||
</div>
|
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
|
||||||
|
/>
|
||||||
</If>
|
</If>
|
||||||
<div className="item hoverEffect unselectable" onClick={this.handleSettingsClick}>
|
<SideBarItem
|
||||||
<SettingsIcon className="icon" />
|
frontIcon={<SettingsIcon className="icon" />}
|
||||||
Settings
|
contents="Settings"
|
||||||
</div>
|
onClick={this.handleSettingsClick}
|
||||||
<div
|
/>
|
||||||
className="item hoverEffect unselectable"
|
<SideBarItem
|
||||||
|
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
|
||||||
|
contents="Documentation"
|
||||||
onClick={() => openLink("https://docs.waveterm.dev")}
|
onClick={() => openLink("https://docs.waveterm.dev")}
|
||||||
>
|
/>
|
||||||
<HelpIcon className="icon" />
|
<SideBarItem
|
||||||
Documentation
|
frontIcon={<i className="fa-brands fa-discord icon" />}
|
||||||
</div>
|
contents="Discord"
|
||||||
<div
|
|
||||||
className="item hoverEffect unselectable"
|
|
||||||
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
|
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
|
||||||
>
|
/>
|
||||||
<DiscordIcon className="icon discord" />
|
|
||||||
Discord
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ import { TextAreaInput } from "./textareainput";
|
|||||||
import { If, For } from "tsx-control-statements/components";
|
import { If, For } from "tsx-control-statements/components";
|
||||||
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
||||||
import { Markdown } from "../../common/common";
|
import { Markdown } from "../../common/common";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class AIChat extends React.Component<{}, {}> {
|
class AIChat extends React.Component<{}, {}> {
|
||||||
@ -76,8 +77,8 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
let inputModel = model.inputModel;
|
let inputModel = model.inputModel;
|
||||||
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
||||||
let resetCodeSelect = !ctrlMod;
|
let resetCodeSelect = !ctrlMod;
|
||||||
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
if (e.code == "Enter") {
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!ctrlMod) {
|
if (!ctrlMod) {
|
||||||
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||||
@ -91,17 +92,18 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
|
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.code == "Escape") {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
inputModel.closeAIAssistantChat();
|
inputModel.closeAIAssistantChat();
|
||||||
}
|
}
|
||||||
if (e.code == "KeyL" && e.getModifierState("Control")) {
|
|
||||||
|
if (checkKeyPressed(waveEvent, "Ctrl:l")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
inputModel.clearAIAssistantChat();
|
inputModel.clearAIAssistantChat();
|
||||||
}
|
}
|
||||||
if (e.code == "ArrowUp") {
|
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||||
if (this.getLinePos(e.target).linePos > 1) {
|
if (this.getLinePos(e.target).linePos > 1) {
|
||||||
// normal up arrow
|
// normal up arrow
|
||||||
return;
|
return;
|
||||||
@ -110,7 +112,7 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
inputModel.codeSelectSelectNextOldestCodeBlock();
|
inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||||
resetCodeSelect = false;
|
resetCodeSelect = false;
|
||||||
}
|
}
|
||||||
if (e.code == "ArrowDown") {
|
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||||
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
.cmd-input-filter {
|
.cmd-input-filter {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1.0;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@ -198,7 +198,7 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1.0;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +251,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg-user {
|
.chat-msg-user {
|
||||||
|
|
||||||
.msg-text {
|
.msg-text {
|
||||||
font-family: @markdown-font;
|
font-family: @markdown-font;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -265,7 +264,6 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.grow-spacer {
|
.grow-spacer {
|
||||||
flex: 1 0 10px;
|
flex: 1 0 10px;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
|||||||
import { getMonoFontSize } from "../../../util/textmeasure";
|
import { getMonoFontSize } from "../../../util/textmeasure";
|
||||||
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
|
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
|
||||||
import * as appconst from "../../appconst";
|
import * as appconst from "../../appconst";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||||
|
|
||||||
type OV<T> = mobx.IObservableValue<T>;
|
type OV<T> = mobx.IObservableValue<T>;
|
||||||
|
|
||||||
@ -177,12 +178,13 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
||||||
let curLine = inputModel.getCurLine();
|
let curLine = inputModel.getCurLine();
|
||||||
|
|
||||||
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
let lastTab = this.lastTab;
|
let lastTab = this.lastTab;
|
||||||
this.lastTab = e.code == "Tab";
|
this.lastTab = checkKeyPressed(waveEvent, "Tab");
|
||||||
let lastHist = this.lastHistoryUpDown;
|
let lastHist = this.lastHistoryUpDown;
|
||||||
this.lastHistoryUpDown = false;
|
this.lastHistoryUpDown = false;
|
||||||
|
|
||||||
if (e.code == "Tab") {
|
if (checkKeyPressed(waveEvent, "Tab")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (lastTab) {
|
if (lastTab) {
|
||||||
GlobalModel.submitCommand(
|
GlobalModel.submitCommand(
|
||||||
@ -204,7 +206,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.code == "Enter") {
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!ctrlMod) {
|
if (!ctrlMod) {
|
||||||
if (GlobalModel.inputModel.isEmpty()) {
|
if (GlobalModel.inputModel.isEmpty()) {
|
||||||
@ -224,7 +226,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
GlobalModel.inputModel.setCurLine(e.target.value);
|
GlobalModel.inputModel.setCurLine(e.target.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Escape") {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let inputModel = GlobalModel.inputModel;
|
let inputModel = GlobalModel.inputModel;
|
||||||
@ -235,50 +237,50 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
inputModel.closeAIAssistantChat();
|
inputModel.closeAIAssistantChat();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyE" && e.getModifierState("Meta")) {
|
if (checkKeyPressed(waveEvent, "Cmd:e")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let inputModel = GlobalModel.inputModel;
|
let inputModel = GlobalModel.inputModel;
|
||||||
inputModel.toggleExpandInput();
|
inputModel.toggleExpandInput();
|
||||||
}
|
}
|
||||||
if (e.code == "KeyC" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:c")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.resetInput();
|
inputModel.resetInput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyU" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:u")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.controlU();
|
this.controlU();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyP" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:p")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.controlP();
|
this.controlP();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyN" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:n")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.controlN();
|
this.controlN();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyW" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:w")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.controlW();
|
this.controlW();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyY" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:y")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.controlY();
|
this.controlY();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyR" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:r")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.openHistory();
|
inputModel.openHistory();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((e.code == "ArrowUp" || e.code == "ArrowDown") && hasNoModifiers(e)) {
|
if (checkKeyPressed(waveEvent, "ArrowUp") || checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||||
if (!inputModel.isHistoryLoaded()) {
|
if (!inputModel.isHistoryLoaded()) {
|
||||||
if (e.code == "ArrowUp") {
|
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||||
this.lastHistoryUpDown = true;
|
this.lastHistoryUpDown = true;
|
||||||
inputModel.loadHistory(false, 1, "screen");
|
inputModel.loadHistory(false, 1, "screen");
|
||||||
}
|
}
|
||||||
@ -286,7 +288,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
}
|
}
|
||||||
// invisible history movement
|
// invisible history movement
|
||||||
let linePos = this.getLinePos(e.target);
|
let linePos = this.getLinePos(e.target);
|
||||||
if (e.code == "ArrowUp") {
|
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||||
if (!lastHist && linePos.linePos > 1) {
|
if (!lastHist && linePos.linePos > 1) {
|
||||||
// regular arrow
|
// regular arrow
|
||||||
return;
|
return;
|
||||||
@ -296,7 +298,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
this.lastHistoryUpDown = true;
|
this.lastHistoryUpDown = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "ArrowDown") {
|
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||||
if (!lastHist && linePos.linePos < linePos.numLines) {
|
if (!lastHist && linePos.linePos < linePos.numLines) {
|
||||||
// regular arrow
|
// regular arrow
|
||||||
return;
|
return;
|
||||||
@ -307,16 +309,16 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.code == "PageUp" || e.code == "PageDown") {
|
if (checkKeyPressed(waveEvent, "PageUp") || checkKeyPressed(waveEvent, "PageDown")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let infoScroll = inputModel.hasScrollingInfoMsg();
|
let infoScroll = inputModel.hasScrollingInfoMsg();
|
||||||
if (infoScroll) {
|
if (infoScroll) {
|
||||||
let div = document.querySelector(".cmd-input-info");
|
let div = document.querySelector(".cmd-input-info");
|
||||||
let amt = pageSize(div);
|
let amt = pageSize(div);
|
||||||
scrollDiv(div, e.code == "PageUp" ? -amt : amt);
|
scrollDiv(div, checkKeyPressed(waveEvent, "PageUp") ? -amt : amt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.code == "Space" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:Space")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.openAIAssistantChat();
|
inputModel.openAIAssistantChat();
|
||||||
}
|
}
|
||||||
@ -338,32 +340,29 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
onHistoryKeyDown(e: any) {
|
onHistoryKeyDown(e: any) {
|
||||||
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
let inputModel = GlobalModel.inputModel;
|
let inputModel = GlobalModel.inputModel;
|
||||||
if (e.code == "Escape") {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.resetHistory();
|
inputModel.resetHistory();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Enter") {
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.grabSelectedHistoryItem();
|
inputModel.grabSelectedHistoryItem();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyG" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:g")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.resetInput();
|
inputModel.resetInput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyC" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:c")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.resetInput();
|
inputModel.resetInput();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (checkKeyPressed(waveEvent, "Cmd:r") || checkKeyPressed(waveEvent, "Ctrl:r")) {
|
||||||
e.code == "KeyR" &&
|
|
||||||
(e.getModifierState("Meta") || e.getModifierState("Control")) &&
|
|
||||||
!e.getModifierState("Shift")
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||||
if (opts.limitRemote) {
|
if (opts.limitRemote) {
|
||||||
@ -376,7 +375,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
inputModel.setHistoryQueryOpts(opts);
|
inputModel.setHistoryQueryOpts(opts);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyS" && (e.getModifierState("Meta") || e.getModifierState("Control"))) {
|
if (checkKeyPressed(waveEvent, "Cmd:s") || checkKeyPressed(waveEvent, "Ctrl:s")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||||
let htype = opts.queryType;
|
let htype = opts.queryType;
|
||||||
@ -390,26 +389,26 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
inputModel.setHistoryType(htype);
|
inputModel.setHistoryType(htype);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Tab") {
|
if (checkKeyPressed(waveEvent, "Tab")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "ArrowUp" || e.code == "ArrowDown") {
|
if (checkKeyPressed(waveEvent, "ArrowUp") || checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.moveHistorySelection(e.code == "ArrowUp" ? 1 : -1);
|
inputModel.moveHistorySelection(checkKeyPressed(waveEvent, "ArrowUp") ? 1 : -1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "PageUp" || e.code == "PageDown") {
|
if (checkKeyPressed(waveEvent, "PageUp") || checkKeyPressed(waveEvent, "PageDown")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.moveHistorySelection(e.code == "PageUp" ? 10 : -10);
|
inputModel.moveHistorySelection(checkKeyPressed(waveEvent, "PageUp") ? 10 : -10);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyP" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:p")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.moveHistorySelection(1);
|
inputModel.moveHistorySelection(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyN" && e.getModifierState("Control")) {
|
if (checkKeyPressed(waveEvent, "Ctrl:n")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
inputModel.moveHistorySelection(-1);
|
inputModel.moveHistorySelection(-1);
|
||||||
return;
|
return;
|
||||||
|
@ -7,7 +7,8 @@ import * as mobx from "mobx";
|
|||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||||
import { StatusIndicator, renderCmdText } from "../../common/common";
|
import { ActionsIcon, StatusIndicator, CenteredIcon } from "../../common/icons/icons";
|
||||||
|
import { renderCmdText } from "../../common/common";
|
||||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||||
import * as constants from "../../appconst";
|
import * as constants from "../../appconst";
|
||||||
import { Reorder } from "framer-motion";
|
import { Reorder } from "framer-motion";
|
||||||
@ -79,14 +80,12 @@ class ScreenTab extends React.Component<
|
|||||||
|
|
||||||
let tabIndex = null;
|
let tabIndex = null;
|
||||||
if (index + 1 <= 9) {
|
if (index + 1 <= 9) {
|
||||||
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>;
|
tabIndex = (
|
||||||
}
|
<CenteredIcon className="tab-index">
|
||||||
|
<div>{renderCmdText(String(index + 1))}</div>
|
||||||
let settings = (
|
</CenteredIcon>
|
||||||
<div onClick={(e) => this.openScreenSettings(e, screen)} title="Actions" className="tab-gear">
|
|
||||||
<div className="icon hoverEffect fa-sharp fa-solid fa-ellipsis-vertical"></div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
let archived = screen.archived.get() ? (
|
let archived = screen.archived.get() ? (
|
||||||
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
|
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
|
||||||
) : null;
|
) : null;
|
||||||
@ -96,6 +95,7 @@ class ScreenTab extends React.Component<
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const statusIndicatorLevel = screen.statusIndicator.get();
|
const statusIndicatorLevel = screen.statusIndicator.get();
|
||||||
|
const runningCommands = screen.numRunningCmds.get() > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Reorder.Item
|
<Reorder.Item
|
||||||
@ -115,20 +115,16 @@ class ScreenTab extends React.Component<
|
|||||||
onContextMenu={(event) => this.openScreenSettings(event, screen)}
|
onContextMenu={(event) => this.openScreenSettings(event, screen)}
|
||||||
onDragEnd={this.handleDragEnd}
|
onDragEnd={this.handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="front-icon">
|
<CenteredIcon className="front-icon">{this.renderTabIcon(screen)}</CenteredIcon>
|
||||||
{this.renderTabIcon(screen)}
|
|
||||||
</div>
|
|
||||||
<div className="tab-name truncate">
|
<div className="tab-name truncate">
|
||||||
{archived}
|
{archived}
|
||||||
{webShared}
|
{webShared}
|
||||||
{screen.name.get()}
|
{screen.name.get()}
|
||||||
</div>
|
</div>
|
||||||
<div className="end-icon">
|
<div className="end-icons">
|
||||||
<div className="end-icon-inner">
|
<StatusIndicator level={statusIndicatorLevel} runningCommands={runningCommands} />
|
||||||
<StatusIndicator level={statusIndicatorLevel}/>
|
|
||||||
{tabIndex}
|
{tabIndex}
|
||||||
{settings}
|
<ActionsIcon onClick={(e) => this.openScreenSettings(e, screen)} />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Reorder.Item>
|
</Reorder.Item>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import "../../../app/common/themes/themes.less";
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
@import "../../../app/common/icons/icons.less";
|
||||||
|
|
||||||
#main .screen-tabs .screen-tab {
|
#main .screen-tabs .screen-tab {
|
||||||
border-top: 1px solid transparent;
|
border-top: 1px solid transparent;
|
||||||
@ -27,10 +28,6 @@
|
|||||||
rgba(88, 193, 66, 0) 86.79%
|
rgba(88, 193, 66, 0) 86.79%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon i {
|
|
||||||
color: @tab-green;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.color-orange {
|
&.color-orange {
|
||||||
@ -242,14 +239,6 @@
|
|||||||
|
|
||||||
.screen-tabs-container-inner {
|
.screen-tabs-container-inner {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
&::-webkit-scrollbar-thumb,
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::-webkit-scrollbar-thumb {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen-tabs {
|
.screen-tabs {
|
||||||
@ -268,14 +257,7 @@
|
|||||||
padding: 0 8px 0 8px;
|
padding: 0 8px 0 8px;
|
||||||
|
|
||||||
.front-icon {
|
.front-icon {
|
||||||
margin-right: 5px;
|
.positional-icon-visible;
|
||||||
.svg-icon svg {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
.fa-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-name {
|
.tab-name {
|
||||||
@ -294,57 +276,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only one of these will be visible at a time
|
// Only one of these will be visible at a time
|
||||||
.end-icon {
|
.end-icons {
|
||||||
// This makes the calculations below easier since we don't need to account for the right margin on the parent tab.
|
// This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab.
|
||||||
margin: 0 -8px 0 0;
|
margin: 0 -8px 0 0;
|
||||||
.end-icon-inner {
|
line-height: normal;
|
||||||
& > div {
|
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
& > * {
|
|
||||||
margin: auto auto;
|
|
||||||
}
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.status-indicator {
|
|
||||||
display: block;
|
|
||||||
// The status indicator is a little shorter than the text; this raises it up a bit so it's more centered vertically
|
|
||||||
padding-bottom: 1px;
|
|
||||||
margin-top: -1px;
|
|
||||||
}
|
|
||||||
.tab-gear {
|
|
||||||
display: none;
|
|
||||||
.icon {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tab-index {
|
.tab-index {
|
||||||
display: none;
|
font-size: 12.5px;
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:hover) .status-indicator {
|
||||||
|
.positional-icon-visible;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.tab-gear {
|
.screen-tab:not(:hover) .tab-index {
|
||||||
display: block;
|
.positional-icon-visible;
|
||||||
}
|
}
|
||||||
|
.screen-tab:hover .actions {
|
||||||
|
.positional-icon-visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .screen-tab {
|
|
||||||
.tab-index {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .screen-tab:hover .tab-index {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-screen {
|
.new-screen {
|
||||||
@ -357,7 +310,6 @@
|
|||||||
height: 37px;
|
height: 37px;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
height: 2rem;
|
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
|
@ -9,7 +9,6 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { For } from "tsx-control-statements/components";
|
import { For } from "tsx-control-statements/components";
|
||||||
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model";
|
||||||
import { ReactComponent as AddIcon } from "../../assets/icons/add.svg";
|
import { ReactComponent as AddIcon } from "../../assets/icons/add.svg";
|
||||||
import * as constants from "../../appconst";
|
|
||||||
import { Reorder } from "framer-motion";
|
import { Reorder } from "framer-motion";
|
||||||
import { ScreenTab } from "./tab";
|
import { ScreenTab } from "./tab";
|
||||||
|
|
||||||
@ -181,7 +180,7 @@ class ScreenTabs extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<div className="screen-tabs-container">
|
<div className="screen-tabs-container">
|
||||||
{/* Inner container ensures that hovering over the scrollbar doesn't trigger the hover effect on the tabs. This prevents weird flickering of the icons when the mouse is moved over the scrollbar. */}
|
{/* Inner container ensures that hovering over the scrollbar doesn't trigger the hover effect on the tabs. This prevents weird flickering of the icons when the mouse is moved over the scrollbar. */}
|
||||||
<div className="screen-tabs-container-inner">
|
<div className="screen-tabs-container-inner hideScrollbarUntillHover">
|
||||||
<Reorder.Group
|
<Reorder.Group
|
||||||
className="screen-tabs"
|
className="screen-tabs"
|
||||||
ref={this.tabsRef}
|
ref={this.tabsRef}
|
||||||
|
@ -12,6 +12,8 @@ import * as winston from "winston";
|
|||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "../util/keyutil";
|
||||||
|
import { platform } from "os";
|
||||||
|
|
||||||
const WaveAppPathVarName = "WAVETERM_APP_PATH";
|
const WaveAppPathVarName = "WAVETERM_APP_PATH";
|
||||||
const WaveDevVarName = "WAVETERM_DEV";
|
const WaveDevVarName = "WAVETERM_DEV";
|
||||||
@ -34,7 +36,7 @@ ensureDir(waveHome);
|
|||||||
// these are either "darwin/amd64" or "darwin/arm64"
|
// these are either "darwin/amd64" or "darwin/arm64"
|
||||||
// normalize darwin/x64 to darwin/amd64 for GOARCH compatibility
|
// normalize darwin/x64 to darwin/amd64 for GOARCH compatibility
|
||||||
let unamePlatform = process.platform;
|
let unamePlatform = process.platform;
|
||||||
let unameArch = process.arch;
|
let unameArch: string = process.arch;
|
||||||
if (unameArch == "x64") {
|
if (unameArch == "x64") {
|
||||||
unameArch = "amd64";
|
unameArch = "amd64";
|
||||||
}
|
}
|
||||||
@ -187,15 +189,21 @@ let menuTemplate = [
|
|||||||
{ role: "quit" },
|
{ role: "quit" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "File",
|
|
||||||
submenu: [{ role: "close" },],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
role: "editMenu",
|
role: "editMenu",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "viewMenu",
|
role: "viewMenu",
|
||||||
|
submenu: [
|
||||||
|
{ role: "reload", accelerator: "Option+R" },
|
||||||
|
{ role: "toggleDevTools" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "resetZoom" },
|
||||||
|
{ role: "zoomIn" },
|
||||||
|
{ role: "zoomOut" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "togglefullscreen" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "windowMenu",
|
role: "windowMenu",
|
||||||
@ -244,6 +252,7 @@ function shFrameNavHandler(event: any, url: any) {
|
|||||||
|
|
||||||
function createMainWindow(clientData) {
|
function createMainWindow(clientData) {
|
||||||
let bounds = calcBounds(clientData);
|
let bounds = calcBounds(clientData);
|
||||||
|
setKeyUtilPlatform(platform());
|
||||||
let win = new electron.BrowserWindow({
|
let win = new electron.BrowserWindow({
|
||||||
x: bounds.x,
|
x: bounds.x,
|
||||||
y: bounds.y,
|
y: bounds.y,
|
||||||
@ -261,6 +270,7 @@ function createMainWindow(clientData) {
|
|||||||
let indexHtml = isDev ? "index-dev.html" : "index.html";
|
let indexHtml = isDev ? "index-dev.html" : "index.html";
|
||||||
win.loadFile(path.join(getAppBasePath(), "public", indexHtml));
|
win.loadFile(path.join(getAppBasePath(), "public", indexHtml));
|
||||||
win.webContents.on("before-input-event", (e, input) => {
|
win.webContents.on("before-input-event", (e, input) => {
|
||||||
|
let waveEvent = adaptFromElectronKeyEvent(input);
|
||||||
if (win.isFocused()) {
|
if (win.isFocused()) {
|
||||||
wasActive = true;
|
wasActive = true;
|
||||||
}
|
}
|
||||||
@ -268,50 +278,47 @@ function createMainWindow(clientData) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mods = getMods(input);
|
let mods = getMods(input);
|
||||||
if (input.code == "KeyT" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:t")) {
|
||||||
win.webContents.send("t-cmd", mods);
|
win.webContents.send("t-cmd", mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code == "KeyI" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:i")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!input.alt) {
|
if (!input.alt) {
|
||||||
win.webContents.send("i-cmd", mods);
|
win.webContents.send("i-cmd", mods);
|
||||||
} else {
|
} else {
|
||||||
win.webContents.toggleDevTools();
|
win.webContents.toggleDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code == "KeyR" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:r")) {
|
||||||
if (input.shift) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
win.reload();
|
win.webContents.send("r-cmd", mods);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code == "KeyL" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:l")) {
|
||||||
win.webContents.send("l-cmd", mods);
|
win.webContents.send("l-cmd", mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code == "KeyW" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:w")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
win.webContents.send("w-cmd", mods);
|
win.webContents.send("w-cmd", mods);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code == "KeyH" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:h")) {
|
||||||
win.webContents.send("h-cmd", mods);
|
win.webContents.send("h-cmd", mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.code == "KeyP" && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:p")) {
|
||||||
win.webContents.send("p-cmd", mods);
|
win.webContents.send("p-cmd", mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.meta && (input.code == "ArrowUp" || input.code == "ArrowDown")) {
|
if (checkKeyPressed(waveEvent, "Cmd:ArrowUp") || checkKeyPressed(waveEvent, "Cmd:ArrowDown")) {
|
||||||
if (input.code == "ArrowUp") {
|
if (checkKeyPressed(waveEvent, "Cmd:ArrowUp")) {
|
||||||
win.webContents.send("meta-arrowup");
|
win.webContents.send("meta-arrowup");
|
||||||
} else {
|
} else {
|
||||||
win.webContents.send("meta-arrowdown");
|
win.webContents.send("meta-arrowdown");
|
||||||
@ -319,8 +326,8 @@ function createMainWindow(clientData) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (input.meta && (input.code == "PageUp" || input.code == "PageDown")) {
|
if (checkKeyPressed(waveEvent, "Cmd:PageUp") || checkKeyPressed(waveEvent, "Cmd:PageDown")) {
|
||||||
if (input.code == "PageUp") {
|
if (checkKeyPressed(waveEvent, "Cmd:PageUp")) {
|
||||||
win.webContents.send("meta-pageup");
|
win.webContents.send("meta-pageup");
|
||||||
} else {
|
} else {
|
||||||
win.webContents.send("meta-pagedown");
|
win.webContents.send("meta-pagedown");
|
||||||
@ -336,8 +343,8 @@ function createMainWindow(clientData) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
win.webContents.send("digit-cmd", { digit: digitNum }, mods);
|
win.webContents.send("digit-cmd", { digit: digitNum }, mods);
|
||||||
}
|
}
|
||||||
if ((input.code == "BracketRight" || input.code == "BracketLeft") && input.meta) {
|
if (checkKeyPressed(waveEvent, "Cmd:[") || checkKeyPressed(waveEvent, "Cmd:]")) {
|
||||||
let rel = input.code == "BracketRight" ? 1 : -1;
|
let rel = checkKeyPressed(waveEvent, "Cmd:]") ? 1 : -1;
|
||||||
win.webContents.send("bracket-cmd", { relative: rel }, mods);
|
win.webContents.send("bracket-cmd", { relative: rel }, mods);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
onHCmd: (callback) => ipcRenderer.on("h-cmd", callback),
|
onHCmd: (callback) => ipcRenderer.on("h-cmd", callback),
|
||||||
onWCmd: (callback) => ipcRenderer.on("w-cmd", callback),
|
onWCmd: (callback) => ipcRenderer.on("w-cmd", callback),
|
||||||
onPCmd: (callback) => ipcRenderer.on("p-cmd", callback),
|
onPCmd: (callback) => ipcRenderer.on("p-cmd", callback),
|
||||||
|
onRCmd: (callback) => ipcRenderer.on("r-cmd", callback),
|
||||||
onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback),
|
onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback),
|
||||||
onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback),
|
onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback),
|
||||||
onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback),
|
onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback),
|
||||||
|
@ -84,11 +84,11 @@ import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
|
|||||||
import { MagicLayout } from "../app/magiclayout";
|
import { MagicLayout } from "../app/magiclayout";
|
||||||
import { modalsRegistry } from "../app/common/modals/registry";
|
import { modalsRegistry } from "../app/common/modals/registry";
|
||||||
import * as appconst from "../app/appconst";
|
import * as appconst from "../app/appconst";
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } from "../util/keyutil";
|
||||||
|
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
var GlobalUser = "sawka";
|
|
||||||
const RemotePtyRows = 8; // also in main.tsx
|
const RemotePtyRows = 8; // also in main.tsx
|
||||||
const RemotePtyCols = 80;
|
const RemotePtyCols = 80;
|
||||||
const ProdServerEndpoint = "http://127.0.0.1:1619";
|
const ProdServerEndpoint = "http://127.0.0.1:1619";
|
||||||
@ -203,6 +203,7 @@ type ElectronApi = {
|
|||||||
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onHCmd: (callback: (mods: KeyModsType) => void) => void;
|
onHCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onPCmd: (callback: (mods: KeyModsType) => void) => void;
|
onPCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
|
onRCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onWCmd: (callback: (mods: KeyModsType) => void) => void;
|
onWCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onMenuItemAbout: (callback: () => void) => void;
|
onMenuItemAbout: (callback: () => void) => void;
|
||||||
onMetaArrowUp: (callback: () => void) => void;
|
onMetaArrowUp: (callback: () => void) => void;
|
||||||
@ -252,6 +253,10 @@ class Cmd {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRestartTs(): number {
|
||||||
|
return this.data.get().restartts;
|
||||||
|
}
|
||||||
|
|
||||||
getAsWebCmd(lineid: string): WebCmd {
|
getAsWebCmd(lineid: string): WebCmd {
|
||||||
let cmd = this.data.get();
|
let cmd = this.data.get();
|
||||||
let remote = GlobalModel.getRemote(this.remote.remoteid);
|
let remote = GlobalModel.getRemote(this.remote.remoteid);
|
||||||
@ -327,7 +332,6 @@ class Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDataFromRenderer(data: string, renderer: RendererModel): void {
|
handleDataFromRenderer(data: string, renderer: RendererModel): void {
|
||||||
// console.log("handle data", {data: data});
|
|
||||||
if (!this.isRunning()) {
|
if (!this.isRunning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -372,6 +376,7 @@ class Screen {
|
|||||||
webShareOpts: OV<WebShareOpts>;
|
webShareOpts: OV<WebShareOpts>;
|
||||||
filterRunning: OV<boolean>;
|
filterRunning: OV<boolean>;
|
||||||
statusIndicator: OV<StatusIndicatorLevel>;
|
statusIndicator: OV<StatusIndicatorLevel>;
|
||||||
|
numRunningCmds: OV<number>;
|
||||||
|
|
||||||
constructor(sdata: ScreenDataType) {
|
constructor(sdata: ScreenDataType) {
|
||||||
this.sessionId = sdata.sessionid;
|
this.sessionId = sdata.sessionid;
|
||||||
@ -415,6 +420,9 @@ class Screen {
|
|||||||
this.statusIndicator = mobx.observable.box(StatusIndicatorLevel.None, {
|
this.statusIndicator = mobx.observable.box(StatusIndicatorLevel.None, {
|
||||||
name: "screen-status-indicator",
|
name: "screen-status-indicator",
|
||||||
});
|
});
|
||||||
|
this.numRunningCmds = mobx.observable.box(0, {
|
||||||
|
name: "screen-num-running-cmds",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {}
|
dispose() {}
|
||||||
@ -553,7 +561,6 @@ class Screen {
|
|||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.anchor.set({ anchorLine: anchorLine, anchorOffset: anchorOffset });
|
this.anchor.set({ anchorLine: anchorLine, anchorOffset: anchorOffset });
|
||||||
})();
|
})();
|
||||||
// console.log("set-anchor-fields", anchorLine, anchorOffset, reason);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
|
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
|
||||||
@ -566,7 +573,6 @@ class Screen {
|
|||||||
if (sdata.selectedline != 0) {
|
if (sdata.selectedline != 0) {
|
||||||
sline = this.getLineByNum(sdata.selectedline);
|
sline = this.getLineByNum(sdata.selectedline);
|
||||||
}
|
}
|
||||||
// console.log("refocus", curLineFocus.linenum, "=>", sdata.selectedline, sline.lineid);
|
|
||||||
if (
|
if (
|
||||||
curLineFocus.cmdInputFocus ||
|
curLineFocus.cmdInputFocus ||
|
||||||
(curLineFocus.linenum != null && curLineFocus.linenum != sdata.selectedline)
|
(curLineFocus.linenum != null && curLineFocus.linenum != sdata.selectedline)
|
||||||
@ -794,7 +800,6 @@ class Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLineFocus(lineNum: number, focus: boolean): void {
|
setLineFocus(lineNum: number, focus: boolean): void {
|
||||||
// console.log("SW setLineFocus", lineNum, focus);
|
|
||||||
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
|
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
|
||||||
if (focus && this.selectedLine.get() != lineNum) {
|
if (focus && this.selectedLine.get() != lineNum) {
|
||||||
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
|
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
|
||||||
@ -813,42 +818,59 @@ class Screen {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of running commands for the screen.
|
||||||
|
* @param numRunning The number of running commands.
|
||||||
|
*/
|
||||||
|
setNumRunningCmds(numRunning: number): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.numRunningCmds.set(numRunning);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void {
|
termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void {
|
||||||
if (e.code == "ArrowUp") {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||||
termWrap.terminal.scrollLines(-1);
|
termWrap.terminal.scrollLines(-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "ArrowDown") {
|
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||||
termWrap.terminal.scrollLines(1);
|
termWrap.terminal.scrollLines(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "PageUp") {
|
if (checkKeyPressed(waveEvent, "PageUp")) {
|
||||||
termWrap.terminal.scrollPages(-1);
|
termWrap.terminal.scrollPages(-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "PageDown") {
|
if (checkKeyPressed(waveEvent, "PageDown")) {
|
||||||
termWrap.terminal.scrollPages(1);
|
termWrap.terminal.scrollPages(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isTermCapturedKey(e: any): boolean {
|
isTermCapturedKey(e: any): boolean {
|
||||||
let keys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown"];
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
if (keys.includes(e.code) && keyHasNoMods(e)) {
|
if (
|
||||||
|
checkKeyPressed(waveEvent, "ArrowUp") ||
|
||||||
|
checkKeyPressed(waveEvent, "ArrowDown") ||
|
||||||
|
checkKeyPressed(waveEvent, "PageUp") ||
|
||||||
|
checkKeyPressed(waveEvent, "PageDown")
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
|
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
|
||||||
if (e.type == "keypress" && e.code == "KeyC" && e.shiftKey && e.ctrlKey) {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let sel = termWrap.terminal.getSelection();
|
let sel = termWrap.terminal.getSelection();
|
||||||
navigator.clipboard.writeText(sel);
|
navigator.clipboard.writeText(sel);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (e.type == "keypress" && e.code == "KeyV" && e.shiftKey && e.ctrlKey) {
|
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let p = navigator.clipboard.readText();
|
let p = navigator.clipboard.readText();
|
||||||
@ -880,7 +902,6 @@ class Screen {
|
|||||||
console.log("term-wrap already exists for", this.screenId, lineId);
|
console.log("term-wrap already exists for", this.screenId, lineId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
|
||||||
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
||||||
if (line.contentheight != null && line.contentheight != -1) {
|
if (line.contentheight != null && line.contentheight != -1) {
|
||||||
usedRows = line.contentheight;
|
usedRows = line.contentheight;
|
||||||
@ -910,7 +931,6 @@ class Screen {
|
|||||||
if (this.focusType.get() == "cmd" && this.selectedLine.get() == line.linenum) {
|
if (this.focusType.get() == "cmd" && this.selectedLine.get() == line.linenum) {
|
||||||
termWrap.giveFocus();
|
termWrap.giveFocus();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unloadRenderer(lineId: string) {
|
unloadRenderer(lineId: string) {
|
||||||
@ -936,7 +956,6 @@ class Screen {
|
|||||||
}
|
}
|
||||||
let termWrap = this.getTermWrap(cmd.lineId);
|
let termWrap = this.getTermWrap(cmd.lineId);
|
||||||
if (termWrap == null) {
|
if (termWrap == null) {
|
||||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
|
||||||
let usedRows = GlobalModel.getContentHeight(context);
|
let usedRows = GlobalModel.getContentHeight(context);
|
||||||
if (usedRows != null) {
|
if (usedRows != null) {
|
||||||
return usedRows;
|
return usedRows;
|
||||||
@ -1007,8 +1026,7 @@ class ScreenLines {
|
|||||||
|
|
||||||
getNonArchivedLines(): LineType[] {
|
getNonArchivedLines(): LineType[] {
|
||||||
let rtn: LineType[] = [];
|
let rtn: LineType[] = [];
|
||||||
for (let i = 0; i < this.lines.length; i++) {
|
for (const line of this.lines) {
|
||||||
let line = this.lines[i];
|
|
||||||
if (line.archived) {
|
if (line.archived) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1029,8 +1047,8 @@ class ScreenLines {
|
|||||||
(l: LineType) => sprintf("%013d:%s", l.ts, l.lineid)
|
(l: LineType) => sprintf("%013d:%s", l.ts, l.lineid)
|
||||||
);
|
);
|
||||||
let cmds = slines.cmds || [];
|
let cmds = slines.cmds || [];
|
||||||
for (let i = 0; i < cmds.length; i++) {
|
for (const cmd of cmds) {
|
||||||
this.cmds[cmds[i].lineid] = new Cmd(cmds[i]);
|
this.cmds[cmd.lineid] = new Cmd(cmd);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@ -1048,22 +1066,37 @@ class ScreenLines {
|
|||||||
return this.cmds[lineId];
|
return this.cmds[lineId];
|
||||||
}
|
}
|
||||||
|
|
||||||
getRunningCmdLines(): LineType[] {
|
/**
|
||||||
|
* Get all running cmds in the screen.
|
||||||
|
* @param returnFirst If true, return the first running cmd found.
|
||||||
|
* @returns An array of running cmds, or the first running cmd if returnFirst is true.
|
||||||
|
*/
|
||||||
|
getRunningCmdLines(returnFirst?: boolean): LineType[] {
|
||||||
let rtn: LineType[] = [];
|
let rtn: LineType[] = [];
|
||||||
for (let i = 0; i < this.lines.length; i++) {
|
for (const line of this.lines) {
|
||||||
let line = this.lines[i];
|
const cmd = this.getCmd(line.lineid);
|
||||||
let cmd = this.getCmd(line.lineid);
|
|
||||||
if (cmd == null) {
|
if (cmd == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let status = cmd.getStatus();
|
const status = cmd.getStatus();
|
||||||
if (cmdStatusIsRunning(status)) {
|
if (cmdStatusIsRunning(status)) {
|
||||||
|
if (returnFirst) {
|
||||||
|
return [line];
|
||||||
|
}
|
||||||
rtn.push(line);
|
rtn.push(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rtn;
|
return rtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are any running cmds in the screen.
|
||||||
|
* @returns True if there are any running cmds.
|
||||||
|
*/
|
||||||
|
hasRunningCmdLines(): boolean {
|
||||||
|
return this.getRunningCmdLines(true).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
updateCmd(cmd: CmdDataType): void {
|
updateCmd(cmd: CmdDataType): void {
|
||||||
if (cmd.remove) {
|
if (cmd.remove) {
|
||||||
throw new Error("cannot remove cmd with updateCmd call [" + cmd.lineid + "]");
|
throw new Error("cannot remove cmd with updateCmd call [" + cmd.lineid + "]");
|
||||||
@ -1072,7 +1105,6 @@ class ScreenLines {
|
|||||||
if (origCmd != null) {
|
if (origCmd != null) {
|
||||||
origCmd.setCmd(cmd);
|
origCmd.setCmd(cmd);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeCmd(cmd: CmdDataType): void {
|
mergeCmd(cmd: CmdDataType): void {
|
||||||
@ -1086,7 +1118,6 @@ class ScreenLines {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
origCmd.setCmd(cmd);
|
origCmd.setCmd(cmd);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) {
|
addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) {
|
||||||
@ -1315,11 +1346,9 @@ class InputModel {
|
|||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
this.inputFocused.set(true);
|
this.inputFocused.set(true);
|
||||||
this.lineFocused.set(false);
|
this.lineFocused.set(false);
|
||||||
} else {
|
} else if (this.inputFocused.get()) {
|
||||||
if (this.inputFocused.get()) {
|
|
||||||
this.inputFocused.set(false);
|
this.inputFocused.set(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1328,11 +1357,9 @@ class InputModel {
|
|||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
this.inputFocused.set(false);
|
this.inputFocused.set(false);
|
||||||
this.lineFocused.set(true);
|
this.lineFocused.set(true);
|
||||||
} else {
|
} else if (this.lineFocused.get()) {
|
||||||
if (this.lineFocused.get()) {
|
|
||||||
this.lineFocused.set(false);
|
this.lineFocused.set(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1564,14 +1591,12 @@ class InputModel {
|
|||||||
curRemote = { ownerid: "", name: "", remoteid: "" };
|
curRemote = { ownerid: "", name: "", remoteid: "" };
|
||||||
}
|
}
|
||||||
curRemote = mobx.toJS(curRemote);
|
curRemote = mobx.toJS(curRemote);
|
||||||
for (let i = 0; i < hitems.length; i++) {
|
for (const hitem of hitems) {
|
||||||
let hitem = hitems[i];
|
|
||||||
if (hitem.ismetacmd) {
|
if (hitem.ismetacmd) {
|
||||||
if (!opts.includeMeta) {
|
if (!opts.includeMeta) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (opts.limitRemoteInstance) {
|
||||||
if (opts.limitRemoteInstance) {
|
|
||||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1593,7 +1618,6 @@ class InputModel {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!isBlank(opts.queryStr)) {
|
if (!isBlank(opts.queryStr)) {
|
||||||
if (isBlank(hitem.cmdstr)) {
|
if (isBlank(hitem.cmdstr)) {
|
||||||
continue;
|
continue;
|
||||||
@ -1642,7 +1666,6 @@ class InputModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
historyDiv.scrollTop = elemOffset - titleHeight - buffer;
|
historyDiv.scrollTop = elemOffset - titleHeight - buffer;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1728,7 +1751,7 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAIChatFocus() {
|
setAIChatFocus() {
|
||||||
if (this.aiChatTextAreaRef != null && this.aiChatTextAreaRef.current != null) {
|
if (this.aiChatTextAreaRef?.current != null) {
|
||||||
this.aiChatTextAreaRef.current.focus();
|
this.aiChatTextAreaRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1759,7 +1782,7 @@ class InputModel {
|
|||||||
this.codeSelectSelectedIndex.set(blockIndex);
|
this.codeSelectSelectedIndex.set(blockIndex);
|
||||||
let currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
let currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
||||||
if (currentRef != null) {
|
if (currentRef != null) {
|
||||||
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
if (this.aiChatWindowRef?.current != null) {
|
||||||
let chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
let chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
||||||
let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
||||||
let elemTop = currentRef.offsetTop;
|
let elemTop = currentRef.offsetTop;
|
||||||
@ -1789,7 +1812,7 @@ class InputModel {
|
|||||||
let incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
|
let incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
|
||||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
|
||||||
this.codeSelectDeselectAll();
|
this.codeSelectDeselectAll();
|
||||||
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
if (this.aiChatWindowRef?.current != null) {
|
||||||
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1813,7 +1836,7 @@ class InputModel {
|
|||||||
let decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
let decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
||||||
if (decBlockIndex < 0) {
|
if (decBlockIndex < 0) {
|
||||||
this.codeSelectDeselectAll(this.codeSelectTop);
|
this.codeSelectDeselectAll(this.codeSelectTop);
|
||||||
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
if (this.aiChatWindowRef?.current != null) {
|
||||||
this.aiChatWindowRef.current.scrollTop = 0;
|
this.aiChatWindowRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1855,8 +1878,7 @@ class InputModel {
|
|||||||
clearAIAssistantChat(): void {
|
clearAIAssistantChat(): void {
|
||||||
let prtn = GlobalModel.submitChatInfoCommand("", "", true);
|
let prtn = GlobalModel.submitChatInfoCommand("", "", true);
|
||||||
prtn.then((rtn) => {
|
prtn.then((rtn) => {
|
||||||
if (rtn.success) {
|
if (!rtn.success) {
|
||||||
} else {
|
|
||||||
console.log("submit chat command error: " + rtn.error);
|
console.log("submit chat command error: " + rtn.error);
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@ -1979,7 +2001,6 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCurLine(): string {
|
getCurLine(): string {
|
||||||
let model = GlobalModel;
|
|
||||||
let hidx = this.historyIndex.get();
|
let hidx = this.historyIndex.get();
|
||||||
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
|
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
|
||||||
return this.modHistory[hidx];
|
return this.modHistory[hidx];
|
||||||
@ -2190,7 +2211,6 @@ class SpecialLineContainer {
|
|||||||
console.log("term-wrap already exists for", line.screenid, lineId);
|
console.log("term-wrap already exists for", line.screenid, lineId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
|
||||||
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
||||||
if (line.contentheight != null && line.contentheight != -1) {
|
if (line.contentheight != null && line.contentheight != -1) {
|
||||||
usedRows = line.contentheight;
|
usedRows = line.contentheight;
|
||||||
@ -2214,7 +2234,6 @@ class SpecialLineContainer {
|
|||||||
onUpdateContentHeight: null,
|
onUpdateContentHeight: null,
|
||||||
});
|
});
|
||||||
this.terminal = termWrap;
|
this.terminal = termWrap;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRenderer(lineId: string, renderer: RendererModel): void {
|
registerRenderer(lineId: string, renderer: RendererModel): void {
|
||||||
@ -2324,8 +2343,6 @@ class HistoryViewModel {
|
|||||||
|
|
||||||
specialLineContainer: SpecialLineContainer;
|
specialLineContainer: SpecialLineContainer;
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
closeView(): void {
|
closeView(): void {
|
||||||
GlobalModel.showSessionView();
|
GlobalModel.showSessionView();
|
||||||
setTimeout(() => GlobalModel.inputModel.giveFocus(), 50);
|
setTimeout(() => GlobalModel.inputModel.giveFocus(), 50);
|
||||||
@ -2335,8 +2352,7 @@ class HistoryViewModel {
|
|||||||
if (isBlank(lineId)) {
|
if (isBlank(lineId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.historyItemLines.length; i++) {
|
for (const line of this.historyItemLines) {
|
||||||
let line = this.historyItemLines[i];
|
|
||||||
if (line.lineid == lineId) {
|
if (line.lineid == lineId) {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
@ -2348,8 +2364,7 @@ class HistoryViewModel {
|
|||||||
if (isBlank(lineId)) {
|
if (isBlank(lineId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.historyItemCmds.length; i++) {
|
for (const cmd of this.historyItemCmds) {
|
||||||
let cmd = this.historyItemCmds[i];
|
|
||||||
if (cmd.lineid == lineId) {
|
if (cmd.lineid == lineId) {
|
||||||
return new Cmd(cmd);
|
return new Cmd(cmd);
|
||||||
}
|
}
|
||||||
@ -2361,8 +2376,7 @@ class HistoryViewModel {
|
|||||||
if (isBlank(historyId)) {
|
if (isBlank(historyId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
for (const hitem of this.items) {
|
||||||
let hitem = this.items[i];
|
|
||||||
if (hitem.historyid == historyId) {
|
if (hitem.historyid == historyId) {
|
||||||
return hitem;
|
return hitem;
|
||||||
}
|
}
|
||||||
@ -2421,7 +2435,6 @@ class HistoryViewModel {
|
|||||||
prtn.then((result: CommandRtnType) => {
|
prtn.then((result: CommandRtnType) => {
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
GlobalModel.showAlert({ message: "Error removing history lines." });
|
GlobalModel.showAlert({ message: "Error removing history lines." });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let params = this._getSearchParams();
|
let params = this._getSearchParams();
|
||||||
@ -2436,8 +2449,8 @@ class HistoryViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getSearchParams(newOffset?: number, newRawOffset?: number): HistorySearchParams {
|
_getSearchParams(newOffset?: number, newRawOffset?: number): HistorySearchParams {
|
||||||
let offset = newOffset != null ? newOffset : this.offset.get();
|
let offset = newOffset ?? this.offset.get();
|
||||||
let rawOffset = newRawOffset != null ? newRawOffset : this.curRawOffset;
|
let rawOffset = newRawOffset ?? this.curRawOffset;
|
||||||
let opts: HistorySearchParams = {
|
let opts: HistorySearchParams = {
|
||||||
offset: offset,
|
offset: offset,
|
||||||
rawOffset: rawOffset,
|
rawOffset: rawOffset,
|
||||||
@ -2559,7 +2572,8 @@ class HistoryViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDocKeyDown(e: any): void {
|
handleDocKeyDown(e: any): void {
|
||||||
if (e.code == "Escape") {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.closeView();
|
this.closeView();
|
||||||
return;
|
return;
|
||||||
@ -2731,8 +2745,7 @@ class BookmarksModel {
|
|||||||
if (bookmarkId == null) {
|
if (bookmarkId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.bookmarks.length; i++) {
|
for (const bm of this.bookmarks) {
|
||||||
let bm = this.bookmarks[i];
|
|
||||||
if (bm.bookmarkid == bookmarkId) {
|
if (bm.bookmarkid == bookmarkId) {
|
||||||
return bm;
|
return bm;
|
||||||
}
|
}
|
||||||
@ -2800,7 +2813,8 @@ class BookmarksModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDocKeyDown(e: any): void {
|
handleDocKeyDown(e: any): void {
|
||||||
if (e.code == "Escape") {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.editingBookmark.get() != null) {
|
if (this.editingBookmark.get() != null) {
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
@ -2812,7 +2826,7 @@ class BookmarksModel {
|
|||||||
if (this.editingBookmark.get() != null) {
|
if (this.editingBookmark.get() != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Backspace" || e.code == "Delete") {
|
if (checkKeyPressed(waveEvent, "Backspace") || checkKeyPressed(waveEvent, "Delete")) {
|
||||||
if (this.activeBookmark.get() == null) {
|
if (this.activeBookmark.get() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2820,7 +2834,13 @@ class BookmarksModel {
|
|||||||
this.handleDeleteBookmark(this.activeBookmark.get());
|
this.handleDeleteBookmark(this.activeBookmark.get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "ArrowUp" || e.code == "ArrowDown" || e.code == "PageUp" || e.code == "PageDown") {
|
|
||||||
|
if (
|
||||||
|
checkKeyPressed(waveEvent, "ArrowUp") ||
|
||||||
|
checkKeyPressed(waveEvent, "ArrowDown") ||
|
||||||
|
checkKeyPressed(waveEvent, "PageUp") ||
|
||||||
|
checkKeyPressed(waveEvent, "PageDown")
|
||||||
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.bookmarks.length == 0) {
|
if (this.bookmarks.length == 0) {
|
||||||
return;
|
return;
|
||||||
@ -2844,14 +2864,14 @@ class BookmarksModel {
|
|||||||
})();
|
})();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Enter") {
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
if (this.activeBookmark.get() == null) {
|
if (this.activeBookmark.get() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.useBookmark(this.activeBookmark.get());
|
this.useBookmark(this.activeBookmark.get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyE") {
|
if (checkKeyPressed(waveEvent, "e")) {
|
||||||
if (this.activeBookmark.get() == null) {
|
if (this.activeBookmark.get() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2859,13 +2879,12 @@ class BookmarksModel {
|
|||||||
this.handleEditBookmark(this.activeBookmark.get());
|
this.handleEditBookmark(this.activeBookmark.get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyC") {
|
if (checkKeyPressed(waveEvent, "c")) {
|
||||||
if (this.activeBookmark.get() == null) {
|
if (this.activeBookmark.get() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleCopyBookmark(this.activeBookmark.get());
|
this.handleCopyBookmark(this.activeBookmark.get());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3401,6 +3420,7 @@ class Model {
|
|||||||
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
|
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
|
||||||
name: "model-wavesrv-running",
|
name: "model-wavesrv-running",
|
||||||
});
|
});
|
||||||
|
this.platform = this.getPlatform();
|
||||||
this.termFontSize = mobx.computed(() => {
|
this.termFontSize = mobx.computed(() => {
|
||||||
let cdata = this.clientData.get();
|
let cdata = this.clientData.get();
|
||||||
if (cdata == null || cdata.feopts == null || cdata.feopts.termfontsize == null) {
|
if (cdata == null || cdata.feopts == null || cdata.feopts.termfontsize == null) {
|
||||||
@ -3421,6 +3441,7 @@ class Model {
|
|||||||
getApi().onHCmd(this.onHCmd.bind(this));
|
getApi().onHCmd(this.onHCmd.bind(this));
|
||||||
getApi().onPCmd(this.onPCmd.bind(this));
|
getApi().onPCmd(this.onPCmd.bind(this));
|
||||||
getApi().onWCmd(this.onWCmd.bind(this));
|
getApi().onWCmd(this.onWCmd.bind(this));
|
||||||
|
getApi().onRCmd(this.onRCmd.bind(this));
|
||||||
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
|
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
|
||||||
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
|
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
|
||||||
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
|
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
|
||||||
@ -3444,9 +3465,14 @@ class Model {
|
|||||||
return this.platform;
|
return this.platform;
|
||||||
}
|
}
|
||||||
this.platform = getApi().getPlatform();
|
this.platform = getApi().getPlatform();
|
||||||
|
setKeyUtilPlatform(this.platform);
|
||||||
return this.platform;
|
return this.platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testGlobalModel() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
needsTos(): boolean {
|
needsTos(): boolean {
|
||||||
let cdata = this.clientData.get();
|
let cdata = this.clientData.get();
|
||||||
if (cdata == null) {
|
if (cdata == null) {
|
||||||
@ -3588,16 +3614,17 @@ class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
docKeyDownHandler(e: KeyboardEvent) {
|
docKeyDownHandler(e: KeyboardEvent) {
|
||||||
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
if (isModKeyPress(e)) {
|
if (isModKeyPress(e)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.alertMessage.get() != null) {
|
if (this.alertMessage.get() != null) {
|
||||||
if (e.code == "Escape") {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.cancelAlert();
|
this.cancelAlert();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Enter") {
|
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.confirmAlert();
|
this.confirmAlert();
|
||||||
return;
|
return;
|
||||||
@ -3620,7 +3647,7 @@ class Model {
|
|||||||
this.historyViewModel.handleDocKeyDown(e);
|
this.historyViewModel.handleDocKeyDown(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "Escape") {
|
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.activeMainView.get() == "webshare") {
|
if (this.activeMainView.get() == "webshare") {
|
||||||
this.showSessionView();
|
this.showSessionView();
|
||||||
@ -3636,16 +3663,11 @@ class Model {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyB" && e.getModifierState("Meta")) {
|
if (checkKeyPressed(waveEvent, "Cmd:b")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
GlobalCommandRunner.bookmarksView();
|
GlobalCommandRunner.bookmarksView();
|
||||||
}
|
}
|
||||||
if (
|
if (this.activeMainView.get() == "session" && checkKeyPressed(waveEvent, "Cmd:Ctrl:s")) {
|
||||||
this.activeMainView.get() == "session" &&
|
|
||||||
e.code == "KeyS" &&
|
|
||||||
e.getModifierState("Meta") &&
|
|
||||||
e.getModifierState("Control")
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let activeScreen = this.getActiveScreen();
|
let activeScreen = this.getActiveScreen();
|
||||||
if (activeScreen != null) {
|
if (activeScreen != null) {
|
||||||
@ -3657,7 +3679,7 @@ class Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.code == "KeyD" && e.getModifierState("Meta")) {
|
if (checkKeyPressed(waveEvent, "Cmd:d")) {
|
||||||
let ranDelete = this.deleteActiveLine();
|
let ranDelete = this.deleteActiveLine();
|
||||||
if (ranDelete) {
|
if (ranDelete) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -3691,6 +3713,9 @@ class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onWCmd(e: any, mods: KeyModsType) {
|
onWCmd(e: any, mods: KeyModsType) {
|
||||||
|
if (this.activeMainView.get() != "session") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let activeScreen = this.getActiveScreen();
|
let activeScreen = this.getActiveScreen();
|
||||||
if (activeScreen == null) {
|
if (activeScreen == null) {
|
||||||
return;
|
return;
|
||||||
@ -3707,6 +3732,27 @@ class Model {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRCmd(e: any, mods: KeyModsType) {
|
||||||
|
if (this.activeMainView.get() != "session") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let activeScreen = this.getActiveScreen();
|
||||||
|
if (activeScreen == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mods.shift) {
|
||||||
|
// restart last line
|
||||||
|
GlobalCommandRunner.lineRestart("E", true);
|
||||||
|
} else {
|
||||||
|
// restart selected line
|
||||||
|
let selectedLine = activeScreen.selectedLine.get();
|
||||||
|
if (selectedLine == null || selectedLine == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GlobalCommandRunner.lineRestart(String(selectedLine), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearModals(): boolean {
|
clearModals(): boolean {
|
||||||
let didSomething = false;
|
let didSomething = false;
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
@ -3739,9 +3785,9 @@ class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLocalRemote(): RemoteType {
|
getLocalRemote(): RemoteType {
|
||||||
for (let i = 0; i < this.remotes.length; i++) {
|
for (const remote of this.remotes) {
|
||||||
if (this.remotes[i].local) {
|
if (remote.local) {
|
||||||
return this.remotes[i];
|
return remote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -3851,7 +3897,6 @@ class Model {
|
|||||||
let wasRunning = cmdStatusIsRunning(origStatus);
|
let wasRunning = cmdStatusIsRunning(origStatus);
|
||||||
let isRunning = cmdStatusIsRunning(newStatus);
|
let isRunning = cmdStatusIsRunning(newStatus);
|
||||||
if (wasRunning && !isRunning) {
|
if (wasRunning && !isRunning) {
|
||||||
// console.log("cmd status", screenId, lineId, origStatus, "=>", newStatus);
|
|
||||||
let ptr = this.getActiveLine(screenId, lineId);
|
let ptr = this.getActiveLine(screenId, lineId);
|
||||||
if (ptr != null) {
|
if (ptr != null) {
|
||||||
let screen = ptr.screen;
|
let screen = ptr.screen;
|
||||||
@ -3994,8 +4039,8 @@ class Model {
|
|||||||
this.updateCmd(update.cmd);
|
this.updateCmd(update.cmd);
|
||||||
}
|
}
|
||||||
if ("lines" in update) {
|
if ("lines" in update) {
|
||||||
for (let i = 0; i < update.lines.length; i++) {
|
for (const line of update.lines) {
|
||||||
this.addLineCmd(update.lines[i], null, interactive);
|
this.addLineCmd(line, null, interactive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("screenlines" in update) {
|
if ("screenlines" in update) {
|
||||||
@ -4007,8 +4052,8 @@ class Model {
|
|||||||
}
|
}
|
||||||
this.updateRemotes(update.remotes);
|
this.updateRemotes(update.remotes);
|
||||||
// This code's purpose is to show view remote connection modal when a new connection is added
|
// This code's purpose is to show view remote connection modal when a new connection is added
|
||||||
if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) {
|
if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) {
|
||||||
GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid);
|
GlobalModel.remotesModel.openReadModal(update.remotes[0].remoteid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("mainview" in update) {
|
if ("mainview" in update) {
|
||||||
@ -4059,22 +4104,28 @@ class Model {
|
|||||||
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
|
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
|
||||||
}
|
}
|
||||||
if ("screenstatusindicator" in update) {
|
if ("screenstatusindicator" in update) {
|
||||||
this.getScreenById_single(update.screenstatusindicator.screenid)?.setStatusIndicator(update.screenstatusindicator.status);
|
this.getScreenById_single(update.screenstatusindicator.screenid)?.setStatusIndicator(
|
||||||
|
update.screenstatusindicator.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("screennumrunningcommands" in update) {
|
||||||
|
this.getScreenById_single(update.screennumrunningcommands.screenid)?.setNumRunningCmds(
|
||||||
|
update.screennumrunningcommands.num
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ("userinputrequest" in update) {
|
if ("userinputrequest" in update) {
|
||||||
let userInputRequest: UserInputRequest = update.userinputrequest;
|
let userInputRequest: UserInputRequest = update.userinputrequest;
|
||||||
let userInputResponse: UserInputResponse = {
|
let userInputResponse: UserInputResponse = {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "what wonderful weather we're having",
|
text: "what wonderful weather we're having",
|
||||||
}
|
};
|
||||||
let userInputResponsePacket: UserInputResponsePacket = {
|
let userInputResponsePacket: UserInputResponsePacket = {
|
||||||
type: "userinputresp",
|
type: "userinputresp",
|
||||||
requestid: userInputRequest.requestid,
|
requestid: userInputRequest.requestid,
|
||||||
response: userInputResponse,
|
response: userInputResponse,
|
||||||
}
|
};
|
||||||
this.ws.pushMessage(userInputResponsePacket);
|
this.ws.pushMessage(userInputResponsePacket);
|
||||||
}
|
}
|
||||||
// console.log("run-update>", Date.now(), interactive, update);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRemotes(remotes: RemoteType[]): void {
|
updateRemotes(remotes: RemoteType[]): void {
|
||||||
@ -4087,8 +4138,7 @@ class Model {
|
|||||||
|
|
||||||
getSessionNames(): Record<string, string> {
|
getSessionNames(): Record<string, string> {
|
||||||
let rtn: Record<string, string> = {};
|
let rtn: Record<string, string> = {};
|
||||||
for (let i = 0; i < this.sessionList.length; i++) {
|
for (const session of this.sessionList) {
|
||||||
let session = this.sessionList[i];
|
|
||||||
rtn[session.sessionId] = session.name.get();
|
rtn[session.sessionId] = session.name.get();
|
||||||
}
|
}
|
||||||
return rtn;
|
return rtn;
|
||||||
@ -4106,9 +4156,9 @@ class Model {
|
|||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.sessionList.length; i++) {
|
for (const session of this.sessionList) {
|
||||||
if (this.sessionList[i].sessionId == sessionId) {
|
if (session.sessionId == sessionId) {
|
||||||
return this.sessionList[i];
|
return session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -4135,7 +4185,6 @@ class Model {
|
|||||||
let newWindow = new ScreenLines(slines.screenid);
|
let newWindow = new ScreenLines(slines.screenid);
|
||||||
this.screenLines.set(slines.screenid, newWindow);
|
this.screenLines.set(slines.screenid, newWindow);
|
||||||
newWindow.updateData(slines, load);
|
newWindow.updateData(slines, load);
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
existingWin.updateData(slines, load);
|
existingWin.updateData(slines, load);
|
||||||
existingWin.loaded.set(true);
|
existingWin.loaded.set(true);
|
||||||
@ -4183,12 +4232,28 @@ class Model {
|
|||||||
return session.getActiveScreen();
|
return session.getActiveScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCmdRestart(cmd: CmdDataType) {
|
||||||
|
if (cmd == null || !cmd.restarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let screen = this.screenMap.get(cmd.screenid);
|
||||||
|
if (screen == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let termWrap = screen.getTermWrap(cmd.lineid);
|
||||||
|
if (termWrap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
termWrap.reload(0);
|
||||||
|
}
|
||||||
|
|
||||||
addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) {
|
addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) {
|
||||||
let slines = this.getScreenLinesById(line.screenid);
|
let slines = this.getScreenLinesById(line.screenid);
|
||||||
if (slines == null) {
|
if (slines == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
slines.addLineCmd(line, cmd, interactive);
|
slines.addLineCmd(line, cmd, interactive);
|
||||||
|
this.handleCmdRestart(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd(cmd: CmdDataType) {
|
updateCmd(cmd: CmdDataType) {
|
||||||
@ -4196,6 +4261,7 @@ class Model {
|
|||||||
if (slines != null) {
|
if (slines != null) {
|
||||||
slines.updateCmd(cmd);
|
slines.updateCmd(cmd);
|
||||||
}
|
}
|
||||||
|
this.handleCmdRestart(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
isInfoUpdate(update: UpdateMessage): boolean {
|
isInfoUpdate(update: UpdateMessage): boolean {
|
||||||
@ -4292,7 +4358,7 @@ class Model {
|
|||||||
metacmd: metaCmd,
|
metacmd: metaCmd,
|
||||||
metasubcmd: metaSubCmd,
|
metasubcmd: metaSubCmd,
|
||||||
args: args,
|
args: args,
|
||||||
kwargs: Object.assign({}, kwargs),
|
kwargs: { ...kwargs },
|
||||||
uicontext: this.getUIContext(),
|
uicontext: this.getUIContext(),
|
||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
};
|
};
|
||||||
@ -4367,7 +4433,6 @@ class Model {
|
|||||||
}
|
}
|
||||||
let slines: ScreenLinesType = data.data;
|
let slines: ScreenLinesType = data.data;
|
||||||
this.updateScreenLines(slines, true);
|
this.updateScreenLines(slines, true);
|
||||||
return;
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.errorHandler(sprintf("getting screen-lines=%s", newWin.screenId), err, false);
|
this.errorHandler(sprintf("getting screen-lines=%s", newWin.screenId), err, false);
|
||||||
@ -4389,8 +4454,7 @@ class Model {
|
|||||||
|
|
||||||
getRemoteNames(): Record<string, string> {
|
getRemoteNames(): Record<string, string> {
|
||||||
let rtn: Record<string, string> = {};
|
let rtn: Record<string, string> = {};
|
||||||
for (let i = 0; i < this.remotes.length; i++) {
|
for (const remote of this.remotes) {
|
||||||
let remote = this.remotes[i];
|
|
||||||
if (!isBlank(remote.remotealias)) {
|
if (!isBlank(remote.remotealias)) {
|
||||||
rtn[remote.remoteid] = remote.remotealias;
|
rtn[remote.remoteid] = remote.remotealias;
|
||||||
} else {
|
} else {
|
||||||
@ -4401,9 +4465,9 @@ class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRemoteByName(name: string): RemoteType {
|
getRemoteByName(name: string): RemoteType {
|
||||||
for (let i = 0; i < this.remotes.length; i++) {
|
for (const remote of this.remotes) {
|
||||||
if (this.remotes[i].remotecanonicalname == name || this.remotes[i].remotealias == name) {
|
if (remote.remotecanonicalname == name || remote.remotealias == name) {
|
||||||
return this.remotes[i];
|
return remote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -4434,9 +4498,9 @@ class Model {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let line: LineType = null;
|
let line: LineType = null;
|
||||||
for (let i = 0; i < slines.lines.length; i++) {
|
for (const element of slines.lines) {
|
||||||
if (slines.lines[i].lineid == lineid) {
|
if (element.lineid == lineid) {
|
||||||
line = slines.lines[i];
|
line = element;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4458,7 +4522,7 @@ class Model {
|
|||||||
console.log("[error]", str, err);
|
console.log("[error]", str, err);
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
let errMsg = "error running command";
|
let errMsg = "error running command";
|
||||||
if (err != null && err.message) {
|
if (err?.message) {
|
||||||
errMsg = err.message;
|
errMsg = err.message;
|
||||||
}
|
}
|
||||||
this.inputModel.flashInfoMsg({ infoerror: errMsg }, null);
|
this.inputModel.flashInfoMsg({ infoerror: errMsg }, null);
|
||||||
@ -4515,13 +4579,10 @@ class Model {
|
|||||||
let url = new URL(GlobalModel.getBaseHostPort() + "/api/read-file?" + usp.toString());
|
let url = new URL(GlobalModel.getBaseHostPort() + "/api/read-file?" + usp.toString());
|
||||||
let fetchHeaders = this.getFetchHeaders();
|
let fetchHeaders = this.getFetchHeaders();
|
||||||
let fileInfo: T.FileInfoType = null;
|
let fileInfo: T.FileInfoType = null;
|
||||||
let contentType: string = null;
|
|
||||||
let isError = false;
|
|
||||||
let badResponseStr: string = null;
|
let badResponseStr: string = null;
|
||||||
let prtn = fetch(url, { method: "get", headers: fetchHeaders })
|
let prtn = fetch(url, { method: "get", headers: fetchHeaders })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
isError = true;
|
|
||||||
badResponseStr = sprintf(
|
badResponseStr = sprintf(
|
||||||
"Bad fetch response for /api/read-file: %d %s",
|
"Bad fetch response for /api/read-file: %d %s",
|
||||||
resp.status,
|
resp.status,
|
||||||
@ -4529,7 +4590,6 @@ class Model {
|
|||||||
);
|
);
|
||||||
return resp.text() as any;
|
return resp.text() as any;
|
||||||
}
|
}
|
||||||
contentType = resp.headers.get("Content-Type");
|
|
||||||
fileInfo = JSON.parse(base64ToString(resp.headers.get("X-FileInfo")));
|
fileInfo = JSON.parse(base64ToString(resp.headers.get("X-FileInfo")));
|
||||||
return resp.blob();
|
return resp.blob();
|
||||||
})
|
})
|
||||||
@ -4547,7 +4607,6 @@ class Model {
|
|||||||
throw new Error(badResponseStr);
|
throw new Error(badResponseStr);
|
||||||
}
|
}
|
||||||
throw new Error(textError);
|
throw new Error(textError);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return prtn;
|
return prtn;
|
||||||
@ -4576,15 +4635,13 @@ class Model {
|
|||||||
let prtn = fetch(url, { method: "post", headers: fetchHeaders, body: formData });
|
let prtn = fetch(url, { method: "post", headers: fetchHeaders, body: formData });
|
||||||
return prtn
|
return prtn
|
||||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||||
.then((data) => {
|
.then((_) => {
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommandRunner {
|
class CommandRunner {
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
loadHistory(show: boolean, htype: string) {
|
loadHistory(show: boolean, htype: string) {
|
||||||
let kwargs = { nohist: "1" };
|
let kwargs = { nohist: "1" };
|
||||||
if (!show) {
|
if (!show) {
|
||||||
@ -4642,6 +4699,10 @@ class CommandRunner {
|
|||||||
return GlobalModel.submitCommand("line", "delete", [lineArg], { nohist: "1" }, interactive);
|
return GlobalModel.submitCommand("line", "delete", [lineArg], { nohist: "1" }, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lineRestart(lineArg: string, interactive: boolean): Promise<CommandRtnType> {
|
||||||
|
return GlobalModel.submitCommand("line", "restart", [lineArg], { nohist: "1" }, interactive);
|
||||||
|
}
|
||||||
|
|
||||||
lineSet(lineArg: string, opts: { renderer?: string }): Promise<CommandRtnType> {
|
lineSet(lineArg: string, opts: { renderer?: string }): Promise<CommandRtnType> {
|
||||||
let kwargs = { nohist: "1" };
|
let kwargs = { nohist: "1" };
|
||||||
if ("renderer" in opts) {
|
if ("renderer" in opts) {
|
||||||
|
@ -10,6 +10,7 @@ import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
|||||||
import Split from "react-split-it";
|
import Split from "react-split-it";
|
||||||
import loader from "@monaco-editor/loader";
|
import loader from "@monaco-editor/loader";
|
||||||
loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } });
|
loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } });
|
||||||
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||||
|
|
||||||
import "./code.less";
|
import "./code.less";
|
||||||
|
|
||||||
@ -152,17 +153,18 @@ class SourceCodeRenderer extends React.Component<
|
|||||||
this.setInitialLanguage(editor);
|
this.setInitialLanguage(editor);
|
||||||
this.setEditorHeight();
|
this.setEditorHeight();
|
||||||
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
|
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
|
||||||
if (e.code === "KeyS" && e.metaKey && this.state.isSave) {
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e.browserEvent);
|
||||||
|
if (checkKeyPressed(waveEvent, "Cmd:s") && this.state.isSave) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.doSave();
|
this.doSave();
|
||||||
}
|
}
|
||||||
if (e.code === "KeyD" && e.metaKey) {
|
if (checkKeyPressed(waveEvent, "Cmd:d")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.doClose();
|
this.doClose();
|
||||||
}
|
}
|
||||||
if (e.code === "KeyP" && e.metaKey) {
|
if (checkKeyPressed(waveEvent, "Cmd:p")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.togglePreview();
|
this.togglePreview();
|
||||||
|
@ -245,12 +245,14 @@ type CmdDataType = {
|
|||||||
status: string;
|
status: string;
|
||||||
cmdpid: number;
|
cmdpid: number;
|
||||||
remotepid: number;
|
remotepid: number;
|
||||||
|
restartts: number;
|
||||||
donets: number;
|
donets: number;
|
||||||
exitcode: number;
|
exitcode: number;
|
||||||
durationms: number;
|
durationms: number;
|
||||||
runout: any[];
|
runout: any[];
|
||||||
rtnstate: boolean;
|
rtnstate: boolean;
|
||||||
remove?: boolean;
|
remove?: boolean;
|
||||||
|
restarted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PtyDataUpdateType = {
|
type PtyDataUpdateType = {
|
||||||
@ -295,7 +297,12 @@ enum StatusIndicatorLevel {
|
|||||||
type ScreenStatusIndicatorUpdateType = {
|
type ScreenStatusIndicatorUpdateType = {
|
||||||
screenid: string;
|
screenid: string;
|
||||||
status: StatusIndicatorLevel;
|
status: StatusIndicatorLevel;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type ScreenNumRunningCommandsUpdateType = {
|
||||||
|
screenid: string;
|
||||||
|
num: number;
|
||||||
|
};
|
||||||
|
|
||||||
type ModelUpdateType = {
|
type ModelUpdateType = {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
@ -320,6 +327,7 @@ type ModelUpdateType = {
|
|||||||
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
|
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
|
||||||
alertmessage?: AlertMessageType;
|
alertmessage?: AlertMessageType;
|
||||||
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
|
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
|
||||||
|
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
|
||||||
userinputrequest?: UserInputRequest;
|
userinputrequest?: UserInputRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -823,6 +831,4 @@ export type {
|
|||||||
ScreenStatusIndicatorUpdateType,
|
ScreenStatusIndicatorUpdateType,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { StatusIndicatorLevel };
|
||||||
StatusIndicatorLevel,
|
|
||||||
};
|
|
||||||
|
171
src/util/keyutil.ts
Normal file
171
src/util/keyutil.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as electron from "electron";
|
||||||
|
|
||||||
|
type KeyPressDecl = {
|
||||||
|
mods: {
|
||||||
|
Cmd?: boolean;
|
||||||
|
Option?: boolean;
|
||||||
|
Shift?: boolean;
|
||||||
|
Ctrl?: boolean;
|
||||||
|
Alt?: boolean;
|
||||||
|
Meta?: boolean;
|
||||||
|
};
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
var PLATFORM: string;
|
||||||
|
const PlatformMacOS: string = "darwin";
|
||||||
|
|
||||||
|
function setKeyUtilPlatform(platform: string) {
|
||||||
|
PLATFORM = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKeyDescription(keyDescription: string): KeyPressDecl {
|
||||||
|
let rtn = { key: "", mods: {} } as KeyPressDecl;
|
||||||
|
let keys = keyDescription.replace(/[()]/g, "").split(":");
|
||||||
|
for (let key of keys) {
|
||||||
|
if (key == "Cmd") {
|
||||||
|
rtn.mods.Cmd = true;
|
||||||
|
} else if (key == "Shift") {
|
||||||
|
rtn.mods.Shift = true;
|
||||||
|
} else if (key == "Ctrl") {
|
||||||
|
rtn.mods.Ctrl = true;
|
||||||
|
} else if (key == "Option") {
|
||||||
|
rtn.mods.Option = true;
|
||||||
|
} else if (key == "Alt") {
|
||||||
|
rtn.mods.Alt = true;
|
||||||
|
} else if (key == "Meta") {
|
||||||
|
rtn.mods.Meta = true;
|
||||||
|
} else {
|
||||||
|
rtn.key = key;
|
||||||
|
if (key.length == 1) {
|
||||||
|
// check for if key is upper case
|
||||||
|
if (/[A-Z]/.test(key.charAt(0))) {
|
||||||
|
// this key is an upper case A - Z - we should apply the shift key, even if it wasn't specified
|
||||||
|
rtn.mods.Shift = true;
|
||||||
|
} else if (key == " ") {
|
||||||
|
rtn.key = "Space";
|
||||||
|
// we allow " " and "Space" to be mapped to Space key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkKeyPressed(event: WaveKeyboardEvent, description: string): boolean {
|
||||||
|
let keyPress = parseKeyDescription(description);
|
||||||
|
if (keyPress.mods.Option && !event.option) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keyPress.mods.Cmd && !event.cmd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keyPress.mods.Shift && !event.shift) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keyPress.mods.Ctrl && !event.control) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keyPress.mods.Alt && !event.alt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keyPress.mods.Meta && !event.meta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let eventKey = event.key;
|
||||||
|
let descKey = keyPress.key;
|
||||||
|
if (eventKey.length == 1 && /[A-Z]/.test(eventKey.charAt(0))) {
|
||||||
|
// key is upper case A-Z, this means shift is applied, we want to allow
|
||||||
|
// "Shift:e" as well as "Shift:E" or "E"
|
||||||
|
eventKey = eventKey.toLocaleLowerCase();
|
||||||
|
descKey = descKey.toLocaleLowerCase();
|
||||||
|
} else if (eventKey == " ") {
|
||||||
|
eventKey = "Space";
|
||||||
|
// a space key is shown as " ", we want users to be able to set space key as "Space" or " ", whichever they prefer
|
||||||
|
}
|
||||||
|
if (descKey != eventKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd and Option are portable between Mac and Linux/Windows
|
||||||
|
type ModKeyStrs = "Cmd" | "Option" | "Shift" | "Ctrl" | "Alt" | "Meta";
|
||||||
|
|
||||||
|
interface WaveKeyboardEvent {
|
||||||
|
type: string;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.key.
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.code.
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.shiftKey.
|
||||||
|
*/
|
||||||
|
shift: boolean;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.controlKey.
|
||||||
|
*/
|
||||||
|
control: boolean;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.altKey.
|
||||||
|
*/
|
||||||
|
alt: boolean;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.metaKey.
|
||||||
|
*/
|
||||||
|
meta: boolean;
|
||||||
|
/**
|
||||||
|
* cmd is special, on mac it is meta, on windows it is alt
|
||||||
|
*/
|
||||||
|
cmd: boolean;
|
||||||
|
/**
|
||||||
|
* option is special, on mac it is alt, on windows it is meta
|
||||||
|
*/
|
||||||
|
option: boolean;
|
||||||
|
|
||||||
|
repeat: boolean;
|
||||||
|
/**
|
||||||
|
* Equivalent to KeyboardEvent.location.
|
||||||
|
*/
|
||||||
|
location: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEvent): WaveKeyboardEvent {
|
||||||
|
let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent;
|
||||||
|
rtn.control = event.ctrlKey;
|
||||||
|
rtn.shift = event.shiftKey;
|
||||||
|
rtn.cmd = (PLATFORM == PlatformMacOS ? event.metaKey : event.altKey);
|
||||||
|
rtn.option = (PLATFORM == PlatformMacOS ? event.altKey : event.metaKey);
|
||||||
|
rtn.meta = event.metaKey;
|
||||||
|
rtn.alt = event.altKey;
|
||||||
|
rtn.code = event.code;
|
||||||
|
rtn.key = event.key;
|
||||||
|
rtn.location = event.location;
|
||||||
|
rtn.type = event.type;
|
||||||
|
rtn.repeat = event.repeat;
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent {
|
||||||
|
let rtn: WaveKeyboardEvent = {} as WaveKeyboardEvent;
|
||||||
|
rtn.type = event.type;
|
||||||
|
rtn.control = event.control;
|
||||||
|
rtn.cmd = (PLATFORM == PlatformMacOS ? event.meta : event.alt)
|
||||||
|
rtn.option = (PLATFORM == PlatformMacOS ? event.alt : event.meta);
|
||||||
|
rtn.meta = event.meta;
|
||||||
|
rtn.alt = event.alt;
|
||||||
|
rtn.shift = event.shift;
|
||||||
|
rtn.repeat = event.isAutoRepeat;
|
||||||
|
rtn.location = event.location;
|
||||||
|
rtn.code = event.code;
|
||||||
|
rtn.key = event.key;
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { adaptFromElectronKeyEvent, adaptFromReactOrNativeKeyEvent, checkKeyPressed, setKeyUtilPlatform };
|
||||||
|
export type { WaveKeyboardEvent };
|
@ -1,2 +1,2 @@
|
|||||||
const VERSION = "v0.6.0";
|
const VERSION = "v0.6.1";
|
||||||
module.exports = VERSION;
|
module.exports = VERSION;
|
||||||
|
84
waveshell/pkg/utilfn/syncmap.go
Normal file
84
waveshell/pkg/utilfn/syncmap.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package utilfn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyncMap[K comparable, V any] struct {
|
||||||
|
lock *sync.Mutex
|
||||||
|
m map[K]V
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeSyncMap[K comparable, V any]() *SyncMap[K, V] {
|
||||||
|
return &SyncMap[K, V]{
|
||||||
|
lock: &sync.Mutex{},
|
||||||
|
m: make(map[K]V),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Set(k K, v V) {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
sm.m[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Get(k K) V {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
return sm.m[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) GetEx(k K) (V, bool) {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
v, ok := sm.m[k]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Delete(k K) {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
delete(sm.m, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Clear() {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
sm.m = make(map[K]V)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Len() int {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
return len(sm.m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Keys() []K {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
keys := make([]K, len(sm.m))
|
||||||
|
i := 0
|
||||||
|
for k := range sm.m {
|
||||||
|
keys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SyncMap[K, V]) Replace(newMap map[K]V) {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
sm.m = make(map[K]V, len(newMap))
|
||||||
|
for k, v := range newMap {
|
||||||
|
sm.m[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncSyncMap[K comparable, V int | int64](sm *SyncMap[K, V], key K, incAmt V) {
|
||||||
|
sm.lock.Lock()
|
||||||
|
defer sm.lock.Unlock()
|
||||||
|
sm.m[key] += incAmt
|
||||||
|
}
|
1
wavesrv/db/migrations/000031_restart_cmd.down.sql
Normal file
1
wavesrv/db/migrations/000031_restart_cmd.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE cmd DROP COLUMN restartts;
|
1
wavesrv/db/migrations/000031_restart_cmd.up.sql
Normal file
1
wavesrv/db/migrations/000031_restart_cmd.up.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE cmd ADD COLUMN restartts bigint NOT NULL DEFAULT 0;
|
@ -210,6 +210,7 @@ func init() {
|
|||||||
registerCmdFn("line:setheight", LineSetHeightCommand)
|
registerCmdFn("line:setheight", LineSetHeightCommand)
|
||||||
registerCmdFn("line:view", LineViewCommand)
|
registerCmdFn("line:view", LineViewCommand)
|
||||||
registerCmdFn("line:set", LineSetCommand)
|
registerCmdFn("line:set", LineSetCommand)
|
||||||
|
registerCmdFn("line:restart", LineRestartCommand)
|
||||||
|
|
||||||
registerCmdFn("client", ClientCommand)
|
registerCmdFn("client", ClientCommand)
|
||||||
registerCmdFn("client:show", ClientShowCommand)
|
registerCmdFn("client:show", ClientShowCommand)
|
||||||
@ -491,7 +492,12 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
|
|||||||
}
|
}
|
||||||
runPacket.Command = ":"
|
runPacket.Command = ":"
|
||||||
runPacket.ReturnState = true
|
runPacket.ReturnState = true
|
||||||
cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket)
|
rcOpts := remote.RunCommandOpts{
|
||||||
|
SessionId: ids.SessionId,
|
||||||
|
ScreenId: ids.ScreenId,
|
||||||
|
RemotePtr: ids.Remote.RemotePtr,
|
||||||
|
}
|
||||||
|
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
defer callback()
|
defer callback()
|
||||||
}
|
}
|
||||||
@ -587,7 +593,12 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U
|
|||||||
}
|
}
|
||||||
runPacket.Command = strings.TrimSpace(cmdStr)
|
runPacket.Command = strings.TrimSpace(cmdStr)
|
||||||
runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd)
|
runPacket.ReturnState = resolveBool(pk.Kwargs["rtnstate"], isRtnStateCmd)
|
||||||
cmd, callback, err := remote.RunCommand(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, runPacket)
|
rcOpts := remote.RunCommandOpts{
|
||||||
|
SessionId: ids.SessionId,
|
||||||
|
ScreenId: ids.ScreenId,
|
||||||
|
RemotePtr: ids.Remote.RemotePtr,
|
||||||
|
}
|
||||||
|
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
defer callback()
|
defer callback()
|
||||||
}
|
}
|
||||||
@ -2974,6 +2985,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
update := &sstore.ModelUpdate{
|
update := &sstore.ModelUpdate{
|
||||||
ActiveSessionId: ritem.Id,
|
ActiveSessionId: ritem.Id,
|
||||||
Info: &sstore.InfoMsgType{
|
Info: &sstore.InfoMsgType{
|
||||||
@ -2981,6 +2993,21 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
|
|||||||
TimeoutMs: 2000,
|
TimeoutMs: 2000,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the status indicator for the new active screen
|
||||||
|
session, err := sstore.GetSessionById(ctx, ritem.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get session: %w", err)
|
||||||
|
}
|
||||||
|
if session == nil {
|
||||||
|
return nil, fmt.Errorf("session not found")
|
||||||
|
}
|
||||||
|
err = sstore.ResetStatusIndicator_Update(update, session.ActiveScreenId)
|
||||||
|
if err != nil {
|
||||||
|
// this is not a fatal error, just log it
|
||||||
|
log.Printf("error resetting status indicator after session command: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3320,6 +3347,121 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LineRestartCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var lineId string
|
||||||
|
if len(pk.Args) >= 1 {
|
||||||
|
lineArg := pk.Args[0]
|
||||||
|
resolvedLineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error looking up lineid: %v", err)
|
||||||
|
}
|
||||||
|
lineId = resolvedLineId
|
||||||
|
} else {
|
||||||
|
selectedLineId, err := sstore.GetScreenSelectedLineId(ctx, ids.ScreenId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting selected lineid: %v", err)
|
||||||
|
}
|
||||||
|
lineId = selectedLineId
|
||||||
|
}
|
||||||
|
if lineId == "" {
|
||||||
|
return nil, fmt.Errorf("%s requires a lineid to operate on", GetCmdStr(pk))
|
||||||
|
}
|
||||||
|
line, cmd, err := sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting line: %v", err)
|
||||||
|
}
|
||||||
|
if line == nil {
|
||||||
|
return nil, fmt.Errorf("line not found")
|
||||||
|
}
|
||||||
|
if cmd == nil {
|
||||||
|
return nil, fmt.Errorf("cannot restart line (no cmd found)")
|
||||||
|
}
|
||||||
|
if cmd.Status == sstore.CmdStatusRunning || cmd.Status == sstore.CmdStatusDetached {
|
||||||
|
killCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
err = ids.Remote.MShell.KillRunningCommandAndWait(killCtx, base.MakeCommandKey(ids.ScreenId, lineId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids.Remote.MShell.ResetDataPos(base.MakeCommandKey(ids.ScreenId, lineId))
|
||||||
|
err = sstore.ClearCmdPtyFile(ctx, ids.ScreenId, lineId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error clearing existing pty file: %v", err)
|
||||||
|
}
|
||||||
|
runPacket := packet.MakeRunPacket()
|
||||||
|
runPacket.ReqId = uuid.New().String()
|
||||||
|
runPacket.CK = base.MakeCommandKey(ids.ScreenId, lineId)
|
||||||
|
runPacket.UsePty = true
|
||||||
|
// TODO how can we preseve the original termopts?
|
||||||
|
runPacket.TermOpts, err = GetUITermOpts(pk.UIContext.WinSize, DefaultPTERM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting creating termopts for command: %w", err)
|
||||||
|
}
|
||||||
|
runPacket.Command = cmd.CmdStr
|
||||||
|
runPacket.ReturnState = false
|
||||||
|
rcOpts := remote.RunCommandOpts{
|
||||||
|
SessionId: ids.SessionId,
|
||||||
|
ScreenId: ids.ScreenId,
|
||||||
|
RemotePtr: ids.Remote.RemotePtr,
|
||||||
|
StatePtr: &cmd.StatePtr,
|
||||||
|
NoCreateCmdPtyFile: true,
|
||||||
|
}
|
||||||
|
cmd, callback, err := remote.RunCommand(ctx, rcOpts, runPacket)
|
||||||
|
if callback != nil {
|
||||||
|
defer callback()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newTs := time.Now().UnixMilli()
|
||||||
|
err = sstore.UpdateCmdForRestart(ctx, runPacket.CK, newTs, cmd.CmdPid, cmd.RemotePid, convertTermOpts(runPacket.TermOpts))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating cmd for restart: %w", err)
|
||||||
|
}
|
||||||
|
line, cmd, err = sstore.GetLineCmdByLineId(ctx, ids.ScreenId, lineId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting updated line/cmd: %w", err)
|
||||||
|
}
|
||||||
|
cmd.Restarted = true
|
||||||
|
update := &sstore.ModelUpdate{
|
||||||
|
Line: line,
|
||||||
|
Cmd: cmd,
|
||||||
|
Interactive: pk.Interactive,
|
||||||
|
}
|
||||||
|
screen, focusErr := focusScreenLine(ctx, ids.ScreenId, line.LineNum)
|
||||||
|
if focusErr != nil {
|
||||||
|
// not a fatal error, so just log
|
||||||
|
log.Printf("error focusing screen line: %v\n", focusErr)
|
||||||
|
}
|
||||||
|
if screen != nil {
|
||||||
|
update.Screens = []*sstore.ScreenType{screen}
|
||||||
|
}
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func focusScreenLine(ctx context.Context, screenId string, lineNum int64) (*sstore.ScreenType, error) {
|
||||||
|
screen, err := sstore.GetScreenById(ctx, screenId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting screen: %v", err)
|
||||||
|
}
|
||||||
|
if screen == nil {
|
||||||
|
return nil, fmt.Errorf("screen not found")
|
||||||
|
}
|
||||||
|
updateMap := make(map[string]interface{})
|
||||||
|
updateMap[sstore.ScreenField_SelectedLine] = lineNum
|
||||||
|
updateMap[sstore.ScreenField_Focus] = sstore.ScreenFocusCmd
|
||||||
|
screen, err = sstore.UpdateScreen(ctx, screenId, updateMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating screen: %v", err)
|
||||||
|
}
|
||||||
|
return screen, nil
|
||||||
|
}
|
||||||
|
|
||||||
func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -3751,6 +3893,10 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst
|
|||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", stat.Location))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file", stat.Location))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "file-data", fileDataStr))
|
||||||
}
|
}
|
||||||
|
if cmd.RestartTs > 0 {
|
||||||
|
restartTs := time.UnixMilli(cmd.RestartTs)
|
||||||
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "restartts", restartTs.Format(TsFormatStr)))
|
||||||
|
}
|
||||||
if cmd.DoneTs != 0 {
|
if cmd.DoneTs != 0 {
|
||||||
doneTs := time.UnixMilli(cmd.DoneTs)
|
doneTs := time.UnixMilli(cmd.DoneTs)
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "donets", doneTs.Format(TsFormatStr)))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "donets", doneTs.Format(TsFormatStr)))
|
||||||
|
@ -123,3 +123,12 @@ func convertTermOpts(pkto *packet.TermOpts) *sstore.TermOpts {
|
|||||||
MaxPtySize: pkto.MaxPtySize,
|
MaxPtySize: pkto.MaxPtySize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertToPacketTermOpts(sto sstore.TermOpts) *packet.TermOpts {
|
||||||
|
return &packet.TermOpts{
|
||||||
|
Rows: int(sto.Rows),
|
||||||
|
Cols: int(sto.Cols),
|
||||||
|
FlexRows: sto.FlexRows,
|
||||||
|
MaxPtySize: sto.MaxPtySize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -161,6 +161,7 @@ type MShellProc struct {
|
|||||||
StateMap *server.ShellStateMap
|
StateMap *server.ShellStateMap
|
||||||
NumTryConnect int
|
NumTryConnect int
|
||||||
InitPkShellType string
|
InitPkShellType string
|
||||||
|
DataPosMap *utilfn.SyncMap[base.CommandKey, int64]
|
||||||
|
|
||||||
// install
|
// install
|
||||||
InstallStatus string
|
InstallStatus string
|
||||||
@ -169,7 +170,6 @@ type MShellProc struct {
|
|||||||
InstallErr error
|
InstallErr error
|
||||||
|
|
||||||
RunningCmds map[base.CommandKey]RunCmdType
|
RunningCmds map[base.CommandKey]RunCmdType
|
||||||
WaitingCmds []RunCmdType
|
|
||||||
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name]
|
PendingStateCmds map[pendingStateKey]base.CommandKey // key=[remoteinstance name]
|
||||||
launcher Launcher // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
|
launcher Launcher // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
|
||||||
}
|
}
|
||||||
@ -209,6 +209,18 @@ func (msh *MShellProc) GetDefaultState(shellType string) *packet.ShellState {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) EnsureShellType(ctx context.Context, shellType string) error {
|
||||||
|
if msh.StateMap.HasShell(shellType) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// try to reinit the shell
|
||||||
|
_, err := msh.ReInit(ctx, shellType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error trying to initialize shell %q: %v", shellType, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) GetDefaultStatePtr(shellType string) *sstore.ShellStatePtr {
|
func (msh *MShellProc) GetDefaultStatePtr(shellType string) *sstore.ShellStatePtr {
|
||||||
msh.Lock.Lock()
|
msh.Lock.Lock()
|
||||||
defer msh.Lock.Unlock()
|
defer msh.Lock.Unlock()
|
||||||
@ -692,6 +704,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc {
|
|||||||
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
|
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
|
||||||
StateMap: server.MakeShellStateMap(),
|
StateMap: server.MakeShellStateMap(),
|
||||||
launcher: LegacyLauncher{}, // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
|
launcher: LegacyLauncher{}, // for conditional launch method based on ssh library in use. remove once ssh library is stabilized
|
||||||
|
DataPosMap: utilfn.MakeSyncMap[base.CommandKey, int64](),
|
||||||
}
|
}
|
||||||
// for conditional launch method based on ssh library in use
|
// for conditional launch method based on ssh library in use
|
||||||
// remove once ssh library is stabilized
|
// remove once ssh library is stabilized
|
||||||
@ -1615,12 +1628,8 @@ func replaceHomePath(pathStr string, homeDir string) string {
|
|||||||
func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool {
|
func (msh *MShellProc) IsCmdRunning(ck base.CommandKey) bool {
|
||||||
msh.Lock.Lock()
|
msh.Lock.Lock()
|
||||||
defer msh.Lock.Unlock()
|
defer msh.Lock.Unlock()
|
||||||
for runningCk := range msh.RunningCmds {
|
_, ok := msh.RunningCmds[ck]
|
||||||
if runningCk == ck {
|
return ok
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error {
|
func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error {
|
||||||
@ -1633,6 +1642,30 @@ func (msh *MShellProc) SendInput(dataPk *packet.DataPacketType) error {
|
|||||||
return msh.ServerProc.Input.SendPacket(dataPk)
|
return msh.ServerProc.Input.SendPacket(dataPk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) KillRunningCommandAndWait(ctx context.Context, ck base.CommandKey) error {
|
||||||
|
if !msh.IsCmdRunning(ck) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
siPk := packet.MakeSpecialInputPacket()
|
||||||
|
siPk.CK = ck
|
||||||
|
siPk.SigName = "SIGTERM"
|
||||||
|
err := msh.SendSpecialInput(siPk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error trying to kill running cmd: %w", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if !msh.IsCmdRunning(ck) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// TODO fix busy wait (sync with msh.RunningCmds)
|
||||||
|
// not a huge deal though since this is not processor intensive and not widely used
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) SendSpecialInput(siPk *packet.SpecialInputPacketType) error {
|
func (msh *MShellProc) SendSpecialInput(siPk *packet.SpecialInputPacketType) error {
|
||||||
if !msh.IsConnected() {
|
if !msh.IsConnected() {
|
||||||
return fmt.Errorf("remote is not connected, cannot send input")
|
return fmt.Errorf("remote is not connected, cannot send input")
|
||||||
@ -1682,14 +1715,25 @@ func (msh *MShellProc) removePendingStateCmd(screenId string, rptr sstore.Remote
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns (cmdtype, allow-updates-callback, err)
|
type RunCommandOpts struct {
|
||||||
func RunCommand(ctx context.Context, sessionId string, screenId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) {
|
SessionId string
|
||||||
rct := RunCmdType{
|
ScreenId string
|
||||||
SessionId: sessionId,
|
RemotePtr sstore.RemotePtrType
|
||||||
ScreenId: screenId,
|
|
||||||
RemotePtr: remotePtr,
|
// optional, if not provided shellstate will look up state from remote instance
|
||||||
RunPacket: runPacket,
|
// ReturnState cannot be used with StatePtr
|
||||||
|
// this will also cause this command to bypass the pending state cmd logic
|
||||||
|
StatePtr *sstore.ShellStatePtr
|
||||||
|
|
||||||
|
// set to true to skip creating the pty file (for restarted commands)
|
||||||
|
NoCreateCmdPtyFile bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns (CmdType, allow-updates-callback, err)
|
||||||
|
// we must persist the CmdType to the DB before calling the callback to allow updates
|
||||||
|
// otherwise an early CmdDone packet might not get processed (since cmd will not exist in DB)
|
||||||
|
func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) {
|
||||||
|
sessionId, screenId, remotePtr := rcOpts.SessionId, rcOpts.ScreenId, rcOpts.RemotePtr
|
||||||
if remotePtr.OwnerId != "" {
|
if remotePtr.OwnerId != "" {
|
||||||
return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef())
|
return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef())
|
||||||
}
|
}
|
||||||
@ -1706,6 +1750,13 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt
|
|||||||
if runPacket.State != nil {
|
if runPacket.State != nil {
|
||||||
return nil, nil, fmt.Errorf("runPacket.State should not be set, it is set in RunCommand")
|
return nil, nil, fmt.Errorf("runPacket.State should not be set, it is set in RunCommand")
|
||||||
}
|
}
|
||||||
|
if rcOpts.StatePtr != nil && runPacket.ReturnState {
|
||||||
|
return nil, nil, fmt.Errorf("RunCommand: cannot use ReturnState with StatePtr")
|
||||||
|
}
|
||||||
|
|
||||||
|
// pending state command logic
|
||||||
|
// if we are currently running a command that can change the state, we need to wait for it to finish
|
||||||
|
if rcOpts.StatePtr == nil {
|
||||||
var newPSC *base.CommandKey
|
var newPSC *base.CommandKey
|
||||||
if runPacket.ReturnState {
|
if runPacket.ReturnState {
|
||||||
newPSC = &runPacket.CK
|
newPSC = &runPacket.CK
|
||||||
@ -1721,41 +1772,63 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt
|
|||||||
}
|
}
|
||||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum)
|
return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum)
|
||||||
}
|
}
|
||||||
startCmdWait(runPacket.CK)
|
|
||||||
defer func() {
|
|
||||||
if rtnErr != nil {
|
|
||||||
removeCmdWait(runPacket.CK)
|
|
||||||
if newPSC != nil {
|
if newPSC != nil {
|
||||||
|
defer func() {
|
||||||
|
// if we get an error, remove the pending state cmd
|
||||||
|
// if no error, PSC will get removed when we see a CmdDone or CmdFinal packet
|
||||||
|
if rtnErr != nil {
|
||||||
msh.removePendingStateCmd(screenId, remotePtr, *newPSC)
|
msh.removePendingStateCmd(screenId, remotePtr, *newPSC)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get current remote-instance state
|
// get current remote-instance state
|
||||||
statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
|
var statePtr *sstore.ShellStatePtr
|
||||||
|
if rcOpts.StatePtr != nil {
|
||||||
|
statePtr = rcOpts.StatePtr
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
statePtr, err = sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
|
return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
|
||||||
}
|
}
|
||||||
if statePtr == nil {
|
|
||||||
statePtr = msh.GetDefaultStatePtr(msh.GetShellPref())
|
|
||||||
}
|
}
|
||||||
|
if statePtr == nil { // can be null if there is no remote-instance (screen has unchanged state from default)
|
||||||
|
err := msh.EnsureShellType(ctx, msh.GetShellPref()) // make sure shellType is initialized
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
statePtr = msh.GetDefaultStatePtr(msh.GetShellPref())
|
||||||
if statePtr == nil {
|
if statePtr == nil {
|
||||||
return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr")
|
return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
currentState, err := sstore.GetFullState(ctx, *statePtr)
|
currentState, err := sstore.GetFullState(ctx, *statePtr)
|
||||||
if err != nil || currentState == nil {
|
if err != nil || currentState == nil {
|
||||||
return nil, nil, fmt.Errorf("cannot get current remote state: %w", err)
|
return nil, nil, fmt.Errorf("cannot load current remote state: %w", err)
|
||||||
}
|
}
|
||||||
runPacket.State = addScVarsToState(currentState)
|
runPacket.State = addScVarsToState(currentState)
|
||||||
runPacket.StateComplete = true
|
runPacket.StateComplete = true
|
||||||
runPacket.ShellType = currentState.GetShellType()
|
runPacket.ShellType = currentState.GetShellType()
|
||||||
// check to see if shellType is initialized
|
err = msh.EnsureShellType(ctx, runPacket.ShellType) // make sure shellType is initialized
|
||||||
if !msh.StateMap.HasShell(runPacket.ShellType) {
|
|
||||||
// try to reinit the shell
|
|
||||||
_, err := msh.ReInit(ctx, runPacket.ShellType)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("error trying to initialize shell %q: %v", runPacket.ShellType, err)
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start cmdwait. must be started before sending the run packet
|
||||||
|
// this ensures that we don't process output, or cmddone packets until we set up the line, cmd, and ptyout file
|
||||||
|
startCmdWait(runPacket.CK)
|
||||||
|
defer func() {
|
||||||
|
// if we get an error, remove the cmdwait
|
||||||
|
// if no error, cmdwait will get removed by the caller w/ the callback fn that's returned on success
|
||||||
|
if rtnErr != nil {
|
||||||
|
removeCmdWait(runPacket.CK)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// RegisterRpc + WaitForResponse is used to get any waveshell side errors
|
||||||
|
// waveshell will either return an error (in a ResponsePacketType) or a CmdStartPacketType
|
||||||
msh.ServerProc.Output.RegisterRpc(runPacket.ReqId)
|
msh.ServerProc.Output.RegisterRpc(runPacket.ReqId)
|
||||||
err = shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket)
|
err = shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1776,6 +1849,8 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt
|
|||||||
}
|
}
|
||||||
return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk))
|
return nil, nil, fmt.Errorf("invalid response received from server for run packet: %s", packet.AsString(rtnPk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command is now successfully runnning
|
||||||
status := sstore.CmdStatusRunning
|
status := sstore.CmdStatusRunning
|
||||||
if runPacket.Detached {
|
if runPacket.Detached {
|
||||||
status = sstore.CmdStatusDetached
|
status = sstore.CmdStatusDetached
|
||||||
@ -1797,46 +1872,24 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt
|
|||||||
RunOut: nil,
|
RunOut: nil,
|
||||||
RtnState: runPacket.ReturnState,
|
RtnState: runPacket.ReturnState,
|
||||||
}
|
}
|
||||||
|
if !rcOpts.NoCreateCmdPtyFile {
|
||||||
err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO the cmd is running, so this is a tricky error to handle
|
// TODO the cmd is running, so this is a tricky error to handle
|
||||||
return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err)
|
return nil, nil, fmt.Errorf("cannot create local ptyout file for running command: %v", err)
|
||||||
}
|
}
|
||||||
msh.AddRunningCmd(rct)
|
}
|
||||||
|
msh.AddRunningCmd(RunCmdType{
|
||||||
|
SessionId: sessionId,
|
||||||
|
ScreenId: screenId,
|
||||||
|
RemotePtr: remotePtr,
|
||||||
|
RunPacket: runPacket,
|
||||||
|
})
|
||||||
|
|
||||||
|
go pushNumRunningCmdsUpdate(&runPacket.CK, 1)
|
||||||
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) AddWaitingCmd(rct RunCmdType) {
|
|
||||||
msh.Lock.Lock()
|
|
||||||
defer msh.Lock.Unlock()
|
|
||||||
msh.WaitingCmds = append(msh.WaitingCmds, rct)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) reExecSingle(rct RunCmdType) {
|
|
||||||
// TODO fixme
|
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer cancelFn()
|
|
||||||
_, callback, _ := RunCommand(ctx, rct.SessionId, rct.ScreenId, rct.RemotePtr, rct.RunPacket)
|
|
||||||
if callback != nil {
|
|
||||||
defer callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) ReExecWaitingCmds() {
|
|
||||||
msh.Lock.Lock()
|
|
||||||
defer msh.Lock.Unlock()
|
|
||||||
for len(msh.WaitingCmds) > 0 {
|
|
||||||
rct := msh.WaitingCmds[0]
|
|
||||||
go msh.reExecSingle(rct)
|
|
||||||
if rct.RunPacket.ReturnState {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(msh.WaitingCmds) == 0 {
|
|
||||||
msh.WaitingCmds = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msh *MShellProc) AddRunningCmd(rct RunCmdType) {
|
func (msh *MShellProc) AddRunningCmd(rct RunCmdType) {
|
||||||
msh.Lock.Lock()
|
msh.Lock.Lock()
|
||||||
defer msh.Lock.Unlock()
|
defer msh.Lock.Unlock()
|
||||||
@ -1937,10 +1990,10 @@ func (msh *MShellProc) notifyHangups_nolock() {
|
|||||||
}
|
}
|
||||||
update := &sstore.ModelUpdate{Cmd: cmd}
|
update := &sstore.ModelUpdate{Cmd: cmd}
|
||||||
sstore.MainBus.SendScreenUpdate(ck.GetGroupId(), update)
|
sstore.MainBus.SendScreenUpdate(ck.GetGroupId(), update)
|
||||||
|
go pushNumRunningCmdsUpdate(&ck, -1)
|
||||||
}
|
}
|
||||||
msh.RunningCmds = make(map[base.CommandKey]RunCmdType)
|
msh.RunningCmds = make(map[base.CommandKey]RunCmdType)
|
||||||
msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey)
|
msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey)
|
||||||
msh.WaitingCmds = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
||||||
@ -2011,6 +2064,8 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
|||||||
// fall-through (nothing to do)
|
// fall-through (nothing to do)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go pushNumRunningCmdsUpdate(&donePk.CK, -1)
|
||||||
sstore.MainBus.SendUpdate(update)
|
sstore.MainBus.SendUpdate(update)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2044,6 +2099,7 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType)
|
|||||||
if screen != nil {
|
if screen != nil {
|
||||||
update.Screens = []*sstore.ScreenType{screen}
|
update.Screens = []*sstore.ScreenType{screen}
|
||||||
}
|
}
|
||||||
|
go pushNumRunningCmdsUpdate(&finalPk.CK, -1)
|
||||||
sstore.MainBus.SendUpdate(update)
|
sstore.MainBus.SendUpdate(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2057,7 +2113,11 @@ func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) {
|
func (msh *MShellProc) ResetDataPos(ck base.CommandKey) {
|
||||||
|
msh.DataPosMap.Delete(ck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) {
|
||||||
realData, err := base64.StdEncoding.DecodeString(dataPk.Data64)
|
realData, err := base64.StdEncoding.DecodeString(dataPk.Data64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
||||||
@ -2066,7 +2126,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa
|
|||||||
}
|
}
|
||||||
var ack *packet.DataAckPacketType
|
var ack *packet.DataAckPacketType
|
||||||
if len(realData) > 0 {
|
if len(realData) > 0 {
|
||||||
dataPos := dataPosMap[dataPk.CK]
|
dataPos := dataPosMap.Get(dataPk.CK)
|
||||||
rcmd := msh.GetRunningCmd(dataPk.CK)
|
rcmd := msh.GetRunningCmd(dataPk.CK)
|
||||||
update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos)
|
update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2074,7 +2134,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa
|
|||||||
} else {
|
} else {
|
||||||
ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
|
ack = makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
|
||||||
}
|
}
|
||||||
dataPosMap[dataPk.CK] += int64(len(realData))
|
utilfn.IncSyncMap(dataPosMap, dataPk.CK, int64(len(realData)))
|
||||||
if update != nil {
|
if update != nil {
|
||||||
sstore.MainBus.SendScreenUpdate(dataPk.CK.GetGroupId(), update)
|
sstore.MainBus.SendScreenUpdate(dataPk.CK.GetGroupId(), update)
|
||||||
}
|
}
|
||||||
@ -2085,7 +2145,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa
|
|||||||
// log.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error)
|
// log.Printf("data %s fd=%d len=%d eof=%v err=%v\n", dataPk.CK, dataPk.FdNum, len(realData), dataPk.Eof, dataPk.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap map[base.CommandKey]int64) func() {
|
func (msh *MShellProc) makeHandleDataPacketClosure(dataPk *packet.DataPacketType, dataPosMap *utilfn.SyncMap[base.CommandKey, int64]) func() {
|
||||||
return func() {
|
return func() {
|
||||||
msh.handleDataPacket(dataPk, dataPosMap)
|
msh.handleDataPacket(dataPk, dataPosMap)
|
||||||
}
|
}
|
||||||
@ -2124,12 +2184,10 @@ func (msh *MShellProc) ProcessPackets() {
|
|||||||
go sendScreenUpdates(screens)
|
go sendScreenUpdates(screens)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// TODO need to clean dataPosMap
|
|
||||||
dataPosMap := make(map[base.CommandKey]int64)
|
|
||||||
for pk := range msh.ServerProc.Output.MainCh {
|
for pk := range msh.ServerProc.Output.MainCh {
|
||||||
if pk.GetType() == packet.DataPacketStr {
|
if pk.GetType() == packet.DataPacketStr {
|
||||||
dataPk := pk.(*packet.DataPacketType)
|
dataPk := pk.(*packet.DataPacketType)
|
||||||
runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, dataPosMap))
|
runCmdUpdateFn(dataPk.CK, msh.makeHandleDataPacketClosure(dataPk, msh.DataPosMap))
|
||||||
go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output)
|
go pushStatusIndicatorUpdate(&dataPk.CK, sstore.StatusIndicatorLevel_Output)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -2411,5 +2469,13 @@ func (msh *MShellProc) GetDisplayName() string {
|
|||||||
// Identify the screen for a given CommandKey and push the given status indicator update for that screen
|
// Identify the screen for a given CommandKey and push the given status indicator update for that screen
|
||||||
func pushStatusIndicatorUpdate(ck *base.CommandKey, level sstore.StatusIndicatorLevel) {
|
func pushStatusIndicatorUpdate(ck *base.CommandKey, level sstore.StatusIndicatorLevel) {
|
||||||
screenId := ck.GetGroupId()
|
screenId := ck.GetGroupId()
|
||||||
sstore.SetStatusIndicatorLevel(context.Background(), screenId, level, false)
|
err := sstore.SetStatusIndicatorLevel(context.Background(), screenId, level, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error setting status indicator level: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushNumRunningCmdsUpdate(ck *base.CommandKey, delta int) {
|
||||||
|
screenId := ck.GetGroupId()
|
||||||
|
sstore.IncrementNumRunningCmds(screenId, delta)
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ const WaveLockFile = "waveterm.lock"
|
|||||||
const WaveDirName = ".waveterm" // must match emain.ts
|
const WaveDirName = ".waveterm" // must match emain.ts
|
||||||
const WaveDevDirName = ".waveterm-dev" // must match emain.ts
|
const WaveDevDirName = ".waveterm-dev" // must match emain.ts
|
||||||
const WaveAppPathVarName = "WAVETERM_APP_PATH"
|
const WaveAppPathVarName = "WAVETERM_APP_PATH"
|
||||||
const WaveVersion = "v0.6.0"
|
const WaveVersion = "v0.6.1"
|
||||||
const WaveAuthKeyFileName = "waveterm.authkey"
|
const WaveAuthKeyFileName = "waveterm.authkey"
|
||||||
const MShellVersion = "v0.4.0"
|
const MShellVersion = "v0.4.0"
|
||||||
|
|
||||||
|
@ -763,30 +763,38 @@ func GetScreenById(ctx context.Context, screenId string) (*ScreenType, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// special "E" returns last unarchived line, "EA" returns last line (even if archived)
|
||||||
func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (string, error) {
|
func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (string, error) {
|
||||||
var lineId string
|
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
||||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
if lineArg == "E" {
|
||||||
|
query := `SELECT lineid FROM line WHERE screenid = ? AND NOT archived ORDER BY linenum DESC LIMIT 1`
|
||||||
|
lineId := tx.GetString(query, screenId)
|
||||||
|
return lineId, nil
|
||||||
|
}
|
||||||
|
if lineArg == "EA" {
|
||||||
|
query := `SELECT lineid FROM line WHERE screenid = ? ORDER BY linenum DESC LIMIT 1`
|
||||||
|
lineId := tx.GetString(query, screenId)
|
||||||
|
return lineId, nil
|
||||||
|
}
|
||||||
lineNum, err := strconv.Atoi(lineArg)
|
lineNum, err := strconv.Atoi(lineArg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// valid linenum
|
// valid linenum
|
||||||
query := `SELECT lineid FROM line WHERE screenid = ? AND linenum = ?`
|
query := `SELECT lineid FROM line WHERE screenid = ? AND linenum = ?`
|
||||||
lineId = tx.GetString(query, screenId, lineNum)
|
lineId := tx.GetString(query, screenId, lineNum)
|
||||||
|
return lineId, nil
|
||||||
} else if len(lineArg) == 8 {
|
} else if len(lineArg) == 8 {
|
||||||
// prefix id string match
|
// prefix id string match
|
||||||
query := `SELECT lineid FROM line WHERE screenid = ? AND substr(lineid, 1, 8) = ?`
|
query := `SELECT lineid FROM line WHERE screenid = ? AND substr(lineid, 1, 8) = ?`
|
||||||
lineId = tx.GetString(query, screenId, lineArg)
|
lineId := tx.GetString(query, screenId, lineArg)
|
||||||
|
return lineId, nil
|
||||||
} else {
|
} else {
|
||||||
// id match
|
// id match
|
||||||
query := `SELECT lineid FROM line WHERE screenid = ? AND lineid = ?`
|
query := `SELECT lineid FROM line WHERE screenid = ? AND lineid = ?`
|
||||||
lineId = tx.GetString(query, screenId, lineArg)
|
lineId := tx.GetString(query, screenId, lineArg)
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if txErr != nil {
|
|
||||||
return "", txErr
|
|
||||||
}
|
|
||||||
return lineId, nil
|
return lineId, nil
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) {
|
func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) {
|
||||||
return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) {
|
return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) {
|
||||||
@ -836,8 +844,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error {
|
|||||||
cmd.OrigTermOpts = cmd.TermOpts
|
cmd.OrigTermOpts = cmd.TermOpts
|
||||||
cmdMap := cmd.ToMap()
|
cmdMap := cmd.ToMap()
|
||||||
query = `
|
query = `
|
||||||
INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, cmdpid, remotepid, donets, exitcode, durationms, rtnstate, runout, rtnbasehash, rtndiffhasharr)
|
INSERT INTO cmd ( screenid, lineid, remoteownerid, remoteid, remotename, cmdstr, rawcmdstr, festate, statebasehash, statediffhasharr, termopts, origtermopts, status, cmdpid, remotepid, donets, restartts, exitcode, durationms, rtnstate, runout, rtnbasehash, rtndiffhasharr)
|
||||||
VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:cmdpid,:remotepid,:donets,:exitcode,:durationms,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr)
|
VALUES (:screenid,:lineid,:remoteownerid,:remoteid,:remotename,:cmdstr,:rawcmdstr,:festate,:statebasehash,:statediffhasharr,:termopts,:origtermopts,:status,:cmdpid,:remotepid,:donets,:restartts,:exitcode,:durationms,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr)
|
||||||
`
|
`
|
||||||
tx.NamedExec(query, cmdMap)
|
tx.NamedExec(query, cmdMap)
|
||||||
}
|
}
|
||||||
@ -879,6 +887,20 @@ func UpdateWithUpdateOpenAICmdInfoPacket(ctx context.Context, screenId string, m
|
|||||||
return UpdateWithCurrentOpenAICmdInfoChat(screenId)
|
return UpdateWithCurrentOpenAICmdInfoChat(screenId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateCmdForRestart(ctx context.Context, ck base.CommandKey, ts int64, cmdPid int, remotePid int, termOpts *TermOpts) error {
|
||||||
|
return WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `UPDATE cmd
|
||||||
|
SET restartts = ?, status = ?, exitcode = ?, cmdpid = ?, remotepid = ?, durationms = ?, termopts = ?, origtermopts = ?
|
||||||
|
WHERE screenid = ? AND lineid = ?`
|
||||||
|
tx.Exec(query, ts, CmdStatusRunning, 0, cmdPid, remotePid, 0, quickJson(termOpts), quickJson(termOpts), ck.GetGroupId(), lineIdFromCK(ck))
|
||||||
|
query = `UPDATE history
|
||||||
|
SET ts = ?, status = ?, exitcode = ?, durationms = ?
|
||||||
|
WHERE screenid = ? AND lineid = ?`
|
||||||
|
tx.Exec(query, ts, CmdStatusRunning, 0, 0, ck.GetGroupId(), lineIdFromCK(ck))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) {
|
func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) {
|
||||||
if donePk == nil {
|
if donePk == nil {
|
||||||
return nil, fmt.Errorf("invalid cmddone packet")
|
return nil, fmt.Errorf("invalid cmddone packet")
|
||||||
@ -922,7 +944,12 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
|
|||||||
} else {
|
} else {
|
||||||
indicator = StatusIndicatorLevel_Error
|
indicator = StatusIndicatorLevel_Error
|
||||||
}
|
}
|
||||||
SetStatusIndicatorLevel_Update(ctx, update, screenId, indicator, false)
|
|
||||||
|
err := SetStatusIndicatorLevel_Update(ctx, update, screenId, indicator, false)
|
||||||
|
if err != nil {
|
||||||
|
// This is not a fatal error, so just log it
|
||||||
|
log.Printf("error setting status indicator level after done packet: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
@ -1080,7 +1107,11 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*
|
|||||||
update.OpenAICmdInfoChat = ScreenMemGetCmdInfoChat(screenId).Messages
|
update.OpenAICmdInfoChat = ScreenMemGetCmdInfoChat(screenId).Messages
|
||||||
|
|
||||||
// Clear any previous status indicator for this screen
|
// Clear any previous status indicator for this screen
|
||||||
ResetStatusIndicator_Update(update, screenId)
|
err := ResetStatusIndicator_Update(update, screenId)
|
||||||
|
if err != nil {
|
||||||
|
// This is not a fatal error, so just log it
|
||||||
|
log.Printf("error resetting status indicator when switching screens: %v\n", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
@ -1490,12 +1521,16 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err
|
|||||||
func DeleteScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) {
|
func DeleteScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) {
|
||||||
var lineIds []string
|
var lineIds []string
|
||||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
query := `SELECT lineid FROM line WHERE screenid = ?`
|
query := `SELECT lineid FROM line
|
||||||
lineIds = tx.SelectStrings(query, screenId)
|
WHERE screenid = ?
|
||||||
query = `DELETE FROM line WHERE screenid = ?`
|
AND NOT EXISTS (SELECT lineid FROM cmd c WHERE c.screenid = ? AND c.lineid = line.lineid AND c.status IN ('running', 'detached'))`
|
||||||
tx.Exec(query, screenId)
|
lineIds = tx.SelectStrings(query, screenId, screenId)
|
||||||
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ?`
|
query = `DELETE FROM line
|
||||||
tx.Exec(query, screenId)
|
WHERE screenid = ? AND lineid IN (SELECT value FROM json_each(?))`
|
||||||
|
tx.Exec(query, screenId, quickJsonArr(lineIds))
|
||||||
|
query = `UPDATE history SET lineid = '', linenum = 0
|
||||||
|
WHERE screenid = ? AND lineid IN (SELECT value FROM json_each(?))`
|
||||||
|
tx.Exec(query, screenId, quickJsonArr(lineIds))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
@ -2091,6 +2126,19 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar
|
|||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetScreenSelectedLineId(ctx context.Context, screenId string) (string, error) {
|
||||||
|
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
||||||
|
query := `SELECT selectedline FROM screen WHERE screenid = ?`
|
||||||
|
sline := tx.GetInt(query, screenId)
|
||||||
|
if sline <= 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
query = `SELECT lineid FROM line WHERE screenid = ? AND linenum = ?`
|
||||||
|
lineId := tx.GetString(query, screenId, sline)
|
||||||
|
return lineId, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// returns updated screen (only if updated)
|
// returns updated screen (only if updated)
|
||||||
func FixupScreenSelectedLine(ctx context.Context, screenId string) (*ScreenType, error) {
|
func FixupScreenSelectedLine(ctx context.Context, screenId string) (*ScreenType, error) {
|
||||||
return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) {
|
return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) {
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/cirfile"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/cirfile"
|
||||||
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,6 +40,27 @@ func StatCmdPtyFile(ctx context.Context, screenId string, lineId string) (*cirfi
|
|||||||
return cirfile.StatCirFile(ctx, ptyOutFileName)
|
return cirfile.StatCirFile(ctx, ptyOutFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClearCmdPtyFile(ctx context.Context, screenId string, lineId string) error {
|
||||||
|
ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stat, err := cirfile.StatCirFile(ctx, ptyOutFileName)
|
||||||
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Remove(ptyOutFileName) // ignore error
|
||||||
|
var maxSize int64 = shexec.DefaultMaxPtySize
|
||||||
|
if stat != nil {
|
||||||
|
maxSize = stat.MaxSize
|
||||||
|
}
|
||||||
|
err = CreateCmdPtyFile(ctx, screenId, lineId, maxSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func AppendToCmdPtyBlob(ctx context.Context, screenId string, lineId string, data []byte, pos int64) (*PtyDataUpdate, error) {
|
func AppendToCmdPtyBlob(ctx context.Context, screenId string, lineId string, data []byte, pos int64) (*PtyDataUpdate, error) {
|
||||||
if screenId == "" {
|
if screenId == "" {
|
||||||
return nil, fmt.Errorf("cannot append to PtyBlob, screenid is not set")
|
return nil, fmt.Errorf("cannot append to PtyBlob, screenid is not set")
|
||||||
|
@ -153,13 +153,15 @@ func ScreenMemSetCmdInputText(screenId string, sp utilfn.StrWithPos, seqNum int)
|
|||||||
ScreenMemStore[screenId].CmdInputSeqNum = seqNum
|
ScreenMemStore[screenId].CmdInputSeqNum = seqNum
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScreenMemSetNumRunningCommands(screenId string, num int) {
|
func ScreenMemIncrementNumRunningCommands(screenId string, delta int) int {
|
||||||
MemLock.Lock()
|
MemLock.Lock()
|
||||||
defer MemLock.Unlock()
|
defer MemLock.Unlock()
|
||||||
if ScreenMemStore[screenId] == nil {
|
if ScreenMemStore[screenId] == nil {
|
||||||
ScreenMemStore[screenId] = &ScreenMemState{}
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
}
|
}
|
||||||
ScreenMemStore[screenId].NumRunningCommands = num
|
newNum := ScreenMemStore[screenId].NumRunningCommands + delta
|
||||||
|
ScreenMemStore[screenId].NumRunningCommands = newNum
|
||||||
|
return newNum
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the new indicator level is higher than the current indicator, update the current indicator. Returns the new indicator level.
|
// If the new indicator level is higher than the current indicator, update the current indicator. Returns the new indicator level.
|
||||||
|
@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MaxMigration = 30
|
const MaxMigration = 31
|
||||||
const MigratePrimaryScreenVersion = 9
|
const MigratePrimaryScreenVersion = 9
|
||||||
const CmdScreenSpecialMigration = 13
|
const CmdScreenSpecialMigration = 13
|
||||||
const CmdLineSpecialMigration = 20
|
const CmdLineSpecialMigration = 20
|
||||||
|
@ -1118,13 +1118,15 @@ type CmdType struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CmdPid int `json:"cmdpid"`
|
CmdPid int `json:"cmdpid"`
|
||||||
RemotePid int `json:"remotepid"`
|
RemotePid int `json:"remotepid"`
|
||||||
|
RestartTs int64 `json:"restartts,omitempty"`
|
||||||
DoneTs int64 `json:"donets"`
|
DoneTs int64 `json:"donets"`
|
||||||
ExitCode int `json:"exitcode"`
|
ExitCode int `json:"exitcode"`
|
||||||
DurationMs int `json:"durationms"`
|
DurationMs int `json:"durationms"`
|
||||||
RunOut []packet.PacketType `json:"runout,omitempty"`
|
RunOut []packet.PacketType `json:"runout,omitempty"`
|
||||||
RtnState bool `json:"rtnstate,omitempty"`
|
RtnState bool `json:"rtnstate,omitempty"`
|
||||||
RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"`
|
RtnStatePtr ShellStatePtr `json:"rtnstateptr,omitempty"`
|
||||||
Remove bool `json:"remove,omitempty"`
|
Remove bool `json:"remove,omitempty"` // not persisted to DB
|
||||||
|
Restarted bool `json:"restarted,omitempty"` // not persisted to DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RemoteType) ToMap() map[string]interface{} {
|
func (r *RemoteType) ToMap() map[string]interface{} {
|
||||||
@ -1189,6 +1191,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} {
|
|||||||
rtn["status"] = cmd.Status
|
rtn["status"] = cmd.Status
|
||||||
rtn["cmdpid"] = cmd.CmdPid
|
rtn["cmdpid"] = cmd.CmdPid
|
||||||
rtn["remotepid"] = cmd.RemotePid
|
rtn["remotepid"] = cmd.RemotePid
|
||||||
|
rtn["restartts"] = cmd.RestartTs
|
||||||
rtn["donets"] = cmd.DoneTs
|
rtn["donets"] = cmd.DoneTs
|
||||||
rtn["exitcode"] = cmd.ExitCode
|
rtn["exitcode"] = cmd.ExitCode
|
||||||
rtn["durationms"] = cmd.DurationMs
|
rtn["durationms"] = cmd.DurationMs
|
||||||
@ -1216,6 +1219,7 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool {
|
|||||||
quickSetInt(&cmd.CmdPid, m, "cmdpid")
|
quickSetInt(&cmd.CmdPid, m, "cmdpid")
|
||||||
quickSetInt(&cmd.RemotePid, m, "remotepid")
|
quickSetInt(&cmd.RemotePid, m, "remotepid")
|
||||||
quickSetInt64(&cmd.DoneTs, m, "donets")
|
quickSetInt64(&cmd.DoneTs, m, "donets")
|
||||||
|
quickSetInt64(&cmd.RestartTs, m, "restartts")
|
||||||
quickSetInt(&cmd.ExitCode, m, "exitcode")
|
quickSetInt(&cmd.ExitCode, m, "exitcode")
|
||||||
quickSetInt(&cmd.DurationMs, m, "durationms")
|
quickSetInt(&cmd.DurationMs, m, "durationms")
|
||||||
quickSetJson(&cmd.RunOut, m, "runout")
|
quickSetJson(&cmd.RunOut, m, "runout")
|
||||||
@ -1474,7 +1478,6 @@ func SetReleaseInfo(ctx context.Context, releaseInfo ReleaseInfoType) error {
|
|||||||
// Sets the in-memory status indicator for the given screenId to the given value and adds it to the ModelUpdate. By default, the active screen will be ignored when updating status. To force a status update for the active screen, set force=true.
|
// Sets the in-memory status indicator for the given screenId to the given value and adds it to the ModelUpdate. By default, the active screen will be ignored when updating status. To force a status update for the active screen, set force=true.
|
||||||
func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, screenId string, level StatusIndicatorLevel, force bool) error {
|
func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, screenId string, level StatusIndicatorLevel, force bool) error {
|
||||||
var newStatus StatusIndicatorLevel
|
var newStatus StatusIndicatorLevel
|
||||||
|
|
||||||
if force {
|
if force {
|
||||||
// Force the update and set the new status to the given level, regardless of the current status or the active screen
|
// Force the update and set the new status to the given level, regardless of the current status or the active screen
|
||||||
ScreenMemSetIndicatorLevel(screenId, level)
|
ScreenMemSetIndicatorLevel(screenId, level)
|
||||||
@ -1511,14 +1514,14 @@ func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets the in-memory status indicator for the given screenId to the given value and pushes the new value to the FE
|
// Sets the in-memory status indicator for the given screenId to the given value and pushes the new value to the FE
|
||||||
func SetStatusIndicatorLevel(ctx context.Context, screenId string, level StatusIndicatorLevel, force bool) {
|
func SetStatusIndicatorLevel(ctx context.Context, screenId string, level StatusIndicatorLevel, force bool) error {
|
||||||
update := &ModelUpdate{}
|
update := &ModelUpdate{}
|
||||||
err := SetStatusIndicatorLevel_Update(ctx, update, screenId, level, false)
|
err := SetStatusIndicatorLevel_Update(ctx, update, screenId, level, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error setting status indicator level: %v\n", err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
MainBus.SendUpdate(update)
|
MainBus.SendUpdate(update)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resets the in-memory status indicator for the given screenId to StatusIndicatorLevel_None and adds it to the ModelUpdate
|
// Resets the in-memory status indicator for the given screenId to StatusIndicatorLevel_None and adds it to the ModelUpdate
|
||||||
@ -1528,7 +1531,23 @@ func ResetStatusIndicator_Update(update *ModelUpdate, screenId string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resets the in-memory status indicator for the given screenId to StatusIndicatorLevel_None and pushes the new value to the FE
|
// Resets the in-memory status indicator for the given screenId to StatusIndicatorLevel_None and pushes the new value to the FE
|
||||||
func ResetStatusIndicator(screenId string) {
|
func ResetStatusIndicator(screenId string) error {
|
||||||
// We do not need to set context when resetting the status indicator because we will not need to call the DB
|
// We do not need to set context when resetting the status indicator because we will not need to call the DB
|
||||||
SetStatusIndicatorLevel(context.TODO(), screenId, StatusIndicatorLevel_None, true)
|
return SetStatusIndicatorLevel(context.TODO(), screenId, StatusIndicatorLevel_None, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementNumRunningCmds_Update(update *ModelUpdate, screenId string, delta int) {
|
||||||
|
newNum := ScreenMemIncrementNumRunningCommands(screenId, delta)
|
||||||
|
log.Printf("IncrementNumRunningCmds_Update: screenId=%s, newNum=%d\n", screenId, newNum)
|
||||||
|
update.ScreenNumRunningCommands = &ScreenNumRunningCommandsType{
|
||||||
|
ScreenId: screenId,
|
||||||
|
Num: newNum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementNumRunningCmds(screenId string, delta int) {
|
||||||
|
log.Printf("IncrementNumRunningCmds: screenId=%s, delta=%d\n", screenId, delta)
|
||||||
|
update := &ModelUpdate{}
|
||||||
|
IncrementNumRunningCmds_Update(update, screenId, delta)
|
||||||
|
MainBus.SendUpdate(update)
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,7 @@ type ModelUpdate struct {
|
|||||||
OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"`
|
OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"`
|
||||||
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
|
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
|
||||||
ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"`
|
ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"`
|
||||||
|
ScreenNumRunningCommands *ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
||||||
UserInputRequest *UserInputRequestType `json:"userinputrequest,omitempty"`
|
UserInputRequest *UserInputRequestType `json:"userinputrequest,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,6 +287,11 @@ type ScreenStatusIndicatorType struct {
|
|||||||
Status StatusIndicatorLevel `json:"status"`
|
Status StatusIndicatorLevel `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScreenNumRunningCommandsType struct {
|
||||||
|
ScreenId string `json:"screenid"`
|
||||||
|
Num int `json:"num"`
|
||||||
|
}
|
||||||
|
|
||||||
func (bus *UpdateBus) registerUserInputChannel() (string, chan *UserInputResponseType) {
|
func (bus *UpdateBus) registerUserInputChannel() (string, chan *UserInputResponseType) {
|
||||||
bus.Lock.Lock()
|
bus.Lock.Lock()
|
||||||
defer bus.Lock.Unlock()
|
defer bus.Lock.Unlock()
|
||||||
|
Loading…
Reference in New Issue
Block a user