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
88
.github/workflows/build-helper.yml
vendored
88
.github/workflows/build-helper.yml
vendored
@ -1,9 +1,9 @@
|
||||
name: "Build Helper"
|
||||
on: workflow_dispatch
|
||||
env:
|
||||
WAVETERM_VERSION: 0.6.0
|
||||
GO_VERSION: '1.21.5'
|
||||
NODE_VERSION: '21.5.0'
|
||||
WAVETERM_VERSION: 0.6.1
|
||||
GO_VERSION: "1.21.5"
|
||||
NODE_VERSION: "21.5.0"
|
||||
jobs:
|
||||
runbuild-darwin-x64:
|
||||
name: "Build MacOS x64"
|
||||
@ -12,23 +12,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
wavesrv/go.sum
|
||||
waveshell/go.sum
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
wavesrv/go.sum
|
||||
waveshell/go.sum
|
||||
- run: brew tap scripthaus-dev/scripthaus
|
||||
- run: brew install scripthaus
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: 'yarn'
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: "yarn"
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: scripthaus run build-package
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: waveterm-build-darwin-x64
|
||||
path: out/make/zip/darwin/x64/*.zip
|
||||
retention-days: 2
|
||||
name: waveterm-build-darwin-x64
|
||||
path: out/make/zip/darwin/x64/*.zip
|
||||
retention-days: 2
|
||||
runbuild-darwin-arm64:
|
||||
name: "Build MacOS arm64"
|
||||
runs-on: macos-latest-xlarge
|
||||
@ -36,23 +36,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
wavesrv/go.sum
|
||||
waveshell/go.sum
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
wavesrv/go.sum
|
||||
waveshell/go.sum
|
||||
- run: brew tap scripthaus-dev/scripthaus
|
||||
- run: brew install scripthaus
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: 'yarn'
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: "yarn"
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: scripthaus run build-package
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: waveterm-build-darwin-arm64
|
||||
path: out/make/zip/darwin/arm64/*.zip
|
||||
retention-days: 2
|
||||
name: waveterm-build-darwin-arm64
|
||||
path: out/make/zip/darwin/arm64/*.zip
|
||||
retention-days: 2
|
||||
runbuild-linux:
|
||||
name: "Build Linux x64"
|
||||
runs-on: ubuntu-latest
|
||||
@ -61,42 +61,40 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: scripthaus-dev/scripthaus
|
||||
path: scripthaus
|
||||
repository: scripthaus-dev/scripthaus
|
||||
path: scripthaus
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
wavesrv/go.sum
|
||||
waveshell/go.sum
|
||||
scripthaus/go.sum
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
wavesrv/go.sum
|
||||
waveshell/go.sum
|
||||
scripthaus/go.sum
|
||||
- run: |
|
||||
go work use ./scripthaus;
|
||||
cd scripthaus;
|
||||
go get ./...;
|
||||
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
|
||||
go work use ./scripthaus;
|
||||
cd scripthaus;
|
||||
go get ./...;
|
||||
CGO_ENABLED=1 go build -o scripthaus cmd/main.go
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: 'yarn'
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: "yarn"
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: ./scripthaus/scripthaus run build-package-linux
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
path: buildtemp
|
||||
merge-multiple: true
|
||||
path: buildtemp
|
||||
- run: |
|
||||
mv out/make/zip/linux/x64/Wave-linux-x64-$WAVETERM_VERSION.zip buildtemp/waveterm-linux-x64-v$WAVETERM_VERSION.zip
|
||||
mv out/make/zip/linux/x64/Wave-linux-x64-$WAVETERM_VERSION.zip buildtemp/waveterm-linux-x64-v$WAVETERM_VERSION.zip
|
||||
- run: (cd buildtemp; zip ../waveterm-builds.zip *)
|
||||
- run: aws s3 cp waveterm-builds.zip s3://waveterm-github-artifacts/
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.S3_USERID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.S3_SECRETKEY }}"
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.S3_USERID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.S3_SECRETKEY }}"
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: waveterm-builds
|
||||
path: buildtemp
|
||||
retention-days: 2
|
||||
|
||||
|
||||
name: waveterm-builds
|
||||
path: buildtemp
|
||||
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
|
||||
|
||||
# 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 builds
|
||||
mkdir temp
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "waveterm",
|
||||
"author": "Command Line Inc",
|
||||
"productName": "Wave",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"main": "dist/emain.js",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -99,9 +99,6 @@ body a {
|
||||
|
||||
body code {
|
||||
font-family: @terminal-font;
|
||||
}
|
||||
|
||||
body code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@ -123,11 +120,19 @@ svg.icon {
|
||||
}
|
||||
|
||||
.hideScrollbarUntillHover {
|
||||
overflow: hidden;
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@ -625,13 +630,13 @@ a.a-block {
|
||||
|
||||
.spin {
|
||||
animation: infiniteRotate 2s linear infinite;
|
||||
|
||||
|
||||
@keyframes infiniteRotate {
|
||||
from {
|
||||
transform:rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform:rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -647,7 +652,6 @@ a.a-block {
|
||||
margin-right: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(241, 246, 243, 0.08);
|
||||
background: rgba(13, 13, 13, 0.85);
|
||||
|
||||
.header {
|
||||
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 |
@ -405,7 +405,7 @@
|
||||
padding: 6px 6px 6px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
pre.selected {
|
||||
outline: 2px solid @term-green;
|
||||
}
|
||||
@ -609,7 +609,6 @@
|
||||
|
||||
.wave-dropdown {
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
height: 44px;
|
||||
min-width: 150px;
|
||||
width: 100%;
|
||||
@ -715,9 +714,7 @@
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
margin-top: 2px;
|
||||
padding: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
@ -775,7 +772,6 @@
|
||||
min-width: 412px;
|
||||
gap: 6px;
|
||||
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));
|
||||
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;
|
||||
@ -931,7 +927,6 @@
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
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 { GlobalModel } from "../../model/model";
|
||||
import * as appconst from "../appconst";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
||||
@ -117,7 +118,7 @@ class Checkbox extends React.Component<
|
||||
constructor(props) {
|
||||
super(props);
|
||||
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++}`;
|
||||
}
|
||||
@ -287,15 +288,16 @@ class Button extends React.Component<ButtonProps> {
|
||||
}
|
||||
|
||||
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 (
|
||||
<button
|
||||
className={cn("wave-button", theme, variant, color, { disabled: disabled })}
|
||||
className={cn("wave-button", theme, variant, color, { disabled: disabled }, className)}
|
||||
onClick={this.handleClick}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
autoFocus={this.props.autoFocus}
|
||||
autoFocus={autoFocus}
|
||||
>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
@ -714,13 +716,14 @@ class InlineSettingsTextEdit extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(e: any): void {
|
||||
if (e.code == "Enter") {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.confirmChange();
|
||||
return;
|
||||
}
|
||||
if (e.code == "Escape") {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.cancelChange();
|
||||
@ -868,7 +871,7 @@ class Markdown extends React.Component<
|
||||
if (codeSelect) {
|
||||
return <CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex}>{props.children}</CodeBlockMarkdown>;
|
||||
} else {
|
||||
let clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||
let blockText = (e.target as HTMLElement).innerText;
|
||||
if (blockText) {
|
||||
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
|
||||
@ -896,7 +899,9 @@ class Markdown extends React.Component<
|
||||
};
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -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) {
|
||||
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).
|
||||
@ -1313,6 +1286,5 @@ export {
|
||||
LinkButton,
|
||||
Status,
|
||||
Modal,
|
||||
StatusIndicator,
|
||||
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 { Line } from "../line/linecomps";
|
||||
import { CmdStrCode } from "../common/common";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||
|
||||
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
|
||||
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||
@ -95,14 +96,14 @@ function formatSessionName(snames: Record<string, string>, sessionId: string): s
|
||||
}
|
||||
|
||||
@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
|
||||
clickHandler(): void {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
if (this.props.checked) {
|
||||
return <CheckedCheckbox onClick={this.clickHandler} className="history-checkbox checkbox-icon" />;
|
||||
@ -110,14 +111,18 @@ class HistoryCheckbox extends React.Component<{ checked: boolean, partialCheck?:
|
||||
if (this.props.partialCheck) {
|
||||
return (
|
||||
<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"/>
|
||||
<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" 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"
|
||||
/>
|
||||
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" stroke="#3B3F3A" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return <div onClick={this.clickHandler} className="history-checkbox state-unchecked"/>
|
||||
);
|
||||
} else {
|
||||
return <div onClick={this.clickHandler} className="history-checkbox state-unchecked" />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -207,7 +212,8 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
|
||||
@boundMethod
|
||||
searchKeyDown(e: any) {
|
||||
if (e.code == "Enter") {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
GlobalModel.historyViewModel.submitSearch();
|
||||
return;
|
||||
@ -452,8 +458,8 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
<div onClick={this.toggleSessionDropdown}>
|
||||
<span className="label">
|
||||
{hvm.searchSessionId.get() == null
|
||||
? "Limit Workspace"
|
||||
: formatSessionName(snames, hvm.searchSessionId.get())}
|
||||
? "Limit Workspace"
|
||||
: formatSessionName(snames, hvm.searchSessionId.get())}
|
||||
</span>
|
||||
<AngleDownIcon className="icon" />
|
||||
</div>
|
||||
@ -486,8 +492,8 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
<div onClick={this.toggleRemoteDropdown}>
|
||||
<span className="label">
|
||||
{hvm.searchRemoteId.get() == null
|
||||
? "Limit Remote"
|
||||
: formatRemoteName(rnames, { remoteid: hvm.searchRemoteId.get() })}
|
||||
? "Limit Remote"
|
||||
: formatRemoteName(rnames, { remoteid: hvm.searchRemoteId.get() })}
|
||||
</span>
|
||||
<AngleDownIcon className="icon" />
|
||||
</div>
|
||||
@ -513,9 +519,7 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="fromts">
|
||||
<div className="fromts-text">
|
||||
From:
|
||||
</div>
|
||||
<div className="fromts-text">From: </div>
|
||||
<div className="hoverEffect">
|
||||
<input
|
||||
type="date"
|
||||
@ -550,7 +554,10 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
</div>
|
||||
<div className={cn("control-bar", "is-top", { "is-hidden": items.length == 0 })}>
|
||||
<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
|
||||
className={cn(
|
||||
@ -562,7 +569,7 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
>
|
||||
<span>
|
||||
<TrashIcon className="trash-icon" title="Purge Selected Items" />
|
||||
Delete Items
|
||||
Delete Items
|
||||
</span>
|
||||
</div>
|
||||
<div className="spacer" />
|
||||
@ -591,7 +598,7 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
className={cn("history-item", { "is-selected": hvm.selectedItems.get(item.historyid) })}
|
||||
>
|
||||
<td className="selectbox" onClick={() => this.handleSelect(item.historyid)}>
|
||||
<HistoryCheckbox checked={hvm.selectedItems.get(item.historyid)}/>
|
||||
<HistoryCheckbox checked={hvm.selectedItems.get(item.historyid)} />
|
||||
</td>
|
||||
<td className="cmdstr">
|
||||
<HistoryCmdStr
|
||||
@ -608,13 +615,31 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
<td className="ts text-standard">{getHistoryViewTs(nowDate, item.ts)}</td>
|
||||
<td className="downarrow" onClick={() => this.activateItem(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">
|
||||
<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
|
||||
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>
|
||||
</If>
|
||||
<If condition={activeItemId == item.historyid}>
|
||||
<svg 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
|
||||
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>
|
||||
</If>
|
||||
</td>
|
||||
|
@ -360,6 +360,12 @@ class LineCmd extends React.Component<
|
||||
GlobalCommandRunner.lineDelete(line.lineid, true);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickRestart() {
|
||||
let { line } = this.props;
|
||||
GlobalCommandRunner.lineRestart(line.lineid, true);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickMinimize() {
|
||||
mobx.action(() => {
|
||||
@ -467,12 +473,21 @@ class LineCmd extends React.Component<
|
||||
renderMeta1(cmd: Cmd) {
|
||||
let { line } = this.props;
|
||||
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;
|
||||
return (
|
||||
<div key="meta1" className="meta meta-line1">
|
||||
<SmallLineAvatar line={line} cmd={cmd} />
|
||||
<div className="ts">{formattedTime}</div>
|
||||
<div title={timeTitle} className="ts">{formattedTime}</div>
|
||||
<div> </div>
|
||||
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
||||
<div className="renderer">
|
||||
@ -665,6 +680,9 @@ class LineCmd extends React.Component<
|
||||
{this.renderMeta1(cmd)}
|
||||
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
|
||||
</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}>
|
||||
<i className="fa-sharp fa-regular fa-trash" />
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../../app/common/themes/themes.less";
|
||||
@import "../../app/common/icons/icons.less";
|
||||
|
||||
.main-sidebar {
|
||||
padding: 0;
|
||||
@ -23,14 +24,20 @@
|
||||
&.collapsed {
|
||||
width: 6em;
|
||||
min-width: 6em;
|
||||
.arrow-container, .collapse-button {
|
||||
|
||||
.arrow-container,
|
||||
.collapse-button {
|
||||
transform: rotate(180deg);
|
||||
margin-top: 20px;
|
||||
}
|
||||
.contents {
|
||||
margin-top: 26px;
|
||||
|
||||
.top, .workspaces-item, .middle, .bottom, .separator {
|
||||
|
||||
.top,
|
||||
.workspaces-item,
|
||||
.middle,
|
||||
.bottom,
|
||||
.separator {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
@ -82,7 +89,7 @@
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
@ -90,7 +97,7 @@
|
||||
.collapse-button {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
margin-right: 14px;
|
||||
|
||||
|
||||
svg {
|
||||
margin-top: 3px;
|
||||
width: 1.5em;
|
||||
@ -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;
|
||||
}
|
||||
.index {
|
||||
margin: 0 1.5em 0 -0.5rem;
|
||||
font-size: 0.8em;
|
||||
vertical-align: middle;
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
display: inline-block;
|
||||
}
|
||||
.hotkey {
|
||||
float: left !important;
|
||||
@ -145,58 +149,70 @@
|
||||
|
||||
.item {
|
||||
padding: 5px;
|
||||
margin: 0 6px;
|
||||
margin-left: 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
|
||||
.sessionName {
|
||||
width: 12rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: inherit;
|
||||
max-width: inherit;
|
||||
min-width: inherit;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.front-icon {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
|
||||
.end-icons {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.item-contents {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.icon {
|
||||
margin: -2px 8px 0px 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.actions.icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.hotkey {
|
||||
float: right;
|
||||
margin-right: 6px;
|
||||
visibility: hidden;
|
||||
letter-spacing: 6px;
|
||||
}
|
||||
.disabled .hotkey {
|
||||
display: none;
|
||||
&:hover {
|
||||
:not(.disabled) .hotkey {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
.actions {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
}
|
||||
&:hover .hotkey {
|
||||
visibility: visible;
|
||||
}
|
||||
.actions {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover .actions {
|
||||
visibility: visible;
|
||||
|
||||
&:not(:hover) .status-indicator {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
.add_workspace {
|
||||
float: right;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 2px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
vertical-align: middle;
|
||||
svg {
|
||||
fill: @base-color;
|
||||
}
|
||||
}
|
||||
|
||||
.front-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.end-icons {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.fa-discord {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
|
@ -12,27 +12,54 @@ import { If } from "tsx-control-statements/components";
|
||||
import { compareLoose } from "semver";
|
||||
|
||||
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 ConnectionsIcon } from "../assets/icons/connections.svg";
|
||||
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.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 { 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 "./sidebar.less";
|
||||
import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common/icons/icons";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
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
|
||||
class MainSideBar extends React.Component<{}, {}> {
|
||||
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
||||
@ -99,7 +126,6 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
handlePlaybookClick(): void {
|
||||
console.log("playbook click");
|
||||
return;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -159,42 +185,29 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
}
|
||||
return sessionList.map((session, index) => {
|
||||
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 (
|
||||
<div
|
||||
key={index}
|
||||
className={`item hoverEffect ${isActive ? "active" : ""}`}
|
||||
<SideBarItem
|
||||
className={`${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)}
|
||||
>
|
||||
<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() {
|
||||
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 clientData = GlobalModel.clientData.get();
|
||||
let needsUpdate = false;
|
||||
@ -223,65 +236,66 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
</div>
|
||||
<div className="separator" />
|
||||
<div className="top">
|
||||
<div className="item hoverEffect unselectable" onClick={this.handleHistoryClick}>
|
||||
<HistoryIcon className="icon" />
|
||||
History
|
||||
<span className="hotkey">⌘H</span>
|
||||
</div>
|
||||
{/* <div className="item hoverEffect unselectable" onClick={this.handleBookmarksClick}>
|
||||
<FavoritesIcon className="icon" />
|
||||
Favorites
|
||||
<span className="hotkey">⌘B</span>
|
||||
</div> */}
|
||||
<div className="item hoverEffect unselectable" onClick={this.handleConnectionsClick}>
|
||||
<ConnectionsIcon className="icon" />
|
||||
Connections
|
||||
</div>
|
||||
<SideBarItem
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
|
||||
contents="History"
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
|
||||
onClick={this.handleHistoryClick}
|
||||
/>
|
||||
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">⌘B</span>} onClick={this.handleBookmarksClick}/> */}
|
||||
<SideBarItem
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
|
||||
contents="Connections"
|
||||
onClick={this.handleConnectionsClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="separator" />
|
||||
<div className="item workspaces-item unselectable">
|
||||
<WorkspacesIcon className="icon" />
|
||||
Workspaces
|
||||
<div className="add_workspace hoverEffect" onClick={this.handleNewSession}>
|
||||
<AddIcon />
|
||||
</div>
|
||||
</div>
|
||||
<SideBarItem
|
||||
frontIcon={<WorkspacesIcon className="icon" />}
|
||||
contents="Workspaces"
|
||||
endIcons={[
|
||||
<div
|
||||
key="add_workspace"
|
||||
className="add_workspace hoverEffect"
|
||||
onClick={this.handleNewSession}
|
||||
>
|
||||
<AddIcon />
|
||||
</div>,
|
||||
]}
|
||||
/>
|
||||
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
||||
<div className="bottom">
|
||||
<If condition={needsUpdate}>
|
||||
<div
|
||||
className="item hoverEffect unselectable updateBanner"
|
||||
<SideBarItem
|
||||
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")}
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-up icon" />
|
||||
Update Available
|
||||
</div>
|
||||
/>
|
||||
</If>
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<div className="item hoverEffect unselectable" onClick={this.handlePluginsClick}>
|
||||
<AppsIcon className="icon" />
|
||||
Apps
|
||||
<span className="hotkey">⌘A</span>
|
||||
</div>
|
||||
<SideBarItem
|
||||
frontIcon={<AppsIcon className="icon" />}
|
||||
contents="Apps"
|
||||
onClick={this.handlePluginsClick}
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
|
||||
/>
|
||||
</If>
|
||||
<div className="item hoverEffect unselectable" onClick={this.handleSettingsClick}>
|
||||
<SettingsIcon className="icon" />
|
||||
Settings
|
||||
</div>
|
||||
<div
|
||||
className="item hoverEffect unselectable"
|
||||
<SideBarItem
|
||||
frontIcon={<SettingsIcon className="icon" />}
|
||||
contents="Settings"
|
||||
onClick={this.handleSettingsClick}
|
||||
/>
|
||||
<SideBarItem
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
|
||||
contents="Documentation"
|
||||
onClick={() => openLink("https://docs.waveterm.dev")}
|
||||
>
|
||||
<HelpIcon className="icon" />
|
||||
Documentation
|
||||
</div>
|
||||
<div
|
||||
className="item hoverEffect unselectable"
|
||||
/>
|
||||
<SideBarItem
|
||||
frontIcon={<i className="fa-brands fa-discord icon" />}
|
||||
contents="Discord"
|
||||
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
|
||||
>
|
||||
<DiscordIcon className="icon discord" />
|
||||
Discord
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ import { TextAreaInput } from "./textareainput";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
||||
import { Markdown } from "../../common/common";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||
|
||||
@mobxReact.observer
|
||||
class AIChat extends React.Component<{}, {}> {
|
||||
@ -76,8 +77,8 @@ class AIChat extends React.Component<{}, {}> {
|
||||
let inputModel = model.inputModel;
|
||||
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
||||
let resetCodeSelect = !ctrlMod;
|
||||
|
||||
if (e.code == "Enter") {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
if (!ctrlMod) {
|
||||
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||
@ -91,17 +92,18 @@ class AIChat extends React.Component<{}, {}> {
|
||||
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
|
||||
}
|
||||
}
|
||||
if (e.code == "Escape") {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
inputModel.closeAIAssistantChat();
|
||||
}
|
||||
if (e.code == "KeyL" && e.getModifierState("Control")) {
|
||||
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:l")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
inputModel.clearAIAssistantChat();
|
||||
}
|
||||
if (e.code == "ArrowUp") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||
if (this.getLinePos(e.target).linePos > 1) {
|
||||
// normal up arrow
|
||||
return;
|
||||
@ -110,7 +112,7 @@ class AIChat extends React.Component<{}, {}> {
|
||||
inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||
resetCodeSelect = false;
|
||||
}
|
||||
if (e.code == "ArrowDown") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||
return;
|
||||
}
|
||||
|
@ -82,7 +82,7 @@
|
||||
.cmd-input-filter {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@ -198,14 +198,14 @@
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cmd-aichat {
|
||||
|
||||
.cmd-aichat {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-flow: column nowrap;
|
||||
@ -219,7 +219,7 @@
|
||||
flex-shrink: 1;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
|
||||
.chat-textarea {
|
||||
color: @term-bright-white;
|
||||
background-color: @textarea-background;
|
||||
@ -242,29 +242,27 @@
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
margin-top:5px;
|
||||
margin-bottom:5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chat-msg-assistant {
|
||||
color: @term-white;
|
||||
}
|
||||
|
||||
.chat-msg-user {
|
||||
|
||||
.chat-msg-user {
|
||||
.msg-text {
|
||||
font-family: @markdown-font;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-msg-error {
|
||||
color: @term-bright-red;
|
||||
font-family: @markdown-font;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.grow-spacer {
|
||||
flex: 1 0 10px;
|
||||
|
@ -13,6 +13,7 @@ import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||
import { getMonoFontSize } from "../../../util/textmeasure";
|
||||
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
|
||||
import * as appconst from "../../appconst";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||
|
||||
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 curLine = inputModel.getCurLine();
|
||||
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
let lastTab = this.lastTab;
|
||||
this.lastTab = e.code == "Tab";
|
||||
this.lastTab = checkKeyPressed(waveEvent, "Tab");
|
||||
let lastHist = this.lastHistoryUpDown;
|
||||
this.lastHistoryUpDown = false;
|
||||
|
||||
if (e.code == "Tab") {
|
||||
if (checkKeyPressed(waveEvent, "Tab")) {
|
||||
e.preventDefault();
|
||||
if (lastTab) {
|
||||
GlobalModel.submitCommand(
|
||||
@ -204,7 +206,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.code == "Enter") {
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
if (!ctrlMod) {
|
||||
if (GlobalModel.inputModel.isEmpty()) {
|
||||
@ -224,7 +226,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
GlobalModel.inputModel.setCurLine(e.target.value);
|
||||
return;
|
||||
}
|
||||
if (e.code == "Escape") {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
@ -235,50 +237,50 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
inputModel.closeAIAssistantChat();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyE" && e.getModifierState("Meta")) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:e")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
inputModel.toggleExpandInput();
|
||||
}
|
||||
if (e.code == "KeyC" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:c")) {
|
||||
e.preventDefault();
|
||||
inputModel.resetInput();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyU" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:u")) {
|
||||
e.preventDefault();
|
||||
this.controlU();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyP" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:p")) {
|
||||
e.preventDefault();
|
||||
this.controlP();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyN" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:n")) {
|
||||
e.preventDefault();
|
||||
this.controlN();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyW" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:w")) {
|
||||
e.preventDefault();
|
||||
this.controlW();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyY" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:y")) {
|
||||
e.preventDefault();
|
||||
this.controlY();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyR" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:r")) {
|
||||
e.preventDefault();
|
||||
inputModel.openHistory();
|
||||
return;
|
||||
}
|
||||
if ((e.code == "ArrowUp" || e.code == "ArrowDown") && hasNoModifiers(e)) {
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp") || checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
if (!inputModel.isHistoryLoaded()) {
|
||||
if (e.code == "ArrowUp") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||
this.lastHistoryUpDown = true;
|
||||
inputModel.loadHistory(false, 1, "screen");
|
||||
}
|
||||
@ -286,7 +288,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
}
|
||||
// invisible history movement
|
||||
let linePos = this.getLinePos(e.target);
|
||||
if (e.code == "ArrowUp") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||
if (!lastHist && linePos.linePos > 1) {
|
||||
// regular arrow
|
||||
return;
|
||||
@ -296,7 +298,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
this.lastHistoryUpDown = true;
|
||||
return;
|
||||
}
|
||||
if (e.code == "ArrowDown") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
if (!lastHist && linePos.linePos < linePos.numLines) {
|
||||
// regular arrow
|
||||
return;
|
||||
@ -307,16 +309,16 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.code == "PageUp" || e.code == "PageDown") {
|
||||
if (checkKeyPressed(waveEvent, "PageUp") || checkKeyPressed(waveEvent, "PageDown")) {
|
||||
e.preventDefault();
|
||||
let infoScroll = inputModel.hasScrollingInfoMsg();
|
||||
if (infoScroll) {
|
||||
let div = document.querySelector(".cmd-input-info");
|
||||
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();
|
||||
inputModel.openAIAssistantChat();
|
||||
}
|
||||
@ -338,32 +340,29 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
|
||||
@boundMethod
|
||||
onHistoryKeyDown(e: any) {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
if (e.code == "Escape") {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
inputModel.resetHistory();
|
||||
return;
|
||||
}
|
||||
if (e.code == "Enter") {
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
inputModel.grabSelectedHistoryItem();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyG" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:g")) {
|
||||
e.preventDefault();
|
||||
inputModel.resetInput();
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyC" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:c")) {
|
||||
e.preventDefault();
|
||||
inputModel.resetInput();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.code == "KeyR" &&
|
||||
(e.getModifierState("Meta") || e.getModifierState("Control")) &&
|
||||
!e.getModifierState("Shift")
|
||||
) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:r") || checkKeyPressed(waveEvent, "Ctrl:r")) {
|
||||
e.preventDefault();
|
||||
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||
if (opts.limitRemote) {
|
||||
@ -376,7 +375,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
inputModel.setHistoryQueryOpts(opts);
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyS" && (e.getModifierState("Meta") || e.getModifierState("Control"))) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:s") || checkKeyPressed(waveEvent, "Ctrl:s")) {
|
||||
e.preventDefault();
|
||||
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||
let htype = opts.queryType;
|
||||
@ -390,26 +389,26 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
inputModel.setHistoryType(htype);
|
||||
return;
|
||||
}
|
||||
if (e.code == "Tab") {
|
||||
if (checkKeyPressed(waveEvent, "Tab")) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code == "ArrowUp" || e.code == "ArrowDown") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp") || checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
e.preventDefault();
|
||||
inputModel.moveHistorySelection(e.code == "ArrowUp" ? 1 : -1);
|
||||
inputModel.moveHistorySelection(checkKeyPressed(waveEvent, "ArrowUp") ? 1 : -1);
|
||||
return;
|
||||
}
|
||||
if (e.code == "PageUp" || e.code == "PageDown") {
|
||||
if (checkKeyPressed(waveEvent, "PageUp") || checkKeyPressed(waveEvent, "PageDown")) {
|
||||
e.preventDefault();
|
||||
inputModel.moveHistorySelection(e.code == "PageUp" ? 10 : -10);
|
||||
inputModel.moveHistorySelection(checkKeyPressed(waveEvent, "PageUp") ? 10 : -10);
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyP" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:p")) {
|
||||
e.preventDefault();
|
||||
inputModel.moveHistorySelection(1);
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyN" && e.getModifierState("Control")) {
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:n")) {
|
||||
e.preventDefault();
|
||||
inputModel.moveHistorySelection(-1);
|
||||
return;
|
||||
|
@ -7,7 +7,8 @@ import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
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 * as constants from "../../appconst";
|
||||
import { Reorder } from "framer-motion";
|
||||
@ -79,14 +80,12 @@ class ScreenTab extends React.Component<
|
||||
|
||||
let tabIndex = null;
|
||||
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>
|
||||
</CenteredIcon>
|
||||
);
|
||||
}
|
||||
|
||||
let settings = (
|
||||
<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() ? (
|
||||
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
|
||||
) : null;
|
||||
@ -96,6 +95,7 @@ class ScreenTab extends React.Component<
|
||||
) : null;
|
||||
|
||||
const statusIndicatorLevel = screen.statusIndicator.get();
|
||||
const runningCommands = screen.numRunningCmds.get() > 0;
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
@ -115,20 +115,16 @@ class ScreenTab extends React.Component<
|
||||
onContextMenu={(event) => this.openScreenSettings(event, screen)}
|
||||
onDragEnd={this.handleDragEnd}
|
||||
>
|
||||
<div className="front-icon">
|
||||
{this.renderTabIcon(screen)}
|
||||
</div>
|
||||
<CenteredIcon className="front-icon">{this.renderTabIcon(screen)}</CenteredIcon>
|
||||
<div className="tab-name truncate">
|
||||
{archived}
|
||||
{webShared}
|
||||
{screen.name.get()}
|
||||
</div>
|
||||
<div className="end-icon">
|
||||
<div className="end-icon-inner">
|
||||
<StatusIndicator level={statusIndicatorLevel}/>
|
||||
{tabIndex}
|
||||
{settings}
|
||||
</div>
|
||||
<div className="end-icons">
|
||||
<StatusIndicator level={statusIndicatorLevel} runningCommands={runningCommands} />
|
||||
{tabIndex}
|
||||
<ActionsIcon onClick={(e) => this.openScreenSettings(e, screen)} />
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
@import "../../../app/common/icons/icons.less";
|
||||
|
||||
#main .screen-tabs .screen-tab {
|
||||
border-top: 1px solid transparent;
|
||||
@ -27,10 +28,6 @@
|
||||
rgba(88, 193, 66, 0) 86.79%
|
||||
);
|
||||
}
|
||||
|
||||
.icon i {
|
||||
color: @tab-green;
|
||||
}
|
||||
}
|
||||
|
||||
&.color-orange {
|
||||
@ -242,14 +239,6 @@
|
||||
|
||||
.screen-tabs-container-inner {
|
||||
overflow-x: scroll;
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-tabs {
|
||||
@ -268,14 +257,7 @@
|
||||
padding: 0 8px 0 8px;
|
||||
|
||||
.front-icon {
|
||||
margin-right: 5px;
|
||||
.svg-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.fa-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.positional-icon-visible;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
@ -294,56 +276,27 @@
|
||||
}
|
||||
|
||||
// Only one of these will be visible at a time
|
||||
.end-icon {
|
||||
// This makes the calculations below easier since we don't need to account for the right margin on the parent tab.
|
||||
.end-icons {
|
||||
// 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;
|
||||
.end-icon-inner {
|
||||
& > 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%;
|
||||
}
|
||||
}
|
||||
line-height: normal;
|
||||
.tab-index {
|
||||
display: none;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tab-gear {
|
||||
display: block;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .screen-tab {
|
||||
.tab-index {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: none;
|
||||
}
|
||||
&:not(:hover) .status-indicator {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
|
||||
&:hover .screen-tab:hover .tab-index {
|
||||
display: none;
|
||||
&:hover {
|
||||
.screen-tab:not(:hover) .tab-index {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
.screen-tab:hover .actions {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,7 +310,6 @@
|
||||
height: 37px;
|
||||
|
||||
.icon {
|
||||
height: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
padding: 0.4em;
|
||||
|
@ -9,7 +9,6 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model";
|
||||
import { ReactComponent as AddIcon } from "../../assets/icons/add.svg";
|
||||
import * as constants from "../../appconst";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { ScreenTab } from "./tab";
|
||||
|
||||
@ -181,7 +180,7 @@ class ScreenTabs extends React.Component<
|
||||
return (
|
||||
<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. */}
|
||||
<div className="screen-tabs-container-inner">
|
||||
<div className="screen-tabs-container-inner hideScrollbarUntillHover">
|
||||
<Reorder.Group
|
||||
className="screen-tabs"
|
||||
ref={this.tabsRef}
|
||||
|
@ -12,6 +12,8 @@ import * as winston from "winston";
|
||||
import * as util from "util";
|
||||
import { sprintf } from "sprintf-js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { checkKeyPressed, adaptFromElectronKeyEvent, setKeyUtilPlatform } from "../util/keyutil";
|
||||
import { platform } from "os";
|
||||
|
||||
const WaveAppPathVarName = "WAVETERM_APP_PATH";
|
||||
const WaveDevVarName = "WAVETERM_DEV";
|
||||
@ -34,7 +36,7 @@ ensureDir(waveHome);
|
||||
// these are either "darwin/amd64" or "darwin/arm64"
|
||||
// normalize darwin/x64 to darwin/amd64 for GOARCH compatibility
|
||||
let unamePlatform = process.platform;
|
||||
let unameArch = process.arch;
|
||||
let unameArch: string = process.arch;
|
||||
if (unameArch == "x64") {
|
||||
unameArch = "amd64";
|
||||
}
|
||||
@ -187,15 +189,21 @@ let menuTemplate = [
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "File",
|
||||
submenu: [{ role: "close" },],
|
||||
},
|
||||
{
|
||||
role: "editMenu",
|
||||
},
|
||||
{
|
||||
role: "viewMenu",
|
||||
submenu: [
|
||||
{ role: "reload", accelerator: "Option+R" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "windowMenu",
|
||||
@ -244,6 +252,7 @@ function shFrameNavHandler(event: any, url: any) {
|
||||
|
||||
function createMainWindow(clientData) {
|
||||
let bounds = calcBounds(clientData);
|
||||
setKeyUtilPlatform(platform());
|
||||
let win = new electron.BrowserWindow({
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
@ -261,6 +270,7 @@ function createMainWindow(clientData) {
|
||||
let indexHtml = isDev ? "index-dev.html" : "index.html";
|
||||
win.loadFile(path.join(getAppBasePath(), "public", indexHtml));
|
||||
win.webContents.on("before-input-event", (e, input) => {
|
||||
let waveEvent = adaptFromElectronKeyEvent(input);
|
||||
if (win.isFocused()) {
|
||||
wasActive = true;
|
||||
}
|
||||
@ -268,50 +278,47 @@ function createMainWindow(clientData) {
|
||||
return;
|
||||
}
|
||||
let mods = getMods(input);
|
||||
if (input.code == "KeyT" && input.meta) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:t")) {
|
||||
win.webContents.send("t-cmd", mods);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (input.code == "KeyI" && input.meta) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:i")) {
|
||||
e.preventDefault();
|
||||
if (!input.alt) {
|
||||
win.webContents.send("i-cmd", mods);
|
||||
} else {
|
||||
win.webContents.toggleDevTools();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (input.code == "KeyR" && input.meta) {
|
||||
if (input.shift) {
|
||||
e.preventDefault();
|
||||
win.reload();
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Cmd:r")) {
|
||||
e.preventDefault();
|
||||
win.webContents.send("r-cmd", mods);
|
||||
return;
|
||||
}
|
||||
if (input.code == "KeyL" && input.meta) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:l")) {
|
||||
win.webContents.send("l-cmd", mods);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (input.code == "KeyW" && input.meta) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:w")) {
|
||||
e.preventDefault();
|
||||
win.webContents.send("w-cmd", mods);
|
||||
return;
|
||||
}
|
||||
if (input.code == "KeyH" && input.meta) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:h")) {
|
||||
win.webContents.send("h-cmd", mods);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (input.code == "KeyP" && input.meta) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:p")) {
|
||||
win.webContents.send("p-cmd", mods);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (input.meta && (input.code == "ArrowUp" || input.code == "ArrowDown")) {
|
||||
if (input.code == "ArrowUp") {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:ArrowUp") || checkKeyPressed(waveEvent, "Cmd:ArrowDown")) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:ArrowUp")) {
|
||||
win.webContents.send("meta-arrowup");
|
||||
} else {
|
||||
win.webContents.send("meta-arrowdown");
|
||||
@ -319,8 +326,8 @@ function createMainWindow(clientData) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (input.meta && (input.code == "PageUp" || input.code == "PageDown")) {
|
||||
if (input.code == "PageUp") {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:PageUp") || checkKeyPressed(waveEvent, "Cmd:PageDown")) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:PageUp")) {
|
||||
win.webContents.send("meta-pageup");
|
||||
} else {
|
||||
win.webContents.send("meta-pagedown");
|
||||
@ -336,8 +343,8 @@ function createMainWindow(clientData) {
|
||||
e.preventDefault();
|
||||
win.webContents.send("digit-cmd", { digit: digitNum }, mods);
|
||||
}
|
||||
if ((input.code == "BracketRight" || input.code == "BracketLeft") && input.meta) {
|
||||
let rel = input.code == "BracketRight" ? 1 : -1;
|
||||
if (checkKeyPressed(waveEvent, "Cmd:[") || checkKeyPressed(waveEvent, "Cmd:]")) {
|
||||
let rel = checkKeyPressed(waveEvent, "Cmd:]") ? 1 : -1;
|
||||
win.webContents.send("bracket-cmd", { relative: rel }, mods);
|
||||
e.preventDefault();
|
||||
return;
|
||||
|
@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld("api", {
|
||||
onHCmd: (callback) => ipcRenderer.on("h-cmd", callback),
|
||||
onWCmd: (callback) => ipcRenderer.on("w-cmd", callback),
|
||||
onPCmd: (callback) => ipcRenderer.on("p-cmd", callback),
|
||||
onRCmd: (callback) => ipcRenderer.on("r-cmd", callback),
|
||||
onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback),
|
||||
onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", 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 { modalsRegistry } from "../app/common/modals/registry";
|
||||
import * as appconst from "../app/appconst";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } from "../util/keyutil";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
var GlobalUser = "sawka";
|
||||
const RemotePtyRows = 8; // also in main.tsx
|
||||
const RemotePtyCols = 80;
|
||||
const ProdServerEndpoint = "http://127.0.0.1:1619";
|
||||
@ -203,6 +203,7 @@ type ElectronApi = {
|
||||
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||
onHCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||
onPCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||
onRCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||
onWCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||
onMenuItemAbout: (callback: () => void) => void;
|
||||
onMetaArrowUp: (callback: () => void) => void;
|
||||
@ -252,6 +253,10 @@ class Cmd {
|
||||
})();
|
||||
}
|
||||
|
||||
getRestartTs(): number {
|
||||
return this.data.get().restartts;
|
||||
}
|
||||
|
||||
getAsWebCmd(lineid: string): WebCmd {
|
||||
let cmd = this.data.get();
|
||||
let remote = GlobalModel.getRemote(this.remote.remoteid);
|
||||
@ -327,7 +332,6 @@ class Cmd {
|
||||
}
|
||||
|
||||
handleDataFromRenderer(data: string, renderer: RendererModel): void {
|
||||
// console.log("handle data", {data: data});
|
||||
if (!this.isRunning()) {
|
||||
return;
|
||||
}
|
||||
@ -372,6 +376,7 @@ class Screen {
|
||||
webShareOpts: OV<WebShareOpts>;
|
||||
filterRunning: OV<boolean>;
|
||||
statusIndicator: OV<StatusIndicatorLevel>;
|
||||
numRunningCmds: OV<number>;
|
||||
|
||||
constructor(sdata: ScreenDataType) {
|
||||
this.sessionId = sdata.sessionid;
|
||||
@ -415,6 +420,9 @@ class Screen {
|
||||
this.statusIndicator = mobx.observable.box(StatusIndicatorLevel.None, {
|
||||
name: "screen-status-indicator",
|
||||
});
|
||||
this.numRunningCmds = mobx.observable.box(0, {
|
||||
name: "screen-num-running-cmds",
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
@ -553,7 +561,6 @@ class Screen {
|
||||
mobx.action(() => {
|
||||
this.anchor.set({ anchorLine: anchorLine, anchorOffset: anchorOffset });
|
||||
})();
|
||||
// console.log("set-anchor-fields", anchorLine, anchorOffset, reason);
|
||||
}
|
||||
|
||||
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
|
||||
@ -566,7 +573,6 @@ class Screen {
|
||||
if (sdata.selectedline != 0) {
|
||||
sline = this.getLineByNum(sdata.selectedline);
|
||||
}
|
||||
// console.log("refocus", curLineFocus.linenum, "=>", sdata.selectedline, sline.lineid);
|
||||
if (
|
||||
curLineFocus.cmdInputFocus ||
|
||||
(curLineFocus.linenum != null && curLineFocus.linenum != sdata.selectedline)
|
||||
@ -794,7 +800,6 @@ class Screen {
|
||||
}
|
||||
|
||||
setLineFocus(lineNum: number, focus: boolean): void {
|
||||
// console.log("SW setLineFocus", lineNum, focus);
|
||||
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
|
||||
if (focus && this.selectedLine.get() != lineNum) {
|
||||
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
|
||||
@ -808,47 +813,64 @@ class Screen {
|
||||
* @param indicator The value of the status indicator. One of "none", "error", "success", "output".
|
||||
*/
|
||||
setStatusIndicator(indicator: StatusIndicatorLevel): void {
|
||||
mobx.action(() => {
|
||||
mobx.action(() => {
|
||||
this.statusIndicator.set(indicator);
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (e.code == "ArrowUp") {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||
termWrap.terminal.scrollLines(-1);
|
||||
return;
|
||||
}
|
||||
if (e.code == "ArrowDown") {
|
||||
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
termWrap.terminal.scrollLines(1);
|
||||
return;
|
||||
}
|
||||
if (e.code == "PageUp") {
|
||||
if (checkKeyPressed(waveEvent, "PageUp")) {
|
||||
termWrap.terminal.scrollPages(-1);
|
||||
return;
|
||||
}
|
||||
if (e.code == "PageDown") {
|
||||
if (checkKeyPressed(waveEvent, "PageDown")) {
|
||||
termWrap.terminal.scrollPages(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isTermCapturedKey(e: any): boolean {
|
||||
let keys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown"];
|
||||
if (keys.includes(e.code) && keyHasNoMods(e)) {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (
|
||||
checkKeyPressed(waveEvent, "ArrowUp") ||
|
||||
checkKeyPressed(waveEvent, "ArrowDown") ||
|
||||
checkKeyPressed(waveEvent, "PageUp") ||
|
||||
checkKeyPressed(waveEvent, "PageDown")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
|
||||
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.preventDefault();
|
||||
let sel = termWrap.terminal.getSelection();
|
||||
navigator.clipboard.writeText(sel);
|
||||
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.preventDefault();
|
||||
let p = navigator.clipboard.readText();
|
||||
@ -880,7 +902,6 @@ class Screen {
|
||||
console.log("term-wrap already exists for", this.screenId, lineId);
|
||||
return;
|
||||
}
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
||||
if (line.contentheight != null && line.contentheight != -1) {
|
||||
usedRows = line.contentheight;
|
||||
@ -910,7 +931,6 @@ class Screen {
|
||||
if (this.focusType.get() == "cmd" && this.selectedLine.get() == line.linenum) {
|
||||
termWrap.giveFocus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
unloadRenderer(lineId: string) {
|
||||
@ -936,7 +956,6 @@ class Screen {
|
||||
}
|
||||
let termWrap = this.getTermWrap(cmd.lineId);
|
||||
if (termWrap == null) {
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getContentHeight(context);
|
||||
if (usedRows != null) {
|
||||
return usedRows;
|
||||
@ -1007,8 +1026,7 @@ class ScreenLines {
|
||||
|
||||
getNonArchivedLines(): LineType[] {
|
||||
let rtn: LineType[] = [];
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
let line = this.lines[i];
|
||||
for (const line of this.lines) {
|
||||
if (line.archived) {
|
||||
continue;
|
||||
}
|
||||
@ -1029,8 +1047,8 @@ class ScreenLines {
|
||||
(l: LineType) => sprintf("%013d:%s", l.ts, l.lineid)
|
||||
);
|
||||
let cmds = slines.cmds || [];
|
||||
for (let i = 0; i < cmds.length; i++) {
|
||||
this.cmds[cmds[i].lineid] = new Cmd(cmds[i]);
|
||||
for (const cmd of cmds) {
|
||||
this.cmds[cmd.lineid] = new Cmd(cmd);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@ -1048,22 +1066,37 @@ class ScreenLines {
|
||||
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[] = [];
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
let line = this.lines[i];
|
||||
let cmd = this.getCmd(line.lineid);
|
||||
for (const line of this.lines) {
|
||||
const cmd = this.getCmd(line.lineid);
|
||||
if (cmd == null) {
|
||||
continue;
|
||||
}
|
||||
let status = cmd.getStatus();
|
||||
const status = cmd.getStatus();
|
||||
if (cmdStatusIsRunning(status)) {
|
||||
if (returnFirst) {
|
||||
return [line];
|
||||
}
|
||||
rtn.push(line);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if (cmd.remove) {
|
||||
throw new Error("cannot remove cmd with updateCmd call [" + cmd.lineid + "]");
|
||||
@ -1072,7 +1105,6 @@ class ScreenLines {
|
||||
if (origCmd != null) {
|
||||
origCmd.setCmd(cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mergeCmd(cmd: CmdDataType): void {
|
||||
@ -1086,7 +1118,6 @@ class ScreenLines {
|
||||
return;
|
||||
}
|
||||
origCmd.setCmd(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
addLineCmd(line: LineType, cmd: CmdDataType, interactive: boolean) {
|
||||
@ -1315,10 +1346,8 @@ class InputModel {
|
||||
if (isFocused) {
|
||||
this.inputFocused.set(true);
|
||||
this.lineFocused.set(false);
|
||||
} else {
|
||||
if (this.inputFocused.get()) {
|
||||
this.inputFocused.set(false);
|
||||
}
|
||||
} else if (this.inputFocused.get()) {
|
||||
this.inputFocused.set(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@ -1328,10 +1357,8 @@ class InputModel {
|
||||
if (isFocused) {
|
||||
this.inputFocused.set(false);
|
||||
this.lineFocused.set(true);
|
||||
} else {
|
||||
if (this.lineFocused.get()) {
|
||||
this.lineFocused.set(false);
|
||||
}
|
||||
} else if (this.lineFocused.get()) {
|
||||
this.lineFocused.set(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@ -1564,34 +1591,31 @@ class InputModel {
|
||||
curRemote = { ownerid: "", name: "", remoteid: "" };
|
||||
}
|
||||
curRemote = mobx.toJS(curRemote);
|
||||
for (let i = 0; i < hitems.length; i++) {
|
||||
let hitem = hitems[i];
|
||||
for (const hitem of hitems) {
|
||||
if (hitem.ismetacmd) {
|
||||
if (!opts.includeMeta) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (opts.limitRemoteInstance) {
|
||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(curRemote.ownerid ?? "") != (hitem.remote.ownerid ?? "") ||
|
||||
(curRemote.remoteid ?? "") != (hitem.remote.remoteid ?? "") ||
|
||||
(curRemote.name ?? "") != (hitem.remote.name ?? "")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (opts.limitRemote) {
|
||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(curRemote.ownerid ?? "") != (hitem.remote.ownerid ?? "") ||
|
||||
(curRemote.remoteid ?? "") != (hitem.remote.remoteid ?? "")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (opts.limitRemoteInstance) {
|
||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(curRemote.ownerid ?? "") != (hitem.remote.ownerid ?? "") ||
|
||||
(curRemote.remoteid ?? "") != (hitem.remote.remoteid ?? "") ||
|
||||
(curRemote.name ?? "") != (hitem.remote.name ?? "")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (opts.limitRemote) {
|
||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(curRemote.ownerid ?? "") != (hitem.remote.ownerid ?? "") ||
|
||||
(curRemote.remoteid ?? "") != (hitem.remote.remoteid ?? "")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!isBlank(opts.queryStr)) {
|
||||
@ -1642,7 +1666,6 @@ class InputModel {
|
||||
return;
|
||||
}
|
||||
historyDiv.scrollTop = elemOffset - titleHeight - buffer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1728,7 +1751,7 @@ class InputModel {
|
||||
}
|
||||
|
||||
setAIChatFocus() {
|
||||
if (this.aiChatTextAreaRef != null && this.aiChatTextAreaRef.current != null) {
|
||||
if (this.aiChatTextAreaRef?.current != null) {
|
||||
this.aiChatTextAreaRef.current.focus();
|
||||
}
|
||||
}
|
||||
@ -1759,7 +1782,7 @@ class InputModel {
|
||||
this.codeSelectSelectedIndex.set(blockIndex);
|
||||
let currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
||||
if (currentRef != null) {
|
||||
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
||||
if (this.aiChatWindowRef?.current != null) {
|
||||
let chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
||||
let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
||||
let elemTop = currentRef.offsetTop;
|
||||
@ -1789,7 +1812,7 @@ class InputModel {
|
||||
let incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
|
||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
|
||||
this.codeSelectDeselectAll();
|
||||
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
||||
if (this.aiChatWindowRef?.current != null) {
|
||||
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
||||
}
|
||||
}
|
||||
@ -1813,7 +1836,7 @@ class InputModel {
|
||||
let decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
||||
if (decBlockIndex < 0) {
|
||||
this.codeSelectDeselectAll(this.codeSelectTop);
|
||||
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
||||
if (this.aiChatWindowRef?.current != null) {
|
||||
this.aiChatWindowRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
@ -1855,8 +1878,7 @@ class InputModel {
|
||||
clearAIAssistantChat(): void {
|
||||
let prtn = GlobalModel.submitChatInfoCommand("", "", true);
|
||||
prtn.then((rtn) => {
|
||||
if (rtn.success) {
|
||||
} else {
|
||||
if (!rtn.success) {
|
||||
console.log("submit chat command error: " + rtn.error);
|
||||
}
|
||||
}).catch((error) => {
|
||||
@ -1979,7 +2001,6 @@ class InputModel {
|
||||
}
|
||||
|
||||
getCurLine(): string {
|
||||
let model = GlobalModel;
|
||||
let hidx = this.historyIndex.get();
|
||||
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
|
||||
return this.modHistory[hidx];
|
||||
@ -2190,7 +2211,6 @@ class SpecialLineContainer {
|
||||
console.log("term-wrap already exists for", line.screenid, lineId);
|
||||
return;
|
||||
}
|
||||
let cols = windowWidthToCols(width, GlobalModel.termFontSize.get());
|
||||
let usedRows = GlobalModel.getContentHeight(getRendererContext(line));
|
||||
if (line.contentheight != null && line.contentheight != -1) {
|
||||
usedRows = line.contentheight;
|
||||
@ -2214,7 +2234,6 @@ class SpecialLineContainer {
|
||||
onUpdateContentHeight: null,
|
||||
});
|
||||
this.terminal = termWrap;
|
||||
return;
|
||||
}
|
||||
|
||||
registerRenderer(lineId: string, renderer: RendererModel): void {
|
||||
@ -2324,8 +2343,6 @@ class HistoryViewModel {
|
||||
|
||||
specialLineContainer: SpecialLineContainer;
|
||||
|
||||
constructor() {}
|
||||
|
||||
closeView(): void {
|
||||
GlobalModel.showSessionView();
|
||||
setTimeout(() => GlobalModel.inputModel.giveFocus(), 50);
|
||||
@ -2335,8 +2352,7 @@ class HistoryViewModel {
|
||||
if (isBlank(lineId)) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.historyItemLines.length; i++) {
|
||||
let line = this.historyItemLines[i];
|
||||
for (const line of this.historyItemLines) {
|
||||
if (line.lineid == lineId) {
|
||||
return line;
|
||||
}
|
||||
@ -2348,8 +2364,7 @@ class HistoryViewModel {
|
||||
if (isBlank(lineId)) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.historyItemCmds.length; i++) {
|
||||
let cmd = this.historyItemCmds[i];
|
||||
for (const cmd of this.historyItemCmds) {
|
||||
if (cmd.lineid == lineId) {
|
||||
return new Cmd(cmd);
|
||||
}
|
||||
@ -2361,8 +2376,7 @@ class HistoryViewModel {
|
||||
if (isBlank(historyId)) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
let hitem = this.items[i];
|
||||
for (const hitem of this.items) {
|
||||
if (hitem.historyid == historyId) {
|
||||
return hitem;
|
||||
}
|
||||
@ -2421,7 +2435,6 @@ class HistoryViewModel {
|
||||
prtn.then((result: CommandRtnType) => {
|
||||
if (!result.success) {
|
||||
GlobalModel.showAlert({ message: "Error removing history lines." });
|
||||
return;
|
||||
}
|
||||
});
|
||||
let params = this._getSearchParams();
|
||||
@ -2436,8 +2449,8 @@ class HistoryViewModel {
|
||||
}
|
||||
|
||||
_getSearchParams(newOffset?: number, newRawOffset?: number): HistorySearchParams {
|
||||
let offset = newOffset != null ? newOffset : this.offset.get();
|
||||
let rawOffset = newRawOffset != null ? newRawOffset : this.curRawOffset;
|
||||
let offset = newOffset ?? this.offset.get();
|
||||
let rawOffset = newRawOffset ?? this.curRawOffset;
|
||||
let opts: HistorySearchParams = {
|
||||
offset: offset,
|
||||
rawOffset: rawOffset,
|
||||
@ -2559,7 +2572,8 @@ class HistoryViewModel {
|
||||
}
|
||||
|
||||
handleDocKeyDown(e: any): void {
|
||||
if (e.code == "Escape") {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
this.closeView();
|
||||
return;
|
||||
@ -2731,8 +2745,7 @@ class BookmarksModel {
|
||||
if (bookmarkId == null) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.bookmarks.length; i++) {
|
||||
let bm = this.bookmarks[i];
|
||||
for (const bm of this.bookmarks) {
|
||||
if (bm.bookmarkid == bookmarkId) {
|
||||
return bm;
|
||||
}
|
||||
@ -2800,7 +2813,8 @@ class BookmarksModel {
|
||||
}
|
||||
|
||||
handleDocKeyDown(e: any): void {
|
||||
if (e.code == "Escape") {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
if (this.editingBookmark.get() != null) {
|
||||
this.cancelEdit();
|
||||
@ -2812,7 +2826,7 @@ class BookmarksModel {
|
||||
if (this.editingBookmark.get() != null) {
|
||||
return;
|
||||
}
|
||||
if (e.code == "Backspace" || e.code == "Delete") {
|
||||
if (checkKeyPressed(waveEvent, "Backspace") || checkKeyPressed(waveEvent, "Delete")) {
|
||||
if (this.activeBookmark.get() == null) {
|
||||
return;
|
||||
}
|
||||
@ -2820,7 +2834,13 @@ class BookmarksModel {
|
||||
this.handleDeleteBookmark(this.activeBookmark.get());
|
||||
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();
|
||||
if (this.bookmarks.length == 0) {
|
||||
return;
|
||||
@ -2844,14 +2864,14 @@ class BookmarksModel {
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (e.code == "Enter") {
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
if (this.activeBookmark.get() == null) {
|
||||
return;
|
||||
}
|
||||
this.useBookmark(this.activeBookmark.get());
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyE") {
|
||||
if (checkKeyPressed(waveEvent, "e")) {
|
||||
if (this.activeBookmark.get() == null) {
|
||||
return;
|
||||
}
|
||||
@ -2859,13 +2879,12 @@ class BookmarksModel {
|
||||
this.handleEditBookmark(this.activeBookmark.get());
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyC") {
|
||||
if (checkKeyPressed(waveEvent, "c")) {
|
||||
if (this.activeBookmark.get() == null) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
this.handleCopyBookmark(this.activeBookmark.get());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3401,6 +3420,7 @@ class Model {
|
||||
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
|
||||
name: "model-wavesrv-running",
|
||||
});
|
||||
this.platform = this.getPlatform();
|
||||
this.termFontSize = mobx.computed(() => {
|
||||
let cdata = this.clientData.get();
|
||||
if (cdata == null || cdata.feopts == null || cdata.feopts.termfontsize == null) {
|
||||
@ -3421,6 +3441,7 @@ class Model {
|
||||
getApi().onHCmd(this.onHCmd.bind(this));
|
||||
getApi().onPCmd(this.onPCmd.bind(this));
|
||||
getApi().onWCmd(this.onWCmd.bind(this));
|
||||
getApi().onRCmd(this.onRCmd.bind(this));
|
||||
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
|
||||
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
|
||||
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
|
||||
@ -3444,9 +3465,14 @@ class Model {
|
||||
return this.platform;
|
||||
}
|
||||
this.platform = getApi().getPlatform();
|
||||
setKeyUtilPlatform(this.platform);
|
||||
return this.platform;
|
||||
}
|
||||
|
||||
testGlobalModel() {
|
||||
return "";
|
||||
}
|
||||
|
||||
needsTos(): boolean {
|
||||
let cdata = this.clientData.get();
|
||||
if (cdata == null) {
|
||||
@ -3588,16 +3614,17 @@ class Model {
|
||||
}
|
||||
|
||||
docKeyDownHandler(e: KeyboardEvent) {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (isModKeyPress(e)) {
|
||||
return;
|
||||
}
|
||||
if (this.alertMessage.get() != null) {
|
||||
if (e.code == "Escape") {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
this.cancelAlert();
|
||||
return;
|
||||
}
|
||||
if (e.code == "Enter") {
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
this.confirmAlert();
|
||||
return;
|
||||
@ -3620,7 +3647,7 @@ class Model {
|
||||
this.historyViewModel.handleDocKeyDown(e);
|
||||
return;
|
||||
}
|
||||
if (e.code == "Escape") {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
if (this.activeMainView.get() == "webshare") {
|
||||
this.showSessionView();
|
||||
@ -3636,16 +3663,11 @@ class Model {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.code == "KeyB" && e.getModifierState("Meta")) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:b")) {
|
||||
e.preventDefault();
|
||||
GlobalCommandRunner.bookmarksView();
|
||||
}
|
||||
if (
|
||||
this.activeMainView.get() == "session" &&
|
||||
e.code == "KeyS" &&
|
||||
e.getModifierState("Meta") &&
|
||||
e.getModifierState("Control")
|
||||
) {
|
||||
if (this.activeMainView.get() == "session" && checkKeyPressed(waveEvent, "Cmd:Ctrl:s")) {
|
||||
e.preventDefault();
|
||||
let activeScreen = this.getActiveScreen();
|
||||
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();
|
||||
if (ranDelete) {
|
||||
e.preventDefault();
|
||||
@ -3691,6 +3713,9 @@ class Model {
|
||||
}
|
||||
|
||||
onWCmd(e: any, mods: KeyModsType) {
|
||||
if (this.activeMainView.get() != "session") {
|
||||
return;
|
||||
}
|
||||
let activeScreen = this.getActiveScreen();
|
||||
if (activeScreen == null) {
|
||||
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 {
|
||||
let didSomething = false;
|
||||
mobx.action(() => {
|
||||
@ -3739,9 +3785,9 @@ class Model {
|
||||
}
|
||||
|
||||
getLocalRemote(): RemoteType {
|
||||
for (let i = 0; i < this.remotes.length; i++) {
|
||||
if (this.remotes[i].local) {
|
||||
return this.remotes[i];
|
||||
for (const remote of this.remotes) {
|
||||
if (remote.local) {
|
||||
return remote;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -3851,7 +3897,6 @@ class Model {
|
||||
let wasRunning = cmdStatusIsRunning(origStatus);
|
||||
let isRunning = cmdStatusIsRunning(newStatus);
|
||||
if (wasRunning && !isRunning) {
|
||||
// console.log("cmd status", screenId, lineId, origStatus, "=>", newStatus);
|
||||
let ptr = this.getActiveLine(screenId, lineId);
|
||||
if (ptr != null) {
|
||||
let screen = ptr.screen;
|
||||
@ -3994,8 +4039,8 @@ class Model {
|
||||
this.updateCmd(update.cmd);
|
||||
}
|
||||
if ("lines" in update) {
|
||||
for (let i = 0; i < update.lines.length; i++) {
|
||||
this.addLineCmd(update.lines[i], null, interactive);
|
||||
for (const line of update.lines) {
|
||||
this.addLineCmd(line, null, interactive);
|
||||
}
|
||||
}
|
||||
if ("screenlines" in update) {
|
||||
@ -4007,8 +4052,8 @@ class Model {
|
||||
}
|
||||
this.updateRemotes(update.remotes);
|
||||
// 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()) {
|
||||
GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid);
|
||||
if (update.remotes?.length && this.remotesModel.recentConnAddedState.get()) {
|
||||
GlobalModel.remotesModel.openReadModal(update.remotes[0].remoteid);
|
||||
}
|
||||
}
|
||||
if ("mainview" in update) {
|
||||
@ -4059,22 +4104,28 @@ class Model {
|
||||
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
|
||||
}
|
||||
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) {
|
||||
let userInputRequest: UserInputRequest = update.userinputrequest;
|
||||
let userInputResponse: UserInputResponse = {
|
||||
type: "text",
|
||||
text: "what wonderful weather we're having",
|
||||
}
|
||||
};
|
||||
let userInputResponsePacket: UserInputResponsePacket = {
|
||||
type: "userinputresp",
|
||||
requestid: userInputRequest.requestid,
|
||||
response: userInputResponse,
|
||||
}
|
||||
};
|
||||
this.ws.pushMessage(userInputResponsePacket);
|
||||
}
|
||||
// console.log("run-update>", Date.now(), interactive, update);
|
||||
}
|
||||
|
||||
updateRemotes(remotes: RemoteType[]): void {
|
||||
@ -4087,8 +4138,7 @@ class Model {
|
||||
|
||||
getSessionNames(): Record<string, string> {
|
||||
let rtn: Record<string, string> = {};
|
||||
for (let i = 0; i < this.sessionList.length; i++) {
|
||||
let session = this.sessionList[i];
|
||||
for (const session of this.sessionList) {
|
||||
rtn[session.sessionId] = session.name.get();
|
||||
}
|
||||
return rtn;
|
||||
@ -4106,9 +4156,9 @@ class Model {
|
||||
if (sessionId == null) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.sessionList.length; i++) {
|
||||
if (this.sessionList[i].sessionId == sessionId) {
|
||||
return this.sessionList[i];
|
||||
for (const session of this.sessionList) {
|
||||
if (session.sessionId == sessionId) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -4135,7 +4185,6 @@ class Model {
|
||||
let newWindow = new ScreenLines(slines.screenid);
|
||||
this.screenLines.set(slines.screenid, newWindow);
|
||||
newWindow.updateData(slines, load);
|
||||
return;
|
||||
} else {
|
||||
existingWin.updateData(slines, load);
|
||||
existingWin.loaded.set(true);
|
||||
@ -4183,12 +4232,28 @@ class Model {
|
||||
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) {
|
||||
let slines = this.getScreenLinesById(line.screenid);
|
||||
if (slines == null) {
|
||||
return;
|
||||
}
|
||||
slines.addLineCmd(line, cmd, interactive);
|
||||
this.handleCmdRestart(cmd);
|
||||
}
|
||||
|
||||
updateCmd(cmd: CmdDataType) {
|
||||
@ -4196,6 +4261,7 @@ class Model {
|
||||
if (slines != null) {
|
||||
slines.updateCmd(cmd);
|
||||
}
|
||||
this.handleCmdRestart(cmd);
|
||||
}
|
||||
|
||||
isInfoUpdate(update: UpdateMessage): boolean {
|
||||
@ -4292,7 +4358,7 @@ class Model {
|
||||
metacmd: metaCmd,
|
||||
metasubcmd: metaSubCmd,
|
||||
args: args,
|
||||
kwargs: Object.assign({}, kwargs),
|
||||
kwargs: { ...kwargs },
|
||||
uicontext: this.getUIContext(),
|
||||
interactive: interactive,
|
||||
};
|
||||
@ -4367,7 +4433,6 @@ class Model {
|
||||
}
|
||||
let slines: ScreenLinesType = data.data;
|
||||
this.updateScreenLines(slines, true);
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.errorHandler(sprintf("getting screen-lines=%s", newWin.screenId), err, false);
|
||||
@ -4389,8 +4454,7 @@ class Model {
|
||||
|
||||
getRemoteNames(): Record<string, string> {
|
||||
let rtn: Record<string, string> = {};
|
||||
for (let i = 0; i < this.remotes.length; i++) {
|
||||
let remote = this.remotes[i];
|
||||
for (const remote of this.remotes) {
|
||||
if (!isBlank(remote.remotealias)) {
|
||||
rtn[remote.remoteid] = remote.remotealias;
|
||||
} else {
|
||||
@ -4401,9 +4465,9 @@ class Model {
|
||||
}
|
||||
|
||||
getRemoteByName(name: string): RemoteType {
|
||||
for (let i = 0; i < this.remotes.length; i++) {
|
||||
if (this.remotes[i].remotecanonicalname == name || this.remotes[i].remotealias == name) {
|
||||
return this.remotes[i];
|
||||
for (const remote of this.remotes) {
|
||||
if (remote.remotecanonicalname == name || remote.remotealias == name) {
|
||||
return remote;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -4434,9 +4498,9 @@ class Model {
|
||||
return null;
|
||||
}
|
||||
let line: LineType = null;
|
||||
for (let i = 0; i < slines.lines.length; i++) {
|
||||
if (slines.lines[i].lineid == lineid) {
|
||||
line = slines.lines[i];
|
||||
for (const element of slines.lines) {
|
||||
if (element.lineid == lineid) {
|
||||
line = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -4458,7 +4522,7 @@ class Model {
|
||||
console.log("[error]", str, err);
|
||||
if (interactive) {
|
||||
let errMsg = "error running command";
|
||||
if (err != null && err.message) {
|
||||
if (err?.message) {
|
||||
errMsg = err.message;
|
||||
}
|
||||
this.inputModel.flashInfoMsg({ infoerror: errMsg }, null);
|
||||
@ -4515,13 +4579,10 @@ class Model {
|
||||
let url = new URL(GlobalModel.getBaseHostPort() + "/api/read-file?" + usp.toString());
|
||||
let fetchHeaders = this.getFetchHeaders();
|
||||
let fileInfo: T.FileInfoType = null;
|
||||
let contentType: string = null;
|
||||
let isError = false;
|
||||
let badResponseStr: string = null;
|
||||
let prtn = fetch(url, { method: "get", headers: fetchHeaders })
|
||||
.then((resp) => {
|
||||
if (!resp.ok) {
|
||||
isError = true;
|
||||
badResponseStr = sprintf(
|
||||
"Bad fetch response for /api/read-file: %d %s",
|
||||
resp.status,
|
||||
@ -4529,7 +4590,6 @@ class Model {
|
||||
);
|
||||
return resp.text() as any;
|
||||
}
|
||||
contentType = resp.headers.get("Content-Type");
|
||||
fileInfo = JSON.parse(base64ToString(resp.headers.get("X-FileInfo")));
|
||||
return resp.blob();
|
||||
})
|
||||
@ -4547,7 +4607,6 @@ class Model {
|
||||
throw new Error(badResponseStr);
|
||||
}
|
||||
throw new Error(textError);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return prtn;
|
||||
@ -4576,15 +4635,13 @@ class Model {
|
||||
let prtn = fetch(url, { method: "post", headers: fetchHeaders, body: formData });
|
||||
return prtn
|
||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||
.then((data) => {
|
||||
.then((_) => {
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CommandRunner {
|
||||
constructor() {}
|
||||
|
||||
loadHistory(show: boolean, htype: string) {
|
||||
let kwargs = { nohist: "1" };
|
||||
if (!show) {
|
||||
@ -4642,6 +4699,10 @@ class CommandRunner {
|
||||
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> {
|
||||
let kwargs = { nohist: "1" };
|
||||
if ("renderer" in opts) {
|
||||
|
@ -10,6 +10,7 @@ import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
||||
import Split from "react-split-it";
|
||||
import loader from "@monaco-editor/loader";
|
||||
loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } });
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||
|
||||
import "./code.less";
|
||||
|
||||
@ -152,17 +153,18 @@ class SourceCodeRenderer extends React.Component<
|
||||
this.setInitialLanguage(editor);
|
||||
this.setEditorHeight();
|
||||
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.stopPropagation();
|
||||
this.doSave();
|
||||
}
|
||||
if (e.code === "KeyD" && e.metaKey) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:d")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.doClose();
|
||||
}
|
||||
if (e.code === "KeyP" && e.metaKey) {
|
||||
if (checkKeyPressed(waveEvent, "Cmd:p")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.togglePreview();
|
||||
|
@ -245,12 +245,14 @@ type CmdDataType = {
|
||||
status: string;
|
||||
cmdpid: number;
|
||||
remotepid: number;
|
||||
restartts: number;
|
||||
donets: number;
|
||||
exitcode: number;
|
||||
durationms: number;
|
||||
runout: any[];
|
||||
rtnstate: boolean;
|
||||
remove?: boolean;
|
||||
restarted?: boolean;
|
||||
};
|
||||
|
||||
type PtyDataUpdateType = {
|
||||
@ -295,7 +297,12 @@ enum StatusIndicatorLevel {
|
||||
type ScreenStatusIndicatorUpdateType = {
|
||||
screenid: string;
|
||||
status: StatusIndicatorLevel;
|
||||
}
|
||||
};
|
||||
|
||||
type ScreenNumRunningCommandsUpdateType = {
|
||||
screenid: string;
|
||||
num: number;
|
||||
};
|
||||
|
||||
type ModelUpdateType = {
|
||||
interactive: boolean;
|
||||
@ -320,6 +327,7 @@ type ModelUpdateType = {
|
||||
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
|
||||
alertmessage?: AlertMessageType;
|
||||
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
|
||||
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
|
||||
userinputrequest?: UserInputRequest;
|
||||
};
|
||||
|
||||
@ -823,6 +831,4 @@ export type {
|
||||
ScreenStatusIndicatorUpdateType,
|
||||
};
|
||||
|
||||
export {
|
||||
StatusIndicatorLevel,
|
||||
};
|
||||
export { 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;
|
||||
|
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:view", LineViewCommand)
|
||||
registerCmdFn("line:set", LineSetCommand)
|
||||
registerCmdFn("line:restart", LineRestartCommand)
|
||||
|
||||
registerCmdFn("client", ClientCommand)
|
||||
registerCmdFn("client:show", ClientShowCommand)
|
||||
@ -491,7 +492,12 @@ func SyncCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
|
||||
}
|
||||
runPacket.Command = ":"
|
||||
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 {
|
||||
defer callback()
|
||||
}
|
||||
@ -587,7 +593,12 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U
|
||||
}
|
||||
runPacket.Command = strings.TrimSpace(cmdStr)
|
||||
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 {
|
||||
defer callback()
|
||||
}
|
||||
@ -2974,6 +2985,7 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
update := &sstore.ModelUpdate{
|
||||
ActiveSessionId: ritem.Id,
|
||||
Info: &sstore.InfoMsgType{
|
||||
@ -2981,6 +2993,21 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
|
||||
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
|
||||
}
|
||||
|
||||
@ -3320,6 +3347,121 @@ func LineSetHeightCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
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) {
|
||||
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
||||
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-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 {
|
||||
doneTs := time.UnixMilli(cmd.DoneTs)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
NumTryConnect int
|
||||
InitPkShellType string
|
||||
DataPosMap *utilfn.SyncMap[base.CommandKey, int64]
|
||||
|
||||
// install
|
||||
InstallStatus string
|
||||
@ -169,7 +170,6 @@ type MShellProc struct {
|
||||
InstallErr error
|
||||
|
||||
RunningCmds map[base.CommandKey]RunCmdType
|
||||
WaitingCmds []RunCmdType
|
||||
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
|
||||
}
|
||||
@ -209,6 +209,18 @@ func (msh *MShellProc) GetDefaultState(shellType string) *packet.ShellState {
|
||||
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 {
|
||||
msh.Lock.Lock()
|
||||
defer msh.Lock.Unlock()
|
||||
@ -692,6 +704,7 @@ func MakeMShell(r *sstore.RemoteType) *MShellProc {
|
||||
PendingStateCmds: make(map[pendingStateKey]base.CommandKey),
|
||||
StateMap: server.MakeShellStateMap(),
|
||||
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
|
||||
// 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 {
|
||||
msh.Lock.Lock()
|
||||
defer msh.Lock.Unlock()
|
||||
for runningCk := range msh.RunningCmds {
|
||||
if runningCk == ck {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
_, ok := msh.RunningCmds[ck]
|
||||
return ok
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if !msh.IsConnected() {
|
||||
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)
|
||||
func RunCommand(ctx context.Context, sessionId string, screenId string, remotePtr sstore.RemotePtrType, runPacket *packet.RunPacketType) (rtnCmd *sstore.CmdType, rtnCallback func(), rtnErr error) {
|
||||
rct := RunCmdType{
|
||||
SessionId: sessionId,
|
||||
ScreenId: screenId,
|
||||
RemotePtr: remotePtr,
|
||||
RunPacket: runPacket,
|
||||
}
|
||||
type RunCommandOpts struct {
|
||||
SessionId string
|
||||
ScreenId string
|
||||
RemotePtr sstore.RemotePtrType
|
||||
|
||||
// optional, if not provided shellstate will look up state from remote instance
|
||||
// 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 != "" {
|
||||
return nil, nil, fmt.Errorf("cannot run command against another user's remote '%s'", remotePtr.MakeFullRemoteRef())
|
||||
}
|
||||
@ -1706,56 +1750,85 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt
|
||||
if runPacket.State != nil {
|
||||
return nil, nil, fmt.Errorf("runPacket.State should not be set, it is set in RunCommand")
|
||||
}
|
||||
var newPSC *base.CommandKey
|
||||
if runPacket.ReturnState {
|
||||
newPSC = &runPacket.CK
|
||||
if rcOpts.StatePtr != nil && runPacket.ReturnState {
|
||||
return nil, nil, fmt.Errorf("RunCommand: cannot use ReturnState with StatePtr")
|
||||
}
|
||||
ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC)
|
||||
if !ok {
|
||||
line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err)
|
||||
|
||||
// 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
|
||||
if runPacket.ReturnState {
|
||||
newPSC = &runPacket.CK
|
||||
}
|
||||
if line == nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC)
|
||||
}
|
||||
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 {
|
||||
msh.removePendingStateCmd(screenId, remotePtr, *newPSC)
|
||||
ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC)
|
||||
if !ok {
|
||||
line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running: %v", err)
|
||||
}
|
||||
if line == nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command is still running %s", *existingPSC)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("cannot run command while a stateful command (linenum=%d) is still running", line.LineNum)
|
||||
}
|
||||
}()
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// get current remote-instance state
|
||||
statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
|
||||
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 {
|
||||
return nil, nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
|
||||
}
|
||||
}
|
||||
if statePtr == nil {
|
||||
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 {
|
||||
return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr")
|
||||
if statePtr == nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command, no valid connection stateptr")
|
||||
}
|
||||
}
|
||||
currentState, err := sstore.GetFullState(ctx, *statePtr)
|
||||
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.StateComplete = true
|
||||
runPacket.ShellType = currentState.GetShellType()
|
||||
// check to see if shellType is initialized
|
||||
if !msh.StateMap.HasShell(runPacket.ShellType) {
|
||||
// try to reinit the shell
|
||||
_, err := msh.ReInit(ctx, runPacket.ShellType)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error trying to initialize shell %q: %v", runPacket.ShellType, err)
|
||||
}
|
||||
err = msh.EnsureShellType(ctx, runPacket.ShellType) // make sure shellType is initialized
|
||||
if err != nil {
|
||||
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)
|
||||
err = shexec.SendRunPacketAndRunData(ctx, msh.ServerProc.Input, runPacket)
|
||||
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))
|
||||
}
|
||||
|
||||
// command is now successfully runnning
|
||||
status := sstore.CmdStatusRunning
|
||||
if runPacket.Detached {
|
||||
status = sstore.CmdStatusDetached
|
||||
@ -1797,44 +1872,22 @@ func RunCommand(ctx context.Context, sessionId string, screenId string, remotePt
|
||||
RunOut: nil,
|
||||
RtnState: runPacket.ReturnState,
|
||||
}
|
||||
err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
||||
if err != nil {
|
||||
// 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)
|
||||
}
|
||||
msh.AddRunningCmd(rct)
|
||||
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 !rcOpts.NoCreateCmdPtyFile {
|
||||
err = sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
||||
if err != nil {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
if len(msh.WaitingCmds) == 0 {
|
||||
msh.WaitingCmds = nil
|
||||
}
|
||||
msh.AddRunningCmd(RunCmdType{
|
||||
SessionId: sessionId,
|
||||
ScreenId: screenId,
|
||||
RemotePtr: remotePtr,
|
||||
RunPacket: runPacket,
|
||||
})
|
||||
|
||||
go pushNumRunningCmdsUpdate(&runPacket.CK, 1)
|
||||
return cmd, func() { removeCmdWait(runPacket.CK) }, nil
|
||||
}
|
||||
|
||||
func (msh *MShellProc) AddRunningCmd(rct RunCmdType) {
|
||||
@ -1937,10 +1990,10 @@ func (msh *MShellProc) notifyHangups_nolock() {
|
||||
}
|
||||
update := &sstore.ModelUpdate{Cmd: cmd}
|
||||
sstore.MainBus.SendScreenUpdate(ck.GetGroupId(), update)
|
||||
go pushNumRunningCmdsUpdate(&ck, -1)
|
||||
}
|
||||
msh.RunningCmds = make(map[base.CommandKey]RunCmdType)
|
||||
msh.PendingStateCmds = make(map[pendingStateKey]base.CommandKey)
|
||||
msh.WaitingCmds = nil
|
||||
}
|
||||
|
||||
func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
||||
@ -2011,6 +2064,8 @@ func (msh *MShellProc) handleCmdDonePacket(donePk *packet.CmdDonePacketType) {
|
||||
// fall-through (nothing to do)
|
||||
}
|
||||
}
|
||||
|
||||
go pushNumRunningCmdsUpdate(&donePk.CK, -1)
|
||||
sstore.MainBus.SendUpdate(update)
|
||||
return
|
||||
}
|
||||
@ -2044,6 +2099,7 @@ func (msh *MShellProc) handleCmdFinalPacket(finalPk *packet.CmdFinalPacketType)
|
||||
if screen != nil {
|
||||
update.Screens = []*sstore.ScreenType{screen}
|
||||
}
|
||||
go pushNumRunningCmdsUpdate(&finalPk.CK, -1)
|
||||
sstore.MainBus.SendUpdate(update)
|
||||
}
|
||||
|
||||
@ -2057,7 +2113,11 @@ func (msh *MShellProc) handleCmdErrorPacket(errPk *packet.CmdErrorPacketType) {
|
||||
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)
|
||||
if err != nil {
|
||||
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, 0, err)
|
||||
@ -2066,7 +2126,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa
|
||||
}
|
||||
var ack *packet.DataAckPacketType
|
||||
if len(realData) > 0 {
|
||||
dataPos := dataPosMap[dataPk.CK]
|
||||
dataPos := dataPosMap.Get(dataPk.CK)
|
||||
rcmd := msh.GetRunningCmd(dataPk.CK)
|
||||
update, err := sstore.AppendToCmdPtyBlob(context.Background(), rcmd.ScreenId, dataPk.CK.GetCmdId(), realData, dataPos)
|
||||
if err != nil {
|
||||
@ -2074,7 +2134,7 @@ func (msh *MShellProc) handleDataPacket(dataPk *packet.DataPacketType, dataPosMa
|
||||
} else {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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() {
|
||||
msh.handleDataPacket(dataPk, dataPosMap)
|
||||
}
|
||||
@ -2124,12 +2184,10 @@ func (msh *MShellProc) ProcessPackets() {
|
||||
go sendScreenUpdates(screens)
|
||||
}
|
||||
})
|
||||
// TODO need to clean dataPosMap
|
||||
dataPosMap := make(map[base.CommandKey]int64)
|
||||
for pk := range msh.ServerProc.Output.MainCh {
|
||||
if pk.GetType() == packet.DataPacketStr {
|
||||
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)
|
||||
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
|
||||
func pushStatusIndicatorUpdate(ck *base.CommandKey, level sstore.StatusIndicatorLevel) {
|
||||
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 WaveDevDirName = ".waveterm-dev" // must match emain.ts
|
||||
const WaveAppPathVarName = "WAVETERM_APP_PATH"
|
||||
const WaveVersion = "v0.6.0"
|
||||
const WaveVersion = "v0.6.1"
|
||||
const WaveAuthKeyFileName = "waveterm.authkey"
|
||||
const MShellVersion = "v0.4.0"
|
||||
|
||||
|
@ -763,29 +763,37 @@ 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) {
|
||||
var lineId string
|
||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (string, 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)
|
||||
if err == nil {
|
||||
// valid 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 {
|
||||
// prefix id string match
|
||||
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 {
|
||||
// id match
|
||||
query := `SELECT lineid FROM line WHERE screenid = ? AND lineid = ?`
|
||||
lineId = tx.GetString(query, screenId, lineArg)
|
||||
lineId := tx.GetString(query, screenId, lineArg)
|
||||
return lineId, nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return "", txErr
|
||||
}
|
||||
return lineId, nil
|
||||
}
|
||||
|
||||
func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) {
|
||||
@ -836,8 +844,8 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error {
|
||||
cmd.OrigTermOpts = cmd.TermOpts
|
||||
cmdMap := cmd.ToMap()
|
||||
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)
|
||||
VALUES (: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,:restartts,:exitcode,:durationms,:rtnstate,:runout,:rtnbasehash,:rtndiffhasharr)
|
||||
`
|
||||
tx.NamedExec(query, cmdMap)
|
||||
}
|
||||
@ -879,6 +887,20 @@ func UpdateWithUpdateOpenAICmdInfoPacket(ctx context.Context, screenId string, m
|
||||
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) {
|
||||
if donePk == nil {
|
||||
return nil, fmt.Errorf("invalid cmddone packet")
|
||||
@ -922,7 +944,12 @@ func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.C
|
||||
} else {
|
||||
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
|
||||
}
|
||||
@ -1080,7 +1107,11 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*
|
||||
update.OpenAICmdInfoChat = ScreenMemGetCmdInfoChat(screenId).Messages
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -1490,12 +1521,16 @@ func ArchiveScreenLines(ctx context.Context, screenId string) (*ModelUpdate, err
|
||||
func DeleteScreenLines(ctx context.Context, screenId string) (*ModelUpdate, error) {
|
||||
var lineIds []string
|
||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||
query := `SELECT lineid FROM line WHERE screenid = ?`
|
||||
lineIds = tx.SelectStrings(query, screenId)
|
||||
query = `DELETE FROM line WHERE screenid = ?`
|
||||
tx.Exec(query, screenId)
|
||||
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ?`
|
||||
tx.Exec(query, screenId)
|
||||
query := `SELECT lineid 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'))`
|
||||
lineIds = tx.SelectStrings(query, screenId, screenId)
|
||||
query = `DELETE FROM line
|
||||
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
|
||||
})
|
||||
if txErr != nil {
|
||||
@ -2091,6 +2126,19 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar
|
||||
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)
|
||||
func FixupScreenSelectedLine(ctx context.Context, screenId string) (*ScreenType, error) {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) {
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/cirfile"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||
"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)
|
||||
}
|
||||
|
||||
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) {
|
||||
if screenId == "" {
|
||||
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
|
||||
}
|
||||
|
||||
func ScreenMemSetNumRunningCommands(screenId string, num int) {
|
||||
func ScreenMemIncrementNumRunningCommands(screenId string, delta int) int {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
if ScreenMemStore[screenId] == nil {
|
||||
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.
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
)
|
||||
|
||||
const MaxMigration = 30
|
||||
const MaxMigration = 31
|
||||
const MigratePrimaryScreenVersion = 9
|
||||
const CmdScreenSpecialMigration = 13
|
||||
const CmdLineSpecialMigration = 20
|
||||
|
@ -1118,13 +1118,15 @@ type CmdType struct {
|
||||
Status string `json:"status"`
|
||||
CmdPid int `json:"cmdpid"`
|
||||
RemotePid int `json:"remotepid"`
|
||||
RestartTs int64 `json:"restartts,omitempty"`
|
||||
DoneTs int64 `json:"donets"`
|
||||
ExitCode int `json:"exitcode"`
|
||||
DurationMs int `json:"durationms"`
|
||||
RunOut []packet.PacketType `json:"runout,omitempty"`
|
||||
RtnState bool `json:"rtnstate,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{} {
|
||||
@ -1189,6 +1191,7 @@ func (cmd *CmdType) ToMap() map[string]interface{} {
|
||||
rtn["status"] = cmd.Status
|
||||
rtn["cmdpid"] = cmd.CmdPid
|
||||
rtn["remotepid"] = cmd.RemotePid
|
||||
rtn["restartts"] = cmd.RestartTs
|
||||
rtn["donets"] = cmd.DoneTs
|
||||
rtn["exitcode"] = cmd.ExitCode
|
||||
rtn["durationms"] = cmd.DurationMs
|
||||
@ -1216,6 +1219,7 @@ func (cmd *CmdType) FromMap(m map[string]interface{}) bool {
|
||||
quickSetInt(&cmd.CmdPid, m, "cmdpid")
|
||||
quickSetInt(&cmd.RemotePid, m, "remotepid")
|
||||
quickSetInt64(&cmd.DoneTs, m, "donets")
|
||||
quickSetInt64(&cmd.RestartTs, m, "restartts")
|
||||
quickSetInt(&cmd.ExitCode, m, "exitcode")
|
||||
quickSetInt(&cmd.DurationMs, m, "durationms")
|
||||
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.
|
||||
func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, screenId string, level StatusIndicatorLevel, force bool) error {
|
||||
var newStatus StatusIndicatorLevel
|
||||
|
||||
if force {
|
||||
// Force the update and set the new status to the given level, regardless of the current status or the active screen
|
||||
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
|
||||
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{}
|
||||
err := SetStatusIndicatorLevel_Update(ctx, update, screenId, level, false)
|
||||
if err != nil {
|
||||
log.Printf("error setting status indicator level: %v\n", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
MainBus.SendUpdate(update)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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"`
|
||||
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
|
||||
ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"`
|
||||
ScreenNumRunningCommands *ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
||||
UserInputRequest *UserInputRequestType `json:"userinputrequest,omitempty"`
|
||||
}
|
||||
|
||||
@ -286,6 +287,11 @@ type ScreenStatusIndicatorType struct {
|
||||
Status StatusIndicatorLevel `json:"status"`
|
||||
}
|
||||
|
||||
type ScreenNumRunningCommandsType struct {
|
||||
ScreenId string `json:"screenid"`
|
||||
Num int `json:"num"`
|
||||
}
|
||||
|
||||
func (bus *UpdateBus) registerUserInputChannel() (string, chan *UserInputResponseType) {
|
||||
bus.Lock.Lock()
|
||||
defer bus.Lock.Unlock()
|
||||
|
Loading…
Reference in New Issue
Block a user