mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
Merge branch 'main' of github.com:wavetermdev/waveterm into red/aichat-sidebar
This commit is contained in:
commit
8ea22c4c1f
48
.github/workflows/regression.yml
vendored
48
.github/workflows/regression.yml
vendored
@ -1,24 +1,28 @@
|
|||||||
name: TestDriver.ai Regression Testing (test)
|
name: TestDriver.ai Regression Testing
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches:
|
||||||
|
- main
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 21 * * *" # every day at 9pm
|
- cron: 0 21 * * *
|
||||||
workflow_dispatch:
|
workflow_dispatch: null
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # To allow the action to read repository contents
|
||||||
|
pull-requests: write # To allow the action to create/update pull request comments
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: "TestDriver"
|
name: TestDriver
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dashcamio/testdriver@main
|
- uses: dashcamio/testdriver@main
|
||||||
id: testdriver
|
id: testdriver
|
||||||
# note that .testdriver/prerun.sh runs before this, so the app has launched already
|
|
||||||
with:
|
with:
|
||||||
version: v2.9.4
|
version: v2.10.2
|
||||||
prerun: |
|
prerun: |
|
||||||
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
|
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
|
||||||
cd ~/actions-runner/_work/testdriver/testdriver/
|
cd ~/actions-runner/_work/testdriver/testdriver/
|
||||||
@ -38,8 +42,24 @@ jobs:
|
|||||||
echo "Electron Done"
|
echo "Electron Done"
|
||||||
exit
|
exit
|
||||||
prompt: |
|
prompt: |
|
||||||
2. click "Create new tab"
|
1. wait 10 seconds
|
||||||
2. focus the Wave input with the keyboard shorcut Command + I
|
1. click "Continue"
|
||||||
3. type 'ls' into the input
|
1. click "Create new tab"
|
||||||
4. press return
|
1. validate that overlapping text does not appear in the application
|
||||||
5. validate Wave shows the result of 'ls'
|
1. focus the Wave input with the keyboard shorcut Command + I
|
||||||
|
1. type 'ls' into the input
|
||||||
|
1. press return
|
||||||
|
1. validate Wave shows the result of 'ls'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: peter-evans/create-or-update-comment@v4
|
||||||
|
if: ${{always()}}
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
body: |
|
||||||
|
## TestDriver Summary
|
||||||
|
${{ steps.testdriver.outputs.markdown }}
|
||||||
|
${{ steps.testdriver.outputs.summary }}
|
||||||
|
reactions: |
|
||||||
|
+1
|
||||||
|
-1
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
# Wave Terminal
|
# Wave Terminal
|
||||||
|
|
||||||
|
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_shield)
|
||||||
|
|
||||||
Wave is an open-source AI-native terminal built for seamless workflows.
|
Wave is an open-source AI-native terminal built for seamless workflows.
|
||||||
|
|
||||||
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. Wave combines command line with the power of the open web to help veteran CLI users and new developers alike.
|
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. Wave combines command line with the power of the open web to help veteran CLI users and new developers alike.
|
||||||
@ -56,3 +58,7 @@ Find more information in our [Contributions Guide](CONTRIBUTING.md), which inclu
|
|||||||
|
|
||||||
- [Ways to contribute](CONTRIBUTING.md#contributing-to-wave-terminal)
|
- [Ways to contribute](CONTRIBUTING.md#contributing-to-wave-terminal)
|
||||||
- [Contribution guidelines](CONTRIBUTING.md#before-you-start)
|
- [Contribution guidelines](CONTRIBUTING.md#before-you-start)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Wave Terminal is licensed under the Apache-2.0 License. For more information on our dependencies, see [here](./acknowledgements/README.md).
|
||||||
|
@ -1,20 +1,5 @@
|
|||||||
# Open-Source Acknowledgements
|
# Open-Source Acknowledgements
|
||||||
|
|
||||||
We make use of many amazing open-source projects to build Wave Terminal. Here are the links to the latest acknowledgements for each of our components, including license disclaimers for each dependency:
|
We make use of many amazing open-source projects to build Wave Terminal. We automatically generate license reports via FOSSA to comply with the license distribution requirements of our dependencies. Below is a summary of the licenses used by our product. Clicking on the image will take you to the full report on FOSSA's website.
|
||||||
|
|
||||||
- [Frontend](./disclaimers/frontend.md)
|
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_large)
|
||||||
- [Backend](./disclaimers/backend.md)
|
|
||||||
|
|
||||||
## Generating license disclaimers
|
|
||||||
|
|
||||||
The license disclaimers for the backend are generated using the [go-licenses](https://github.com/google/go-licenses) tool. We supply a template file ([`go_licenses_report.tpl`](./go_licenses_report.tpl)) to generate a pretty print of the disclaimers for each dependency. This outputs to the file [`backend.md`](./disclaimers/backend.md).
|
|
||||||
|
|
||||||
The license disclaimers for the frontend are generated using the [`yarn licenses` tool](https://classic.yarnpkg.com/lang/en/docs/cli/licenses/). This outputs to the file [`frontend.md`](./disclaimers/frontend.md).
|
|
||||||
|
|
||||||
These disclaimer files linked above will be periodically regenerated to reflect new dependencies.
|
|
||||||
|
|
||||||
The [`scripthaus.md` file](../scripthaus.md) contains scripts to generate the disclaimers and package them. To manually generate the disclaimers, run the following from the repository root directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripthaus run generate-license-disclaimers
|
|
||||||
```
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,16 +0,0 @@
|
|||||||
# Wave Terminal Backend Acknowledgements
|
|
||||||
|
|
||||||
The following sets forth attribution notices for third party software that may be contained in portions of the Wave Terminal product.
|
|
||||||
{{ range . }}
|
|
||||||
## {{ .Name }}
|
|
||||||
|
|
||||||
* Name: {{ .Name }}
|
|
||||||
* Version: {{ .Version }}
|
|
||||||
* License: [{{ .LicenseName }}]({{ .LicenseURL }})
|
|
||||||
|
|
||||||
```txt
|
|
||||||
{{ .LicenseText }}
|
|
||||||
```
|
|
||||||
|
|
||||||
-----
|
|
||||||
{{ end }}
|
|
@ -156,7 +156,7 @@
|
|||||||
--table-border-color: rgba(241, 246, 243, 0.15);
|
--table-border-color: rgba(241, 246, 243, 0.15);
|
||||||
--table-thead-border-top-color: rgba(250, 250, 250, 0.1);
|
--table-thead-border-top-color: rgba(250, 250, 250, 0.1);
|
||||||
--table-thead-bright-border-color: rgb(204, 204, 204);
|
--table-thead-bright-border-color: rgb(204, 204, 204);
|
||||||
--table-thead-bg-color: rgba(250, 250, 250, 0.02);
|
--table-thead-bg-color: rgb(5, 5, 5);
|
||||||
--table-tr-border-bottom-color: rgba(241, 246, 243, 0.15);
|
--table-tr-border-bottom-color: rgba(241, 246, 243, 0.15);
|
||||||
--table-tr-hover-bg-color: rgba(255, 255, 255, 0.06);
|
--table-tr-hover-bg-color: rgba(255, 255, 255, 0.06);
|
||||||
--table-tr-selected-bg-color: rgb(34, 34, 34);
|
--table-tr-selected-bg-color: rgb(34, 34, 34);
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
--app-accent-color: rgb(75, 166, 57);
|
--app-accent-color: rgb(75, 166, 57);
|
||||||
--app-accent-bg-color: rgba(75, 166, 57, 0.2);
|
--app-accent-bg-color: rgba(75, 166, 57, 0.2);
|
||||||
--app-text-color: rgb(0, 0, 0);
|
--app-text-color: rgb(0, 0, 0);
|
||||||
--app-text-primary-color: rgb(0, 0, 0, 0.9);
|
--app-text-primary-color: rgb(23, 23, 23);
|
||||||
--app-text-secondary-color: rgb(0, 0, 0, 0.7);
|
--app-text-secondary-color: rgb(76, 76, 76);
|
||||||
--app-border-color: rgb(139 145 138);
|
--app-border-color: rgb(139 145 138);
|
||||||
--app-panel-bg-color: rgb(224, 224, 224);
|
--app-panel-bg-color: rgb(224, 224, 224);
|
||||||
--app-panel-bg-color-dev: rgb(224, 224, 224);
|
--app-panel-bg-color-dev: rgb(224, 224, 224);
|
||||||
@ -57,6 +57,10 @@
|
|||||||
--scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.4);
|
--scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.4);
|
||||||
--scrollbar-thumb-active-color: rgba(0, 0, 0, 0.5);
|
--scrollbar-thumb-active-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
/* table colors */
|
||||||
|
--table-thead-bg-color: rgb(253, 253, 253);
|
||||||
|
--table-tr-selected-bg-color: rgb(216, 216, 216);
|
||||||
|
|
||||||
/* line color */
|
/* line color */
|
||||||
--line-actions-bg-color: rgba(0, 0, 0, 0.1);
|
--line-actions-bg-color: rgba(0, 0, 0, 0.1);
|
||||||
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
|
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.clientsettings-view {
|
.clientsettings-view {
|
||||||
.content {
|
.content {
|
||||||
padding: 14px 18px 0 30px;
|
padding: 14px 18px 14px 30px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-dropdown {
|
.wave-dropdown {
|
||||||
|
@ -14,6 +14,7 @@ import * as appconst from "@/app/appconst";
|
|||||||
|
|
||||||
import "./clientsettings.less";
|
import "./clientsettings.less";
|
||||||
import { MainView } from "../common/elements/mainview";
|
import { MainView } from "../common/elements/mainview";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
|
||||||
class ClientSettingsKeybindings extends React.Component<{}, {}> {
|
class ClientSettingsKeybindings extends React.Component<{}, {}> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -251,7 +252,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
|||||||
const curSudoPwClearOnSleep = GlobalModel.getSudoPwClearOnSleep();
|
const curSudoPwClearOnSleep = GlobalModel.getSudoPwClearOnSleep();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
|
<MainView
|
||||||
|
className="clientsettings-view"
|
||||||
|
title="Client Settings"
|
||||||
|
onClose={this.handleClose}
|
||||||
|
scrollable={true}
|
||||||
|
>
|
||||||
<If condition={!isHidden}>
|
<If condition={!isHidden}>
|
||||||
<ClientSettingsKeybindings></ClientSettingsKeybindings>
|
<ClientSettingsKeybindings></ClientSettingsKeybindings>
|
||||||
</If>
|
</If>
|
||||||
|
@ -40,7 +40,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mainview-content {
|
.mainview-content {
|
||||||
padding-top: 14px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import cn from "classnames";
|
|||||||
import { GlobalModel } from "@/models";
|
import { GlobalModel } from "@/models";
|
||||||
|
|
||||||
import "./mainview.less";
|
import "./mainview.less";
|
||||||
|
import { Choose, If, Otherwise, When } from "tsx-control-statements/components";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class MainView extends React.Component<{
|
class MainView extends React.Component<{
|
||||||
@ -14,6 +16,8 @@ class MainView extends React.Component<{
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
scrollable?: boolean;
|
||||||
|
onScrollbarInitialized?: () => void;
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const sidebarModel = GlobalModel.mainSidebarModel;
|
const sidebarModel = GlobalModel.mainSidebarModel;
|
||||||
@ -31,7 +35,21 @@ class MainView extends React.Component<{
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<div className="mainview-content">{this.props.children}</div>
|
<Choose>
|
||||||
|
<When condition={this.props.scrollable}>
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
className="mainview-content"
|
||||||
|
options={{ scrollbars: { autoHide: "leave" } }}
|
||||||
|
defer={true}
|
||||||
|
events={{ initialized: this.props.onScrollbarInitialized }}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</When>
|
||||||
|
<Otherwise>
|
||||||
|
<div className="mainview-content">{this.props.children}</div>
|
||||||
|
</Otherwise>
|
||||||
|
</Choose>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
padding-right: 4px !important;
|
||||||
i {
|
i {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, keybindings, title }
|
|||||||
</If>
|
</If>
|
||||||
{<div className="wave-modal-title">{title}</div>}
|
{<div className="wave-modal-title">{title}</div>}
|
||||||
<If condition={onClose}>
|
<If condition={onClose}>
|
||||||
<Button className="secondary ghost" onClick={onClose}>
|
<Button className="secondary ghost" onClick={() => onClose()} title="Close (ESC)">
|
||||||
<i className="fa-sharp fa-solid fa-xmark"></i>
|
<i className="fa-sharp fa-solid fa-xmark"></i>
|
||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
.rconndetail-modal {
|
.rconndetail-modal {
|
||||||
width: auto;
|
|
||||||
max-width: 80vw;
|
|
||||||
min-height: 565px;
|
|
||||||
|
|
||||||
.wave-modal-content {
|
.wave-modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
width: auto;
|
||||||
|
max-width: 80vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
|
||||||
.wave-modal-body {
|
.wave-modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0px 20px;
|
padding: 20px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
.name-header-actions-wrapper {
|
.name-header-actions-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -53,6 +52,7 @@
|
|||||||
|
|
||||||
.remote-detail {
|
.remote-detail {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding-top: 16px;
|
||||||
|
|
||||||
.settings-field {
|
.settings-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -15,6 +15,7 @@ import * as appconst from "@/app/appconst";
|
|||||||
|
|
||||||
import "./viewremoteconndetail.less";
|
import "./viewremoteconndetail.less";
|
||||||
import { ModalKeybindings } from "../elements/modal";
|
import { ModalKeybindings } from "../elements/modal";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||||
@ -299,15 +300,33 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className="rconndetail-modal">
|
<Modal className="rconndetail-modal">
|
||||||
|
<ModalKeybindings
|
||||||
|
onOk={() => {
|
||||||
|
if (selectedRemoteStatus == "connecting") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleClose();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
if (selectedRemoteStatus == "connecting") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleClose();
|
||||||
|
}}
|
||||||
|
></ModalKeybindings>
|
||||||
<Modal.Header title="Connection" onClose={this.handleClose} />
|
<Modal.Header title="Connection" onClose={this.handleClose} />
|
||||||
<div className="wave-modal-body">
|
<OverlayScrollbarsComponent
|
||||||
|
className="wave-modal-body"
|
||||||
|
options={{ scrollbars: { autoHide: "leave" } }}
|
||||||
|
defer={true}
|
||||||
|
>
|
||||||
<div className="name-header-actions-wrapper">
|
<div className="name-header-actions-wrapper">
|
||||||
<div className="name text-primary name-wrapper">
|
<div className="name text-primary name-wrapper">
|
||||||
{util.getRemoteName(remote)} {getImportTooltip(remote)}
|
{util.getRemoteName(remote)} {getImportTooltip(remote)}
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="remote-detail" style={{ overflow: "hidden" }}>
|
<div className="remote-detail">
|
||||||
<div className="settings-field">
|
<div className="settings-field">
|
||||||
<div className="settings-label">Conn Id</div>
|
<div className="settings-label">Conn Id</div>
|
||||||
<div className="settings-input">{remote.remoteid}</div>
|
<div className="settings-input">{remote.remoteid}</div>
|
||||||
@ -381,33 +400,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</OverlayScrollbarsComponent>
|
||||||
<div className="wave-modal-footer">
|
|
||||||
<ModalKeybindings
|
|
||||||
onOk={() => {
|
|
||||||
if (selectedRemoteStatus == "connecting") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleClose();
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
if (selectedRemoteStatus == "connecting") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleClose();
|
|
||||||
}}
|
|
||||||
></ModalKeybindings>
|
|
||||||
<Button
|
|
||||||
className="secondary"
|
|
||||||
disabled={selectedRemoteStatus == "connecting"}
|
|
||||||
onClick={this.handleClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button disabled={selectedRemoteStatus == "connecting"} onClick={this.handleClose}>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,20 @@
|
|||||||
margin: 20px 50px 20px 20px;
|
margin: 20px 50px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connections-table-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-height: 85vh;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 970px;
|
||||||
|
}
|
||||||
|
|
||||||
.connections-table {
|
.connections-table {
|
||||||
margin: 0px 10px 10px 10px;
|
margin: 0px 10px 10px 10px;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
max-width: 970px;
|
position: relative;
|
||||||
|
|
||||||
colgroup {
|
colgroup {
|
||||||
.first-col {
|
.first-col {
|
||||||
@ -28,13 +38,16 @@
|
|||||||
|
|
||||||
thead {
|
thead {
|
||||||
border-radius: var(--sizing-2-xs, 4px);
|
border-radius: var(--sizing-2-xs, 4px);
|
||||||
border-bottom: 2px solid var(--table-thead-bright-border-color);
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 5px 15px 5px 10px;
|
padding: 5px 15px 5px 10px;
|
||||||
color: var(--app-text-color);
|
color: var(--app-text-color);
|
||||||
|
border-bottom: 2px solid var(--table-thead-bright-border-color);
|
||||||
|
background: var(--table-thead-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,8 +91,10 @@
|
|||||||
|
|
||||||
footer {
|
footer {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-shrink: 0;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import * as util from "@/util/util";
|
|||||||
|
|
||||||
import "./connections.less";
|
import "./connections.less";
|
||||||
import { MainView } from "../common/elements/mainview";
|
import { MainView } from "../common/elements/mainview";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
|
||||||
class ConnectionsKeybindings extends React.Component<{}, {}> {
|
class ConnectionsKeybindings extends React.Component<{}, {}> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -149,57 +150,63 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
|||||||
<If condition={!isHidden}>
|
<If condition={!isHidden}>
|
||||||
<ConnectionsKeybindings></ConnectionsKeybindings>
|
<ConnectionsKeybindings></ConnectionsKeybindings>
|
||||||
</If>
|
</If>
|
||||||
<table
|
<OverlayScrollbarsComponent
|
||||||
className="connections-table"
|
className="connections-table-container"
|
||||||
cellSpacing="0"
|
options={{ scrollbars: { autoHide: "leave" } }}
|
||||||
cellPadding="0"
|
defer={true}
|
||||||
border={0}
|
|
||||||
ref={this.tableRef}
|
|
||||||
onMouseLeave={this.handleTableHoverLeave}
|
|
||||||
>
|
>
|
||||||
<colgroup>
|
<table
|
||||||
<col className="first-col" />
|
className="connections-table"
|
||||||
<col className="second-col" />
|
cellSpacing="0"
|
||||||
<col className="third-col" />
|
cellPadding="0"
|
||||||
</colgroup>
|
border={0}
|
||||||
<thead>
|
ref={this.tableRef}
|
||||||
<tr>
|
onMouseLeave={this.handleTableHoverLeave}
|
||||||
<th className="text-standard col-name">
|
>
|
||||||
<div>Name</div>
|
<colgroup>
|
||||||
</th>
|
<col className="first-col" />
|
||||||
<th className="text-standard col-type">
|
<col className="second-col" />
|
||||||
<div>Type</div>
|
<col className="third-col" />
|
||||||
</th>
|
</colgroup>
|
||||||
<th className="text-standard col-status">
|
<thead>
|
||||||
<div>Status</div>
|
<tr>
|
||||||
</th>
|
<th className="text-standard col-name">
|
||||||
</tr>
|
<div>Name</div>
|
||||||
</thead>
|
</th>
|
||||||
<tbody>
|
<th className="text-standard col-type">
|
||||||
<For index="idx" each="item" of={items}>
|
<div>Type</div>
|
||||||
<tr
|
</th>
|
||||||
key={item.remoteid}
|
<th className="text-standard col-status">
|
||||||
className={cn("connections-item", {
|
<div>Status</div>
|
||||||
hovered: this.state.hoveredItemId === item.remoteid,
|
</th>
|
||||||
})}
|
|
||||||
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here
|
|
||||||
>
|
|
||||||
<td className="col-name">
|
|
||||||
<Status status={this.getStatus(item.status)} text=""></Status>
|
|
||||||
{this.getName(item)} {this.getImportSymbol(item)}
|
|
||||||
</td>
|
|
||||||
<td className="col-type">
|
|
||||||
<div>{item.remotetype}</div>
|
|
||||||
</td>
|
|
||||||
<td className="col-status">
|
|
||||||
<div>
|
|
||||||
<Status status={this.getStatus(item.status)} text={item.status} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</For>
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
<For index="idx" each="item" of={items}>
|
||||||
|
<tr
|
||||||
|
key={item.remoteid}
|
||||||
|
className={cn("connections-item", {
|
||||||
|
hovered: this.state.hoveredItemId === item.remoteid,
|
||||||
|
})}
|
||||||
|
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here
|
||||||
|
>
|
||||||
|
<td className="col-name">
|
||||||
|
<Status status={this.getStatus(item.status)} text=""></Status>
|
||||||
|
{this.getName(item)} {this.getImportSymbol(item)}
|
||||||
|
</td>
|
||||||
|
<td className="col-type">
|
||||||
|
<div>{item.remotetype}</div>
|
||||||
|
</td>
|
||||||
|
<td className="col-status">
|
||||||
|
<div>
|
||||||
|
<Status status={this.getStatus(item.status)} text={item.status} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
<footer>
|
<footer>
|
||||||
<Button
|
<Button
|
||||||
className="secondary"
|
className="secondary"
|
||||||
|
@ -260,8 +260,8 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
|||||||
<div className="logo">
|
<div className="logo">
|
||||||
<WaveLogo />
|
<WaveLogo />
|
||||||
</div>
|
</div>
|
||||||
<div className="close-button">
|
<div className="close-button" onClick={toggleCollapse}>
|
||||||
<i className="fa-sharp fa-solid fa-xmark-large" onClick={toggleCollapse} />
|
<i className="fa-sharp fa-solid fa-xmark-large" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="contents">
|
<div className="contents">
|
||||||
|
@ -61,9 +61,11 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
mobx.makeObservable(this);
|
||||||
this.chatWindowScrollRef = React.createRef();
|
this.chatWindowScrollRef = React.createRef();
|
||||||
this.textAreaRef = React.createRef();
|
this.textAreaRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
if (this.chatWindowScrollRef?.current != null) {
|
if (this.chatWindowScrollRef?.current != null) {
|
||||||
@ -88,7 +90,7 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submitChatMessage(messageStr: string) {
|
submitChatMessage(messageStr: string) {
|
||||||
const curLine = GlobalModel.inputModel.getCurLine();
|
const curLine = GlobalModel.inputModel.curLine;
|
||||||
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
|
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
|
||||||
prtn.then((rtn) => {
|
prtn.then((rtn) => {
|
||||||
if (!rtn.success) {
|
if (!rtn.success) {
|
||||||
@ -103,15 +105,19 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
return { numLines, linePos };
|
return { numLines, linePos };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action.bound
|
||||||
onTextAreaFocused(e: any) {
|
onTextAreaFocused(e: any) {
|
||||||
GlobalModel.inputModel.setAuxViewFocus(true);
|
GlobalModel.inputModel.setAuxViewFocus(true);
|
||||||
|
this.onTextAreaChange(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action.bound
|
||||||
onTextAreaBlur(e: any) {
|
onTextAreaBlur(e: any) {
|
||||||
GlobalModel.inputModel.setAuxViewFocus(false);
|
//GlobalModel.inputModel.setAuxViewFocus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust the height of the textarea to fit the text
|
// Adjust the height of the textarea to fit the text
|
||||||
|
@boundMethod
|
||||||
onTextAreaChange(e: any) {
|
onTextAreaChange(e: any) {
|
||||||
// Calculate the bounding height of the text area
|
// Calculate the bounding height of the text area
|
||||||
const textAreaMaxLines = 4;
|
const textAreaMaxLines = 4;
|
||||||
@ -126,6 +132,9 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
// Set the new height of the text area, bounded by the min and max height.
|
// Set the new height of the text area, bounded by the min and max height.
|
||||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||||
this.textAreaRef.current.style.height = newHeight + "px";
|
this.textAreaRef.current.style.height = newHeight + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextAreaInput(e: any) {
|
||||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,8 +149,10 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
this.submitChatMessage(messageStr);
|
this.submitChatMessage(messageStr);
|
||||||
currentRef.value = "";
|
currentRef.value = "";
|
||||||
} else {
|
} else {
|
||||||
inputModel.grabCodeSelectSelection();
|
mobx.action(() => {
|
||||||
inputModel.setAuxViewFocus(false);
|
inputModel.grabCodeSelectSelection();
|
||||||
|
inputModel.setAuxViewFocus(false);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +193,6 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mobx.action
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
onKeyDown(e: any) {}
|
onKeyDown(e: any) {}
|
||||||
|
|
||||||
@ -254,9 +264,10 @@ class AIChat extends React.Component<{}, {}> {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
id="chat-cmd-input"
|
id="chat-cmd-input"
|
||||||
onFocus={this.onTextAreaFocused.bind(this)}
|
onFocus={this.onTextAreaFocused}
|
||||||
onBlur={this.onTextAreaBlur.bind(this)}
|
onBlur={this.onTextAreaBlur}
|
||||||
onChange={this.onTextAreaChange.bind(this)}
|
onChange={this.onTextAreaChange}
|
||||||
|
onInput={this.onTextAreaInput}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
style={{ fontSize: this.termFontSize }}
|
style={{ fontSize: this.termFontSize }}
|
||||||
className="chat-textarea"
|
className="chat-textarea"
|
||||||
|
@ -193,8 +193,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This aligns the icons with the prompt field.
|
// This aligns the icons with the prompt field.
|
||||||
// We don't need right padding because the whole input field is already padded.
|
// We don't need right margin because the whole input field is already padded.
|
||||||
padding: 2px 0 0 12px;
|
margin: 2px 0 0 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,11 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
cmdInputRef: React.RefObject<any> = React.createRef();
|
cmdInputRef: React.RefObject<any> = React.createRef();
|
||||||
promptRef: React.RefObject<any> = React.createRef();
|
promptRef: React.RefObject<any> = React.createRef();
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
mobx.makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateCmdInputHeight();
|
this.updateCmdInputHeight();
|
||||||
}
|
}
|
||||||
@ -56,7 +61,7 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
this.updateCmdInputHeight();
|
this.updateCmdInputHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
clickFocusInputHint(): void {
|
clickFocusInputHint(): void {
|
||||||
GlobalModel.inputModel.giveFocus();
|
GlobalModel.inputModel.giveFocus();
|
||||||
}
|
}
|
||||||
@ -75,7 +80,7 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
GlobalModel.inputModel.setAuxViewFocus(false);
|
GlobalModel.inputModel.setAuxViewFocus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
clickAIAction(e: any): void {
|
clickAIAction(e: any): void {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -87,7 +92,7 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
clickHistoryAction(e: any): void {
|
clickHistoryAction(e: any): void {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -105,11 +110,9 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
GlobalCommandRunner.connectRemote(remoteId);
|
GlobalCommandRunner.connectRemote(remoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
toggleFilter(screen: Screen) {
|
toggleFilter(screen: Screen) {
|
||||||
mobx.action(() => {
|
screen.filterRunning.set(!screen.filterRunning.get());
|
||||||
screen.filterRunning.set(!screen.filterRunning.get());
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
color: var(--app-text-color);
|
color: var(--app-text-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
.history-item {
|
.history-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -42,6 +42,11 @@ class HItem extends React.Component<
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
> {
|
> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
mobx.makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
renderRemote(hitem: HistoryItem): any {
|
renderRemote(hitem: HistoryItem): any {
|
||||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||||
return sprintf("%-15s ", "");
|
return sprintf("%-15s ", "");
|
||||||
@ -169,12 +174,12 @@ class HistoryInfo extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
handleClose() {
|
handleClose() {
|
||||||
GlobalModel.inputModel.closeAuxView();
|
GlobalModel.inputModel.closeAuxView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
handleItemClick(hitem: HistoryItem) {
|
handleItemClick(hitem: HistoryItem) {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
const selItem = inputModel.getHistorySelectedItem();
|
const selItem = inputModel.getHistorySelectedItem();
|
||||||
@ -195,14 +200,14 @@ class HistoryInfo extends React.Component<{}, {}> {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
handleClickType() {
|
handleClickType() {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
inputModel.setAuxViewFocus(true);
|
inputModel.setAuxViewFocus(true);
|
||||||
inputModel.toggleHistoryType();
|
inputModel.toggleHistoryType();
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
handleClickRemote() {
|
handleClickRemote() {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
inputModel.setAuxViewFocus(true);
|
inputModel.setAuxViewFocus(true);
|
||||||
@ -229,7 +234,7 @@ class HistoryInfo extends React.Component<{}, {}> {
|
|||||||
render() {
|
render() {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
const selItem = inputModel.getHistorySelectedItem();
|
const selItem = inputModel.getHistorySelectedItem();
|
||||||
const hitems = inputModel.getFilteredHistoryItems();
|
const hitems = inputModel.filteredHistoryItems;
|
||||||
const opts = inputModel.historyQueryOpts.get();
|
const opts = inputModel.historyQueryOpts.get();
|
||||||
let hitem: HistoryItem = null;
|
let hitem: HistoryItem = null;
|
||||||
let snames: Record<string, string> = {};
|
let snames: Record<string, string> = {};
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as mobxReact from "mobx-react";
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
import { If, For } from "tsx-control-statements/components";
|
import { If, For } from "tsx-control-statements/components";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@ -17,6 +18,11 @@ dayjs.extend(localizedFormat);
|
|||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class InfoMsg extends React.Component<{}, {}> {
|
class InfoMsg extends React.Component<{}, {}> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
mobx.makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
getAfterSlash(s: string): string {
|
getAfterSlash(s: string): string {
|
||||||
if (s.startsWith("^/")) {
|
if (s.startsWith("^/")) {
|
||||||
return s.substring(1);
|
return s.substring(1);
|
||||||
|
@ -117,7 +117,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
|
|||||||
const lastTab = this.lastTab;
|
const lastTab = this.lastTab;
|
||||||
this.lastTab = true;
|
this.lastTab = true;
|
||||||
this.curPress = "tab";
|
this.curPress = "tab";
|
||||||
const curLine = inputModel.getCurLine();
|
const curLine = inputModel.curLine;
|
||||||
if (lastTab) {
|
if (lastTab) {
|
||||||
GlobalModel.submitCommand(
|
GlobalModel.submitCommand(
|
||||||
"_compgen",
|
"_compgen",
|
||||||
@ -250,9 +250,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
lastSP: StrWithPos = { str: "", pos: appconst.NoStrPos };
|
lastSP: StrWithPos = { str: "", pos: appconst.NoStrPos };
|
||||||
version: OV<number> = mobx.observable.box(0, { name: "textAreaInput-version" }); // forces render updates
|
version: OV<number> = mobx.observable.box(0, { name: "textAreaInput-version" }); // forces render updates
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
mobx.makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
incVersion(): void {
|
incVersion(): void {
|
||||||
const v = this.version.get();
|
const v = this.version.get();
|
||||||
mobx.action(() => this.version.set(v + 1))();
|
this.version.set(v + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurSP(): StrWithPos {
|
getCurSP(): StrWithPos {
|
||||||
@ -278,6 +284,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP);
|
GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setFocus(): void {
|
setFocus(): void {
|
||||||
GlobalModel.inputModel.giveFocus();
|
GlobalModel.inputModel.giveFocus();
|
||||||
}
|
}
|
||||||
@ -311,7 +318,8 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
@mobx.action.bound
|
||||||
|
handleComponentDidMount() {
|
||||||
const activeScreen = GlobalModel.getActiveScreen();
|
const activeScreen = GlobalModel.getActiveScreen();
|
||||||
if (activeScreen != null) {
|
if (activeScreen != null) {
|
||||||
const focusType = activeScreen.focusType.get();
|
const focusType = activeScreen.focusType.get();
|
||||||
@ -324,6 +332,24 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
this.updateSP();
|
this.updateSP();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.handleComponentDidMount();
|
||||||
|
this.updateCursorPosIfForced();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCursorPosIfForced() {
|
||||||
|
const inputModel = GlobalModel.inputModel;
|
||||||
|
const fcpos = inputModel.forceCursorPos.get();
|
||||||
|
if (fcpos != null && fcpos != appconst.NoStrPos) {
|
||||||
|
if (this.mainInputRef.current != null) {
|
||||||
|
this.mainInputRef.current.selectionStart = fcpos;
|
||||||
|
this.mainInputRef.current.selectionEnd = fcpos;
|
||||||
|
}
|
||||||
|
inputModel.forceCursorPos.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const activeScreen = GlobalModel.getActiveScreen();
|
const activeScreen = GlobalModel.getActiveScreen();
|
||||||
if (activeScreen != null) {
|
if (activeScreen != null) {
|
||||||
@ -334,14 +360,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
this.lastFocusType = focusType;
|
this.lastFocusType = focusType;
|
||||||
}
|
}
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
const fcpos = inputModel.forceCursorPos.get();
|
this.updateCursorPosIfForced();
|
||||||
if (fcpos != null && fcpos != appconst.NoStrPos) {
|
|
||||||
if (this.mainInputRef.current != null) {
|
|
||||||
this.mainInputRef.current.selectionStart = fcpos;
|
|
||||||
this.mainInputRef.current.selectionEnd = fcpos;
|
|
||||||
}
|
|
||||||
mobx.action(() => inputModel.forceCursorPos.set(null))();
|
|
||||||
}
|
|
||||||
if (inputModel.forceInputFocus) {
|
if (inputModel.forceInputFocus) {
|
||||||
inputModel.forceInputFocus = false;
|
inputModel.forceInputFocus = false;
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
@ -414,21 +433,18 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
||||||
GlobalModel.inputModel.setCurLine(currentRef.value);
|
GlobalModel.inputModel.curLine = currentRef.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mobx.action
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
onKeyDown(e: any) {}
|
onKeyDown(e: any) {}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
onChange(e: any) {
|
onChange(e: any) {
|
||||||
mobx.action(() => {
|
GlobalModel.inputModel.curLine = e.target.value;
|
||||||
GlobalModel.inputModel.setCurLine(e.target.value);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
onSelect(e: any) {
|
onSelect(e: any) {
|
||||||
this.incVersion();
|
this.incVersion();
|
||||||
}
|
}
|
||||||
@ -453,7 +469,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
controlP() {
|
controlP() {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
if (!inputModel.isHistoryLoaded()) {
|
if (!inputModel.isHistoryLoaded()) {
|
||||||
@ -465,7 +481,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
this.lastHistoryUpDown = true;
|
this.lastHistoryUpDown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
controlN() {
|
controlN() {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
inputModel.moveHistorySelection(-1);
|
inputModel.moveHistorySelection(-1);
|
||||||
@ -526,17 +542,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
handleHistoryInput(e: any) {
|
handleHistoryInput(e: any) {
|
||||||
const inputModel = GlobalModel.inputModel;
|
const inputModel = GlobalModel.inputModel;
|
||||||
mobx.action(() => {
|
const opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||||
const opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
opts.queryStr = e.target.value;
|
||||||
opts.queryStr = e.target.value;
|
inputModel.setHistoryQueryOpts(opts);
|
||||||
inputModel.setHistoryQueryOpts(opts);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action.bound
|
||||||
handleFocus(e: any) {
|
handleFocus(e: any) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
GlobalModel.inputModel.giveFocus();
|
GlobalModel.inputModel.giveFocus();
|
||||||
@ -561,7 +575,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
render() {
|
render() {
|
||||||
const model = GlobalModel;
|
const model = GlobalModel;
|
||||||
const inputModel = model.inputModel;
|
const inputModel = model.inputModel;
|
||||||
const curLine = inputModel.getCurLine();
|
const curLine = inputModel.curLine;
|
||||||
let displayLines = 1;
|
let displayLines = 1;
|
||||||
const numLines = curLine.split("\n").length;
|
const numLines = curLine.split("\n").length;
|
||||||
const maxCols = this.getTextAreaMaxCols();
|
const maxCols = this.getTextAreaMaxCols();
|
||||||
@ -606,7 +620,6 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
|
|
||||||
const renderCmdInputKeybindings = inputModel.shouldRenderAuxViewKeybindings(null);
|
const renderCmdInputKeybindings = inputModel.shouldRenderAuxViewKeybindings(null);
|
||||||
const renderHistoryKeybindings = inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_History);
|
const renderHistoryKeybindings = inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_History);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="textareainput-div control is-expanded"
|
className="textareainput-div control is-expanded"
|
||||||
|
@ -81,7 +81,7 @@ class BookmarksModel {
|
|||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.globalModel.showSessionView();
|
this.globalModel.showSessionView();
|
||||||
this.globalModel.inputModel.setCurLine(bm.cmdstr);
|
this.globalModel.inputModel.curLine = bm.cmdstr;
|
||||||
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
|
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,10 @@
|
|||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { boundMethod } from "autobind-decorator";
|
|
||||||
import { isBlank } from "@/util/util";
|
import { isBlank } from "@/util/util";
|
||||||
import * as appconst from "@/app/appconst";
|
import * as appconst from "@/app/appconst";
|
||||||
import type { Model } from "./model";
|
import type { Model } from "./model";
|
||||||
import { GlobalCommandRunner, GlobalModel } from "./global";
|
import { GlobalCommandRunner, GlobalModel } from "./global";
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
|
function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
|
||||||
return {
|
return {
|
||||||
@ -48,7 +46,6 @@ class InputModel {
|
|||||||
name: "history-items",
|
name: "history-items",
|
||||||
deep: false,
|
deep: false,
|
||||||
}); // sorted in reverse (most recent is index 0)
|
}); // sorted in reverse (most recent is index 0)
|
||||||
filteredHistoryItems: mobx.IComputedValue<HistoryItem[]> = null;
|
|
||||||
historyIndex: mobx.IObservableValue<number> = mobx.observable.box(0, {
|
historyIndex: mobx.IObservableValue<number> = mobx.observable.box(0, {
|
||||||
name: "history-index",
|
name: "history-index",
|
||||||
}); // 1-indexed (because 0 is current)
|
}); // 1-indexed (because 0 is current)
|
||||||
@ -73,11 +70,11 @@ class InputModel {
|
|||||||
physicalInputFocused: OV<boolean> = mobx.observable.box(false);
|
physicalInputFocused: OV<boolean> = mobx.observable.box(false);
|
||||||
forceInputFocus: boolean = false;
|
forceInputFocus: boolean = false;
|
||||||
|
|
||||||
|
lastCurLine: string = "";
|
||||||
|
|
||||||
constructor(globalModel: Model) {
|
constructor(globalModel: Model) {
|
||||||
this.globalModel = globalModel;
|
this.globalModel = globalModel;
|
||||||
this.filteredHistoryItems = mobx.computed(() => {
|
mobx.makeObservable(this);
|
||||||
return this._getFilteredHistoryItems();
|
|
||||||
});
|
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.codeSelectSelectedIndex.set(-1);
|
this.codeSelectSelectedIndex.set(-1);
|
||||||
this.codeSelectBlockRefArray = [];
|
this.codeSelectBlockRefArray = [];
|
||||||
@ -85,12 +82,12 @@ class InputModel {
|
|||||||
this.codeSelectUuid = "";
|
this.codeSelectUuid = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setInputMode(inputMode: null | "comment" | "global"): void {
|
setInputMode(inputMode: null | "comment" | "global"): void {
|
||||||
mobx.action(() => {
|
this.inputMode.set(inputMode);
|
||||||
this.inputMode.set(inputMode);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
toggleHistoryType(): void {
|
toggleHistoryType(): void {
|
||||||
const opts = mobx.toJS(this.historyQueryOpts.get());
|
const opts = mobx.toJS(this.historyQueryOpts.get());
|
||||||
let htype = opts.queryType;
|
let htype = opts.queryType;
|
||||||
@ -104,6 +101,7 @@ class InputModel {
|
|||||||
this.setHistoryType(htype);
|
this.setHistoryType(htype);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
toggleRemoteType(): void {
|
toggleRemoteType(): void {
|
||||||
const opts = mobx.toJS(this.historyQueryOpts.get());
|
const opts = mobx.toJS(this.historyQueryOpts.get());
|
||||||
if (opts.limitRemote) {
|
if (opts.limitRemote) {
|
||||||
@ -116,67 +114,63 @@ class InputModel {
|
|||||||
this.setHistoryQueryOpts(opts);
|
this.setHistoryQueryOpts(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
onInputFocus(isFocused: boolean): void {
|
onInputFocus(isFocused: boolean): void {
|
||||||
mobx.action(() => {
|
if (isFocused) {
|
||||||
if (isFocused) {
|
this.inputFocused.set(true);
|
||||||
this.inputFocused.set(true);
|
this.lineFocused.set(false);
|
||||||
this.lineFocused.set(false);
|
} else if (this.inputFocused.get()) {
|
||||||
} else if (this.inputFocused.get()) {
|
this.inputFocused.set(false);
|
||||||
this.inputFocused.set(false);
|
}
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
onLineFocus(isFocused: boolean): void {
|
onLineFocus(isFocused: boolean): void {
|
||||||
mobx.action(() => {
|
if (isFocused) {
|
||||||
if (isFocused) {
|
this.inputFocused.set(false);
|
||||||
this.inputFocused.set(false);
|
this.lineFocused.set(true);
|
||||||
this.lineFocused.set(true);
|
} else if (this.lineFocused.get()) {
|
||||||
} else if (this.lineFocused.get()) {
|
this.lineFocused.set(false);
|
||||||
this.lineFocused.set(false);
|
}
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focuses the main input or the auxiliary view, depending on the active auxiliary view
|
// Focuses the main input or the auxiliary view, depending on the active auxiliary view
|
||||||
|
@mobx.action
|
||||||
giveFocus(): void {
|
giveFocus(): void {
|
||||||
// Override active view to the main input if aux view does not have focus
|
// Override active view to the main input if aux view does not have focus
|
||||||
const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null;
|
const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null;
|
||||||
mobx.action(() => {
|
switch (activeAuxView) {
|
||||||
switch (activeAuxView) {
|
case appconst.InputAuxView_History: {
|
||||||
case appconst.InputAuxView_History: {
|
const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
|
||||||
const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
|
if (elem != null) {
|
||||||
if (elem != null) {
|
elem.focus();
|
||||||
elem.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case appconst.InputAuxView_AIChat:
|
|
||||||
this.setAIChatFocus();
|
|
||||||
break;
|
|
||||||
case null: {
|
|
||||||
const elem = document.getElementById("main-cmd-input");
|
|
||||||
if (elem != null) {
|
|
||||||
elem.focus();
|
|
||||||
}
|
|
||||||
this.setPhysicalInputFocused(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
const elem: HTMLElement = document.querySelector(".cmd-input .auxview");
|
|
||||||
if (elem != null) {
|
|
||||||
elem.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
})();
|
case appconst.InputAuxView_AIChat:
|
||||||
|
this.setAIChatFocus();
|
||||||
|
break;
|
||||||
|
case null: {
|
||||||
|
const elem = document.getElementById("main-cmd-input");
|
||||||
|
if (elem != null) {
|
||||||
|
elem.focus();
|
||||||
|
}
|
||||||
|
this.setPhysicalInputFocused(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const elem: HTMLElement = document.querySelector(".cmd-input .auxview");
|
||||||
|
if (elem != null) {
|
||||||
|
elem.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setPhysicalInputFocused(isFocused: boolean): void {
|
setPhysicalInputFocused(isFocused: boolean): void {
|
||||||
mobx.action(() => {
|
this.physicalInputFocused.set(isFocused);
|
||||||
this.physicalInputFocused.set(isFocused);
|
|
||||||
})();
|
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
const screen = this.globalModel.getActiveScreen();
|
const screen = this.globalModel.getActiveScreen();
|
||||||
if (screen != null) {
|
if (screen != null) {
|
||||||
@ -203,6 +197,7 @@ class InputModel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setHistoryType(htype: HistoryTypeStrs): void {
|
setHistoryType(htype: HistoryTypeStrs): void {
|
||||||
if (this.historyQueryOpts.get().queryType == htype) {
|
if (this.historyQueryOpts.get().queryType == htype) {
|
||||||
return;
|
return;
|
||||||
@ -214,7 +209,7 @@ class InputModel {
|
|||||||
if (oldItem == null) {
|
if (oldItem == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const newItems = this.getFilteredHistoryItems();
|
const newItems = this.filteredHistoryItems;
|
||||||
if (newItems.length == 0) {
|
if (newItems.length == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -234,15 +229,15 @@ class InputModel {
|
|||||||
return bestIdx + 1;
|
return bestIdx + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setHistoryQueryOpts(opts: HistoryQueryOpts): void {
|
setHistoryQueryOpts(opts: HistoryQueryOpts): void {
|
||||||
mobx.action(() => {
|
const oldItem = this.getHistorySelectedItem();
|
||||||
const oldItem = this.getHistorySelectedItem();
|
this.historyQueryOpts.set(opts);
|
||||||
this.historyQueryOpts.set(opts);
|
const bestIndex = this.findBestNewIndex(oldItem);
|
||||||
const bestIndex = this.findBestNewIndex(oldItem);
|
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
|
||||||
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
|
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
|
||||||
this.AICmdInfoChatItems.replace(chat);
|
this.AICmdInfoChatItems.replace(chat);
|
||||||
this.codeSelectBlockRefArray = [];
|
this.codeSelectBlockRefArray = [];
|
||||||
@ -256,6 +251,7 @@ class InputModel {
|
|||||||
return hitems != null;
|
return hitems != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
loadHistory(show: boolean, afterLoadIndex: number, htype: HistoryTypeStrs) {
|
loadHistory(show: boolean, afterLoadIndex: number, htype: HistoryTypeStrs) {
|
||||||
if (this.historyLoading.get()) {
|
if (this.historyLoading.get()) {
|
||||||
return;
|
return;
|
||||||
@ -266,12 +262,11 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.historyAfterLoadIndex = afterLoadIndex;
|
this.historyAfterLoadIndex = afterLoadIndex;
|
||||||
mobx.action(() => {
|
this.historyLoading.set(true);
|
||||||
this.historyLoading.set(true);
|
|
||||||
})();
|
|
||||||
GlobalCommandRunner.loadHistory(show, htype);
|
GlobalCommandRunner.loadHistory(show, htype);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
openHistory(): void {
|
openHistory(): void {
|
||||||
if (this.historyLoading.get()) {
|
if (this.historyLoading.get()) {
|
||||||
return;
|
return;
|
||||||
@ -287,13 +282,12 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
updateCmdLine(cmdLine: StrWithPos): void {
|
updateCmdLine(cmdLine: StrWithPos): void {
|
||||||
mobx.action(() => {
|
this.curLine = cmdLine.str;
|
||||||
this.setCurLine(cmdLine.str);
|
if (cmdLine.pos != appconst.NoStrPos) {
|
||||||
if (cmdLine.pos != appconst.NoStrPos) {
|
this.forceCursorPos.set(cmdLine.pos);
|
||||||
this.forceCursorPos.set(cmdLine.pos);
|
}
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHistorySelectedItem(): HistoryItem {
|
getHistorySelectedItem(): HistoryItem {
|
||||||
@ -301,7 +295,7 @@ class InputModel {
|
|||||||
if (hidx == 0) {
|
if (hidx == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const hitems = this.getFilteredHistoryItems();
|
const hitems = this.filteredHistoryItems;
|
||||||
if (hidx > hitems.length) {
|
if (hidx > hitems.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -309,15 +303,16 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFirstHistoryItem(): HistoryItem {
|
getFirstHistoryItem(): HistoryItem {
|
||||||
const hitems = this.getFilteredHistoryItems();
|
const hitems = this.filteredHistoryItems;
|
||||||
if (hitems.length == 0) {
|
if (hitems.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return hitems[0];
|
return hitems[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setHistorySelectionNum(hnum: string): void {
|
setHistorySelectionNum(hnum: string): void {
|
||||||
const hitems = this.getFilteredHistoryItems();
|
const hitems = this.filteredHistoryItems;
|
||||||
for (const [i, hitem] of hitems.entries()) {
|
for (const [i, hitem] of hitems.entries()) {
|
||||||
if (hitem.historynum == hnum) {
|
if (hitem.historynum == hnum) {
|
||||||
this.setHistoryIndex(i + 1);
|
this.setHistoryIndex(i + 1);
|
||||||
@ -326,37 +321,33 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setHistoryInfo(hinfo: HistoryInfoType): void {
|
setHistoryInfo(hinfo: HistoryInfoType): void {
|
||||||
mobx.action(() => {
|
const oldItem = this.getHistorySelectedItem();
|
||||||
const oldItem = this.getHistorySelectedItem();
|
const hitems: HistoryItem[] = hinfo.items ?? [];
|
||||||
const hitems: HistoryItem[] = hinfo.items ?? [];
|
this.historyItems.set(hitems);
|
||||||
this.historyItems.set(hitems);
|
this.historyLoading.set(false);
|
||||||
this.historyLoading.set(false);
|
this.historyQueryOpts.get().queryType = hinfo.historytype;
|
||||||
this.historyQueryOpts.get().queryType = hinfo.historytype;
|
if (hinfo.historytype == "session" || hinfo.historytype == "global") {
|
||||||
if (hinfo.historytype == "session" || hinfo.historytype == "global") {
|
this.historyQueryOpts.get().limitRemote = false;
|
||||||
this.historyQueryOpts.get().limitRemote = false;
|
this.historyQueryOpts.get().limitRemoteInstance = false;
|
||||||
this.historyQueryOpts.get().limitRemoteInstance = false;
|
}
|
||||||
|
if (this.historyAfterLoadIndex == -1) {
|
||||||
|
const bestIndex = this.findBestNewIndex(oldItem);
|
||||||
|
setTimeout(() => this.setHistoryIndex(bestIndex, true), 100);
|
||||||
|
} else if (this.historyAfterLoadIndex) {
|
||||||
|
if (hitems.length >= this.historyAfterLoadIndex) {
|
||||||
|
this.setHistoryIndex(this.historyAfterLoadIndex);
|
||||||
}
|
}
|
||||||
if (this.historyAfterLoadIndex == -1) {
|
}
|
||||||
const bestIndex = this.findBestNewIndex(oldItem);
|
this.historyAfterLoadIndex = 0;
|
||||||
setTimeout(() => this.setHistoryIndex(bestIndex, true), 100);
|
if (hinfo.show) {
|
||||||
} else if (this.historyAfterLoadIndex) {
|
this.openHistory();
|
||||||
if (hitems.length >= this.historyAfterLoadIndex) {
|
}
|
||||||
this.setHistoryIndex(this.historyAfterLoadIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.historyAfterLoadIndex = 0;
|
|
||||||
if (hinfo.show) {
|
|
||||||
this.openHistory();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilteredHistoryItems(): HistoryItem[] {
|
@mobx.computed
|
||||||
return this.filteredHistoryItems.get();
|
get filteredHistoryItems(): HistoryItem[] {
|
||||||
}
|
|
||||||
|
|
||||||
_getFilteredHistoryItems(): HistoryItem[] {
|
|
||||||
const hitems: HistoryItem[] = this.historyItems.get() ?? [];
|
const hitems: HistoryItem[] = this.historyItems.get() ?? [];
|
||||||
const rtn: HistoryItem[] = [];
|
const rtn: HistoryItem[] = [];
|
||||||
const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get());
|
const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get());
|
||||||
@ -416,16 +407,15 @@ class InputModel {
|
|||||||
elem.scrollIntoView({ block: "nearest" });
|
elem.scrollIntoView({ block: "nearest" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
grabSelectedHistoryItem(): void {
|
grabSelectedHistoryItem(): void {
|
||||||
const hitem = this.getHistorySelectedItem();
|
const hitem = this.getHistorySelectedItem();
|
||||||
if (hitem == null) {
|
if (hitem == null) {
|
||||||
this.resetHistory();
|
this.resetHistory();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mobx.action(() => {
|
this.resetInput();
|
||||||
this.resetInput();
|
this.curLine = hitem.cmdstr;
|
||||||
this.setCurLine(hitem.cmdstr);
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the auxiliary view if it is open, focuses the main input
|
// Closes the auxiliary view if it is open, focuses the main input
|
||||||
@ -449,8 +439,8 @@ class InputModel {
|
|||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.auxViewFocus.set(view != null);
|
this.auxViewFocus.set(view != null);
|
||||||
this.activeAuxView.set(view);
|
this.activeAuxView.set(view);
|
||||||
|
this.giveFocus();
|
||||||
})();
|
})();
|
||||||
this.giveFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus.
|
// Gets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus.
|
||||||
@ -463,39 +453,32 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus.
|
// Sets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus.
|
||||||
|
@mobx.action
|
||||||
setAuxViewFocus(focus: boolean): void {
|
setAuxViewFocus(focus: boolean): void {
|
||||||
mobx.action(() => {
|
this.auxViewFocus.set(focus);
|
||||||
this.auxViewFocus.set(focus);
|
|
||||||
})();
|
|
||||||
this.giveFocus();
|
this.giveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean {
|
shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean {
|
||||||
return mobx
|
if (view != null && this.getActiveAuxView() != view) {
|
||||||
.computed(() => {
|
return false;
|
||||||
if (view != null && this.getActiveAuxView() != view) {
|
}
|
||||||
return false;
|
if (view != null && !this.getAuxViewFocus()) {
|
||||||
}
|
return false;
|
||||||
if (view != null && !this.getAuxViewFocus()) {
|
}
|
||||||
return false;
|
if (view == null && this.hasFocus() && !this.getAuxViewFocus()) {
|
||||||
}
|
return true;
|
||||||
if (view == null && this.hasFocus() && !this.getAuxViewFocus()) {
|
}
|
||||||
return true;
|
if (view != null && this.getAuxViewFocus()) {
|
||||||
}
|
return true;
|
||||||
if (view != null && this.getAuxViewFocus()) {
|
}
|
||||||
return true;
|
if (GlobalModel.getActiveScreen().getFocusType() == "input" && GlobalModel.activeMainView.get() == "session") {
|
||||||
}
|
return true;
|
||||||
if (
|
}
|
||||||
GlobalModel.getActiveScreen().getFocusType() == "input" &&
|
return false;
|
||||||
GlobalModel.activeMainView.get() == "session"
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setHistoryIndex(hidx: number, force?: boolean): void {
|
setHistoryIndex(hidx: number, force?: boolean): void {
|
||||||
if (hidx < 0) {
|
if (hidx < 0) {
|
||||||
return;
|
return;
|
||||||
@ -503,18 +486,16 @@ class InputModel {
|
|||||||
if (!force && this.historyIndex.get() == hidx) {
|
if (!force && this.historyIndex.get() == hidx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mobx.action(() => {
|
this.historyIndex.set(hidx);
|
||||||
this.historyIndex.set(hidx);
|
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
||||||
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
let hitem = this.getHistorySelectedItem();
|
||||||
let hitem = this.getHistorySelectedItem();
|
if (hitem == null) {
|
||||||
if (hitem == null) {
|
hitem = this.getFirstHistoryItem();
|
||||||
hitem = this.getFirstHistoryItem();
|
|
||||||
}
|
|
||||||
if (hitem != null) {
|
|
||||||
this.scrollHistoryItemIntoView(hitem.historynum);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
if (hitem != null) {
|
||||||
|
this.scrollHistoryItemIntoView(hitem.historynum);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveHistorySelection(amt: number): void {
|
moveHistorySelection(amt: number): void {
|
||||||
@ -524,7 +505,7 @@ class InputModel {
|
|||||||
if (!this.isHistoryLoaded()) {
|
if (!this.isHistoryLoaded()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hitems = this.getFilteredHistoryItems();
|
const hitems = this.filteredHistoryItems;
|
||||||
let idx = this.historyIndex.get() + amt;
|
let idx = this.historyIndex.get() + amt;
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
idx = 0;
|
idx = 0;
|
||||||
@ -535,11 +516,10 @@ class InputModel {
|
|||||||
this.setHistoryIndex(idx);
|
this.setHistoryIndex(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
flashInfoMsg(info: InfoType, timeoutMs: number): void {
|
flashInfoMsg(info: InfoType, timeoutMs: number): void {
|
||||||
this._clearInfoTimeout();
|
this._clearInfoTimeout();
|
||||||
mobx.action(() => {
|
this.infoMsg.set(info);
|
||||||
this.infoMsg.set(info);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (info == null && this.getActiveAuxView() == appconst.InputAuxView_Info) {
|
if (info == null && this.getActiveAuxView() == appconst.InputAuxView_Info) {
|
||||||
this.setActiveAuxView(null);
|
this.setActiveAuxView(null);
|
||||||
@ -578,7 +558,7 @@ class InputModel {
|
|||||||
) {
|
) {
|
||||||
const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
|
const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
|
||||||
const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline
|
const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline
|
||||||
this.setCurLine(codeText);
|
this.curLine = codeText;
|
||||||
this.giveFocus();
|
this.giveFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -594,72 +574,69 @@ class InputModel {
|
|||||||
return rtn;
|
return rtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
setCodeSelectSelectedCodeBlock(blockIndex: number) {
|
setCodeSelectSelectedCodeBlock(blockIndex: number) {
|
||||||
mobx.action(() => {
|
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
|
||||||
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
|
this.codeSelectSelectedIndex.set(blockIndex);
|
||||||
this.codeSelectSelectedIndex.set(blockIndex);
|
const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
||||||
const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
if (currentRef != null && this.aiChatWindowRef?.current != null) {
|
||||||
if (currentRef != null && this.aiChatWindowRef?.current != null) {
|
const chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
||||||
const chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
||||||
const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
const elemTop = currentRef.offsetTop;
|
||||||
const elemTop = currentRef.offsetTop;
|
let elemBottom = elemTop - currentRef.offsetHeight;
|
||||||
let elemBottom = elemTop - currentRef.offsetHeight;
|
const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
|
||||||
const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
|
if (!elementIsInView) {
|
||||||
if (!elementIsInView) {
|
this.aiChatWindowRef.current.scrollTop = elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
|
||||||
this.aiChatWindowRef.current.scrollTop =
|
|
||||||
elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.codeSelectBlockRefArray = [];
|
}
|
||||||
this.setAIChatFocus();
|
this.codeSelectBlockRefArray = [];
|
||||||
})();
|
this.setActiveAuxView(appconst.InputAuxView_AIChat);
|
||||||
|
this.setAuxViewFocus(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
codeSelectSelectNextNewestCodeBlock() {
|
codeSelectSelectNextNewestCodeBlock() {
|
||||||
// oldest code block = index 0 in array
|
// oldest code block = index 0 in array
|
||||||
// this decrements codeSelectSelected index
|
// this decrements codeSelectSelected index
|
||||||
mobx.action(() => {
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
|
||||||
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
|
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||||
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
return;
|
||||||
return;
|
}
|
||||||
|
const incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
|
||||||
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
|
||||||
|
this.codeSelectDeselectAll();
|
||||||
|
if (this.aiChatWindowRef?.current != null) {
|
||||||
|
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
const incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
|
}
|
||||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
|
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
|
||||||
this.codeSelectDeselectAll();
|
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
|
||||||
if (this.aiChatWindowRef?.current != null) {
|
}
|
||||||
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
|
|
||||||
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
codeSelectSelectNextOldestCodeBlock() {
|
codeSelectSelectNextOldestCodeBlock() {
|
||||||
mobx.action(() => {
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
if (this.codeSelectBlockRefArray.length > 0) {
|
||||||
if (this.codeSelectBlockRefArray.length > 0) {
|
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
|
||||||
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
|
} else {
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||||
if (decBlockIndex < 0) {
|
return;
|
||||||
this.codeSelectDeselectAll(this.codeSelectTop);
|
}
|
||||||
if (this.aiChatWindowRef?.current != null) {
|
const decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
||||||
this.aiChatWindowRef.current.scrollTop = 0;
|
if (decBlockIndex < 0) {
|
||||||
}
|
this.codeSelectDeselectAll(this.codeSelectTop);
|
||||||
|
if (this.aiChatWindowRef?.current != null) {
|
||||||
|
this.aiChatWindowRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) {
|
}
|
||||||
this.setCodeSelectSelectedCodeBlock(decBlockIndex);
|
if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) {
|
||||||
}
|
this.setCodeSelectSelectedCodeBlock(decBlockIndex);
|
||||||
})();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCodeSelectSelectedIndex() {
|
getCodeSelectSelectedIndex() {
|
||||||
@ -684,6 +661,7 @@ class InputModel {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
openAIAssistantChat(): void {
|
openAIAssistantChat(): void {
|
||||||
this.setActiveAuxView(appconst.InputAuxView_AIChat);
|
this.setActiveAuxView(appconst.InputAuxView_AIChat);
|
||||||
this.setAuxViewFocus(true);
|
this.setAuxViewFocus(true);
|
||||||
@ -723,19 +701,19 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
clearInfoMsg(setNull: boolean): void {
|
clearInfoMsg(setNull: boolean): void {
|
||||||
this._clearInfoTimeout();
|
this._clearInfoTimeout();
|
||||||
|
|
||||||
if (this.getActiveAuxView() == appconst.InputAuxView_Info) {
|
if (this.getActiveAuxView() == appconst.InputAuxView_Info) {
|
||||||
this.setActiveAuxView(null);
|
this.setActiveAuxView(null);
|
||||||
}
|
}
|
||||||
mobx.action(() => {
|
if (setNull) {
|
||||||
if (setNull) {
|
this.infoMsg.set(null);
|
||||||
this.infoMsg.set(null);
|
}
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
toggleInfoMsg(): void {
|
toggleInfoMsg(): void {
|
||||||
this._clearInfoTimeout();
|
this._clearInfoTimeout();
|
||||||
if (this.activeAuxView.get() == appconst.InputAuxView_Info) {
|
if (this.activeAuxView.get() == appconst.InputAuxView_Info) {
|
||||||
@ -745,65 +723,51 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
|
||||||
uiSubmitCommand(): void {
|
uiSubmitCommand(): void {
|
||||||
|
const commandStr = this.curLine;
|
||||||
|
if (commandStr.trim() == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
const commandStr = this.getCurLine();
|
|
||||||
if (commandStr.trim() == "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.resetInput();
|
this.resetInput();
|
||||||
this.globalModel.submitRawCommand(commandStr, true, true);
|
|
||||||
})();
|
})();
|
||||||
|
this.globalModel.submitRawCommand(commandStr, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.getCurLine().trim() == "";
|
return this.curLine.trim() == "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
resetInputMode(): void {
|
resetInputMode(): void {
|
||||||
mobx.action(() => {
|
this.setInputMode(null);
|
||||||
this.setInputMode(null);
|
this.curLine = "";
|
||||||
this.setCurLine("");
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurLine(val: string): void {
|
|
||||||
const hidx = this.historyIndex.get();
|
|
||||||
mobx.action(() => {
|
|
||||||
if (this.modHistory.length <= hidx) {
|
|
||||||
this.modHistory.length = hidx + 1;
|
|
||||||
}
|
|
||||||
this.modHistory[hidx] = val;
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
resetInput(): void {
|
resetInput(): void {
|
||||||
mobx.action(() => {
|
this.setActiveAuxView(null);
|
||||||
this.setActiveAuxView(null);
|
this.inputMode.set(null);
|
||||||
this.inputMode.set(null);
|
this.resetHistory();
|
||||||
this.resetHistory();
|
this.dropModHistory(false);
|
||||||
this.dropModHistory(false);
|
this.infoMsg.set(null);
|
||||||
this.infoMsg.set(null);
|
this.inputExpanded.set(false);
|
||||||
this.inputExpanded.set(false);
|
this._clearInfoTimeout();
|
||||||
this._clearInfoTimeout();
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@mobx.action
|
||||||
toggleExpandInput(): void {
|
toggleExpandInput(): void {
|
||||||
mobx.action(() => {
|
this.inputExpanded.set(!this.inputExpanded.get());
|
||||||
this.inputExpanded.set(!this.inputExpanded.get());
|
this.forceInputFocus = true;
|
||||||
this.forceInputFocus = true;
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurLine(): string {
|
@mobx.computed
|
||||||
|
get curLine(): string {
|
||||||
const hidx = this.historyIndex.get();
|
const hidx = this.historyIndex.get();
|
||||||
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
|
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
|
||||||
return this.modHistory[hidx];
|
return this.modHistory[hidx];
|
||||||
}
|
}
|
||||||
const hitems = this.getFilteredHistoryItems();
|
const hitems = this.filteredHistoryItems;
|
||||||
if (hidx == 0 || hitems == null || hidx > hitems.length) {
|
if (hidx == 0 || hitems == null || hidx > hitems.length) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -814,31 +778,40 @@ class InputModel {
|
|||||||
return hitem.cmdstr;
|
return hitem.cmdstr;
|
||||||
}
|
}
|
||||||
|
|
||||||
dropModHistory(keepLine0: boolean): void {
|
set curLine(val: string) {
|
||||||
|
this.lastCurLine = this.curLine;
|
||||||
|
const hidx = this.historyIndex.get();
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
if (keepLine0) {
|
if (this.modHistory.length <= hidx) {
|
||||||
if (this.modHistory.length > 1) {
|
this.modHistory.length = hidx + 1;
|
||||||
this.modHistory.splice(1, this.modHistory.length - 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.modHistory.replace([""]);
|
|
||||||
}
|
}
|
||||||
|
this.modHistory[hidx] = val;
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetHistory(): void {
|
@mobx.action
|
||||||
mobx.action(() => {
|
dropModHistory(keepLine0: boolean): void {
|
||||||
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
if (keepLine0) {
|
||||||
this.setActiveAuxView(null);
|
if (this.modHistory.length > 1) {
|
||||||
|
this.modHistory.splice(1, this.modHistory.length - 1);
|
||||||
}
|
}
|
||||||
this.historyLoading.set(false);
|
} else {
|
||||||
this.historyType.set("screen");
|
this.modHistory.replace([""]);
|
||||||
this.historyItems.set(null);
|
}
|
||||||
this.historyIndex.set(0);
|
}
|
||||||
this.historyQueryOpts.set(getDefaultHistoryQueryOpts());
|
|
||||||
this.historyAfterLoadIndex = 0;
|
@mobx.action
|
||||||
this.dropModHistory(true);
|
resetHistory(): void {
|
||||||
})();
|
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
||||||
|
this.setActiveAuxView(null);
|
||||||
|
}
|
||||||
|
this.historyLoading.set(false);
|
||||||
|
this.historyType.set("screen");
|
||||||
|
this.historyItems.set(null);
|
||||||
|
this.historyIndex.set(0);
|
||||||
|
this.historyQueryOpts.set(getDefaultHistoryQueryOpts());
|
||||||
|
this.historyAfterLoadIndex = 0;
|
||||||
|
this.dropModHistory(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ import { GlobalCommandRunner } from "./global";
|
|||||||
import { clearMonoFontCache, getMonoFontSize } from "@/util/textmeasure";
|
import { clearMonoFontCache, getMonoFontSize } from "@/util/textmeasure";
|
||||||
import type { TermWrap } from "@/plugins/terminal/term";
|
import type { TermWrap } from "@/plugins/terminal/term";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import { url } from "node:inspector";
|
|
||||||
|
|
||||||
type SWLinePtr = {
|
type SWLinePtr = {
|
||||||
line: LineType;
|
line: LineType;
|
||||||
@ -1327,7 +1326,21 @@ class Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
|
/**
|
||||||
|
* Submits a command packet to the server and processes the response.
|
||||||
|
* @param cmdPk The command packet to submit.
|
||||||
|
* @param interactive Whether the command is interactive.
|
||||||
|
* @param runUpdate Whether to run the update after the command is submitted. If true, the update will be processed and the frontend will be updated. If false, the update will be returned in the promise.
|
||||||
|
* @returns A promise that resolves to a CommandRtnType.
|
||||||
|
* @throws An error if the command fails.
|
||||||
|
* @see CommandRtnType
|
||||||
|
* @see FeCmdPacketType
|
||||||
|
**/
|
||||||
|
submitCommandPacket(
|
||||||
|
cmdPk: FeCmdPacketType,
|
||||||
|
interactive: boolean,
|
||||||
|
runUpdate: boolean = true
|
||||||
|
): Promise<CommandRtnType> {
|
||||||
if (this.debugCmds > 0) {
|
if (this.debugCmds > 0) {
|
||||||
console.log("[cmd]", cmdPacketString(cmdPk));
|
console.log("[cmd]", cmdPacketString(cmdPk));
|
||||||
if (this.debugCmds > 1) {
|
if (this.debugCmds > 1) {
|
||||||
@ -1345,16 +1358,20 @@ class Model {
|
|||||||
})
|
})
|
||||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
mobx.action(() => {
|
return mobx.action(() => {
|
||||||
const update = data.data;
|
const update = data.data;
|
||||||
if (update != null) {
|
if (update != null) {
|
||||||
this.runUpdate(update, interactive);
|
if (runUpdate) {
|
||||||
|
this.runUpdate(update, interactive);
|
||||||
|
} else {
|
||||||
|
return { success: true, update: update };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (interactive && !this.isInfoUpdate(update)) {
|
if (interactive && !this.isInfoUpdate(update)) {
|
||||||
this.inputModel.clearInfoMsg(true);
|
this.inputModel.clearInfoMsg(true);
|
||||||
}
|
}
|
||||||
|
return { success: true };
|
||||||
})();
|
})();
|
||||||
return { success: true };
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.errorHandler("calling run-command", err, interactive);
|
this.errorHandler("calling run-command", err, interactive);
|
||||||
@ -1367,12 +1384,23 @@ class Model {
|
|||||||
return prtn;
|
return prtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a command to the server and processes the response.
|
||||||
|
* @param metaCmd The meta command to run.
|
||||||
|
* @param metaSubCmd The meta subcommand to run.
|
||||||
|
* @param args The arguments to pass to the command.
|
||||||
|
* @param kwargs The keyword arguments to pass to the command.
|
||||||
|
* @param interactive Whether the command is interactive.
|
||||||
|
* @param runUpdate Whether to run the update after the command is submitted. If true, the update will be processed and the frontend will be updated. If false, the update will be returned in the promise.
|
||||||
|
* @returns A promise that resolves to a CommandRtnType.
|
||||||
|
*/
|
||||||
submitCommand(
|
submitCommand(
|
||||||
metaCmd: string,
|
metaCmd: string,
|
||||||
metaSubCmd: string,
|
metaSubCmd: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
kwargs: Record<string, string>,
|
kwargs: Record<string, string>,
|
||||||
interactive: boolean
|
interactive: boolean,
|
||||||
|
runUpdate: boolean = true
|
||||||
): Promise<CommandRtnType> {
|
): Promise<CommandRtnType> {
|
||||||
const pk: FeCmdPacketType = {
|
const pk: FeCmdPacketType = {
|
||||||
type: "fecmd",
|
type: "fecmd",
|
||||||
@ -1393,7 +1421,7 @@ class Model {
|
|||||||
pk.interactive
|
pk.interactive
|
||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
return this.submitCommandPacket(pk, interactive);
|
return this.submitCommandPacket(pk, interactive, runUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSingleEphemeralCommandOutput(url: URL): Promise<string> {
|
getSingleEphemeralCommandOutput(url: URL): Promise<string> {
|
||||||
@ -1412,12 +1440,10 @@ class Model {
|
|||||||
let stderr = "";
|
let stderr = "";
|
||||||
if (ephemeralCommandResponse.stdouturl) {
|
if (ephemeralCommandResponse.stdouturl) {
|
||||||
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stdouturl);
|
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stdouturl);
|
||||||
console.log("stdouturl", url);
|
|
||||||
stdout = await this.getSingleEphemeralCommandOutput(url);
|
stdout = await this.getSingleEphemeralCommandOutput(url);
|
||||||
}
|
}
|
||||||
if (ephemeralCommandResponse.stderrurl) {
|
if (ephemeralCommandResponse.stderrurl) {
|
||||||
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stderrurl);
|
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stderrurl);
|
||||||
console.log("stderrurl", url);
|
|
||||||
stderr = await this.getSingleEphemeralCommandOutput(url);
|
stderr = await this.getSingleEphemeralCommandOutput(url);
|
||||||
}
|
}
|
||||||
return { stdout: stdout, stderr: stderr };
|
return { stdout: stdout, stderr: stderr };
|
||||||
@ -1476,14 +1502,14 @@ class Model {
|
|||||||
interactive: interactive,
|
interactive: interactive,
|
||||||
ephemeralopts: ephemeralopts,
|
ephemeralopts: ephemeralopts,
|
||||||
};
|
};
|
||||||
console.log(
|
// console.log(
|
||||||
"CMD",
|
// "CMD",
|
||||||
pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
|
// pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
|
||||||
pk.args,
|
// pk.args,
|
||||||
pk.kwargs,
|
// pk.kwargs,
|
||||||
pk.interactive,
|
// pk.interactive,
|
||||||
pk.ephemeralopts
|
// pk.ephemeralopts
|
||||||
);
|
// );
|
||||||
return this.submitEphemeralCommandPacket(pk, interactive);
|
return this.submitEphemeralCommandPacket(pk, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
src/types/custom.d.ts
vendored
1
src/types/custom.d.ts
vendored
@ -821,6 +821,7 @@ declare global {
|
|||||||
type CommandRtnType = {
|
type CommandRtnType = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
update?: UpdatePacket;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EphemeralCommandOutputType = {
|
type EphemeralCommandOutputType = {
|
||||||
|
@ -833,6 +833,7 @@ type RunPacketType struct {
|
|||||||
Detached bool `json:"detached,omitempty"`
|
Detached bool `json:"detached,omitempty"`
|
||||||
ReturnState bool `json:"returnstate,omitempty"`
|
ReturnState bool `json:"returnstate,omitempty"`
|
||||||
IsSudo bool `json:"issudo,omitempty"`
|
IsSudo bool `json:"issudo,omitempty"`
|
||||||
|
Timeout time.Duration `json:"timeout"` // TODO: added vnext. This is the timeout for the command to run. If the command does not complete in this time, it will be killed. The default zero value will not impose a timeout.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*RunPacketType) GetType() string {
|
func (*RunPacketType) GetType() string {
|
||||||
|
@ -926,12 +926,11 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
|
|||||||
rcFileName = fmt.Sprintf("/dev/fd/%d", rcFileFdNum)
|
rcFileName = fmt.Sprintf("/dev/fd/%d", rcFileFdNum)
|
||||||
}
|
}
|
||||||
if cmd.TmpRcFileName != "" {
|
if cmd.TmpRcFileName != "" {
|
||||||
go func() {
|
time.AfterFunc(2*time.Second, func() {
|
||||||
// cmd.Close() will also remove rcFileName
|
// cmd.Close() will also remove rcFileName
|
||||||
// adding this to also try to proactively clean up after 2-seconds.
|
// adding this to also try to proactively clean up after 2-seconds.
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
os.Remove(cmd.TmpRcFileName)
|
os.Remove(cmd.TmpRcFileName)
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
fullCmdStr := pk.Command
|
fullCmdStr := pk.Command
|
||||||
if pk.ReturnState {
|
if pk.ReturnState {
|
||||||
@ -1109,6 +1108,14 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pk.Timeout > 0 {
|
||||||
|
// Cancel the command if it takes too long
|
||||||
|
time.AfterFunc(pk.Timeout, func() {
|
||||||
|
cmd.Cmd.Cancel()
|
||||||
|
cmd.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
return cmd, nil
|
return cmd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ go 1.22
|
|||||||
toolchain go1.22.0
|
toolchain go1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
|
||||||
github.com/alessio/shellescape v1.4.1
|
github.com/alessio/shellescape v1.4.1
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
||||||
github.com/creack/pty v1.1.18
|
github.com/creack/pty v1.1.18
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
|
||||||
|
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
|
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
|
||||||
@ -55,6 +57,7 @@ github.com/sawka/txwrap v0.1.2 h1:v8xS0Z1LE7/6vMZA81PYihI+0TSR6Zm1MalzzBIuXKc=
|
|||||||
github.com/sawka/txwrap v0.1.2/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw=
|
github.com/sawka/txwrap v0.1.2/go.mod h1:T3nlw2gVpuolo6/XEetvBbk1oMXnY978YmBFy1UyHvw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY=
|
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY=
|
||||||
@ -71,6 +74,8 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
|
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
|
||||||
|
623
wavesrv/pkg/blockstore/blockstore.go
Normal file
623
wavesrv/pkg/blockstore/blockstore.go
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
package blockstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/units"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileOptsType struct {
|
||||||
|
MaxSize int64
|
||||||
|
Circular bool
|
||||||
|
IJson bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMeta = map[string]any
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
BlockId string
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
CreatedTs int64
|
||||||
|
ModTs int64
|
||||||
|
Opts FileOptsType
|
||||||
|
Meta FileMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
const MaxBlockSize = int64(128 * units.Kilobyte)
|
||||||
|
const DefaultFlushTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
type CacheEntry struct {
|
||||||
|
Lock *sync.Mutex
|
||||||
|
CacheTs int64
|
||||||
|
Info *FileInfo
|
||||||
|
DataBlocks []*CacheBlock
|
||||||
|
Refs int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheEntry) IncRefs() {
|
||||||
|
c.Refs += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheEntry) DecRefs() {
|
||||||
|
c.Refs -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type CacheBlock struct {
|
||||||
|
data []byte
|
||||||
|
size int
|
||||||
|
dirty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeCacheEntry(info *FileInfo) *CacheEntry {
|
||||||
|
rtn := &CacheEntry{Lock: &sync.Mutex{}, CacheTs: int64(time.Now().UnixMilli()), Info: info, DataBlocks: []*CacheBlock{}, Refs: 0}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
// add ctx context.Context to all these methods
|
||||||
|
type BlockStore interface {
|
||||||
|
MakeFile(ctx context.Context, blockId string, name string, meta FileMeta, opts FileOptsType) error
|
||||||
|
WriteFile(ctx context.Context, blockId string, name string, meta FileMeta, opts FileOptsType, data []byte) (int, error)
|
||||||
|
AppendData(ctx context.Context, blockId string, name string, p []byte) (int, error)
|
||||||
|
WriteAt(ctx context.Context, blockId string, name string, p []byte, off int64) (int, error)
|
||||||
|
ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int64) (int, error)
|
||||||
|
Stat(ctx context.Context, blockId string, name string) (FileInfo, error)
|
||||||
|
CollapseIJson(ctx context.Context, blockId string, name string) error
|
||||||
|
WriteMeta(ctx context.Context, blockId string, name string, meta FileMeta) error
|
||||||
|
DeleteFile(ctx context.Context, blockId string, name string) error
|
||||||
|
DeleteBlock(ctx context.Context, blockId string) error
|
||||||
|
ListFiles(ctx context.Context, blockId string) []*FileInfo
|
||||||
|
FlushCache(ctx context.Context) error
|
||||||
|
GetAllBlockIds(ctx context.Context) []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache map[string]*CacheEntry = make(map[string]*CacheEntry)
|
||||||
|
var globalLock *sync.Mutex = &sync.Mutex{}
|
||||||
|
var appendLock *sync.Mutex = &sync.Mutex{}
|
||||||
|
var flushTimeout = DefaultFlushTimeout
|
||||||
|
var lastWriteTime time.Time
|
||||||
|
|
||||||
|
func InsertFileIntoDB(ctx context.Context, fileInfo FileInfo) error {
|
||||||
|
metaJson, err := json.Marshal(fileInfo.Meta)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, err)
|
||||||
|
}
|
||||||
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `INSERT INTO block_file VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
tx.Exec(query, fileInfo.BlockId, fileInfo.Name, fileInfo.Opts.MaxSize, fileInfo.Opts.Circular, fileInfo.Size, fileInfo.CreatedTs, fileInfo.ModTs, metaJson)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, txErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFileToDB(ctx context.Context, fileInfo FileInfo) error {
|
||||||
|
metaJson, err := json.Marshal(fileInfo.Meta)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, err)
|
||||||
|
}
|
||||||
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `UPDATE block_file SET blockid = ?, name = ?, maxsize = ?, circular = ?, size = ?, createdts = ?, modts = ?, meta = ? where blockid = ? and name = ?`
|
||||||
|
tx.Exec(query, fileInfo.BlockId, fileInfo.Name, fileInfo.Opts.MaxSize, fileInfo.Opts.Circular, fileInfo.Size, fileInfo.CreatedTs, fileInfo.ModTs, metaJson, fileInfo.BlockId, fileInfo.Name)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return fmt.Errorf("Error writing file %s to db: %v", fileInfo.Name, txErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteDataBlockToDB(ctx context.Context, blockId string, name string, index int, data []byte) error {
|
||||||
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `REPLACE INTO block_data values (?, ?, ?, ?)`
|
||||||
|
tx.Exec(query, blockId, name, index, data)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return fmt.Errorf("Error writing data block to db: %v", txErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeFile(ctx context.Context, blockId string, name string, meta FileMeta, opts FileOptsType) error {
|
||||||
|
curTs := time.Now().UnixMilli()
|
||||||
|
fileInfo := FileInfo{BlockId: blockId, Name: name, Size: 0, CreatedTs: curTs, ModTs: curTs, Opts: opts, Meta: meta}
|
||||||
|
err := InsertFileIntoDB(ctx, fileInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
curCacheEntry := MakeCacheEntry(&fileInfo)
|
||||||
|
SetCacheEntry(ctx, GetCacheId(blockId, name), curCacheEntry)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteToCacheBlockNum(ctx context.Context, blockId string, name string, p []byte, pos int, length int, cacheNum int, pullFromDB bool) (int64, int, error) {
|
||||||
|
cacheEntry, err := GetCacheEntryOrPopulate(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
cacheEntry.IncRefs()
|
||||||
|
cacheEntry.Lock.Lock()
|
||||||
|
defer cacheEntry.Lock.Unlock()
|
||||||
|
block, err := GetCacheBlock(ctx, blockId, name, cacheNum, pullFromDB)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("Error getting cache block: %v", err)
|
||||||
|
}
|
||||||
|
var bytesWritten = 0
|
||||||
|
blockLen := len(block.data)
|
||||||
|
fileMaxSize := cacheEntry.Info.Opts.MaxSize
|
||||||
|
maxWriteSize := fileMaxSize - (int64(cacheNum) * MaxBlockSize)
|
||||||
|
numLeftPad := int64(0)
|
||||||
|
if pos > blockLen {
|
||||||
|
numLeftPad = int64(pos - blockLen)
|
||||||
|
leftPadBytes := []byte{}
|
||||||
|
for index := 0; index < int(numLeftPad); index++ {
|
||||||
|
leftPadBytes = append(leftPadBytes, 0)
|
||||||
|
}
|
||||||
|
leftPadPos := int64(pos) - numLeftPad
|
||||||
|
b, err := WriteToCacheBuf(&block.data, leftPadBytes, int(leftPadPos), int(numLeftPad), maxWriteSize)
|
||||||
|
if err != nil {
|
||||||
|
return int64(b), b, err
|
||||||
|
}
|
||||||
|
numLeftPad = int64(b)
|
||||||
|
cacheEntry.Info.Size += (int64(cacheNum) * MaxBlockSize)
|
||||||
|
}
|
||||||
|
b, writeErr := WriteToCacheBuf(&block.data, p, pos, length, maxWriteSize)
|
||||||
|
bytesWritten += b
|
||||||
|
blockLenDiff := len(block.data) - blockLen
|
||||||
|
block.size = len(block.data)
|
||||||
|
cacheEntry.Info.Size += int64(blockLenDiff)
|
||||||
|
block.dirty = true
|
||||||
|
cacheEntry.DecRefs()
|
||||||
|
return numLeftPad, bytesWritten, writeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFromCacheBlock(ctx context.Context, blockId string, name string, block *CacheBlock, p *[]byte, pos int, length int, destOffset int, maxRead int64) (int, error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("recovered from crash %v ", r)
|
||||||
|
log.Printf("values: %v %v %v %v %v %v", pos, length, destOffset, maxRead, p, block)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if pos > len(block.data) {
|
||||||
|
return 0, fmt.Errorf("Reading past end of cache block, should never happen")
|
||||||
|
}
|
||||||
|
bytesWritten := 0
|
||||||
|
index := pos
|
||||||
|
for ; index < length+pos; index++ {
|
||||||
|
if int64(index) >= maxRead {
|
||||||
|
return index - pos, fmt.Errorf(MaxSizeError)
|
||||||
|
}
|
||||||
|
if index >= len(block.data) {
|
||||||
|
return bytesWritten, nil
|
||||||
|
}
|
||||||
|
destIndex := index - pos + destOffset
|
||||||
|
if destIndex >= len(*p) {
|
||||||
|
return bytesWritten, nil
|
||||||
|
}
|
||||||
|
(*p)[destIndex] = block.data[index]
|
||||||
|
bytesWritten++
|
||||||
|
}
|
||||||
|
if int64(index) >= maxRead {
|
||||||
|
return bytesWritten, fmt.Errorf(MaxSizeError)
|
||||||
|
}
|
||||||
|
return bytesWritten, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const MaxSizeError = "Hit Max Size"
|
||||||
|
|
||||||
|
func WriteToCacheBuf(buf *[]byte, p []byte, pos int, length int, maxWrite int64) (int, error) {
|
||||||
|
bytesToWrite := length
|
||||||
|
if pos > len(*buf) {
|
||||||
|
return 0, fmt.Errorf("writing to a position (%v) in the cache that doesn't exist yet, something went wrong", pos)
|
||||||
|
}
|
||||||
|
if int64(pos+bytesToWrite) > MaxBlockSize {
|
||||||
|
return 0, fmt.Errorf("writing more bytes than max block size, not allowed - length of bytes to write: %v, length of cache: %v", bytesToWrite, len(*buf))
|
||||||
|
}
|
||||||
|
for index := pos; index < bytesToWrite+pos; index++ {
|
||||||
|
if index-pos >= len(p) {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
if int64(index) >= maxWrite {
|
||||||
|
return index - pos, fmt.Errorf(MaxSizeError)
|
||||||
|
}
|
||||||
|
curByte := p[index-pos]
|
||||||
|
if len(*buf) == index {
|
||||||
|
*buf = append(*buf, curByte)
|
||||||
|
} else {
|
||||||
|
(*buf)[index] = curByte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytesToWrite, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCacheId(blockId string, name string) string {
|
||||||
|
return blockId + "~SEP~" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetValuesFromCacheId(cacheId string) (blockId string, name string) {
|
||||||
|
vals := strings.Split(cacheId, "~SEP~")
|
||||||
|
if len(vals) == 2 {
|
||||||
|
return vals[0], vals[1]
|
||||||
|
} else {
|
||||||
|
log.Println("Failure in GetValuesFromCacheId, this should never happen")
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCacheEntry(ctx context.Context, blockId string, name string) (*CacheEntry, bool) {
|
||||||
|
globalLock.Lock()
|
||||||
|
defer globalLock.Unlock()
|
||||||
|
if curCacheEntry, found := cache[GetCacheId(blockId, name)]; found {
|
||||||
|
return curCacheEntry, true
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCacheEntryOrPopulate(ctx context.Context, blockId string, name string) (*CacheEntry, error) {
|
||||||
|
if cacheEntry, found := GetCacheEntry(ctx, blockId, name); found {
|
||||||
|
return cacheEntry, nil
|
||||||
|
} else {
|
||||||
|
log.Printf("populating cache entry\n")
|
||||||
|
_, err := Stat(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cacheEntry, found := GetCacheEntry(ctx, blockId, name); found {
|
||||||
|
return cacheEntry, nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Error getting cache entry %v %v", blockId, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetCacheEntry(ctx context.Context, cacheId string, cacheEntry *CacheEntry) {
|
||||||
|
globalLock.Lock()
|
||||||
|
defer globalLock.Unlock()
|
||||||
|
if _, found := cache[cacheId]; found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cache[cacheId] = cacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCacheEntry(ctx context.Context, blockId string, name string) {
|
||||||
|
globalLock.Lock()
|
||||||
|
defer globalLock.Unlock()
|
||||||
|
delete(cache, GetCacheId(blockId, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCacheBlock(ctx context.Context, blockId string, name string, cacheNum int, pullFromDB bool) (*CacheBlock, error) {
|
||||||
|
curCacheEntry, err := GetCacheEntryOrPopulate(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(curCacheEntry.DataBlocks) < cacheNum+1 {
|
||||||
|
for index := len(curCacheEntry.DataBlocks); index < cacheNum+1; index++ {
|
||||||
|
curCacheEntry.DataBlocks = append(curCacheEntry.DataBlocks, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if curCacheEntry.DataBlocks[cacheNum] == nil {
|
||||||
|
var curCacheBlock *CacheBlock
|
||||||
|
if pullFromDB {
|
||||||
|
cacheData, err := GetCacheFromDB(ctx, blockId, name, 0, MaxBlockSize, int64(cacheNum))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
curCacheBlock = &CacheBlock{data: *cacheData, size: len(*cacheData), dirty: false}
|
||||||
|
curCacheEntry.DataBlocks[cacheNum] = curCacheBlock
|
||||||
|
} else {
|
||||||
|
curCacheBlock = &CacheBlock{data: []byte{}, size: 0, dirty: false}
|
||||||
|
curCacheEntry.DataBlocks[cacheNum] = curCacheBlock
|
||||||
|
}
|
||||||
|
return curCacheBlock, nil
|
||||||
|
} else {
|
||||||
|
return curCacheEntry.DataBlocks[cacheNum], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeepCopyFileInfo(fInfo *FileInfo) *FileInfo {
|
||||||
|
fInfoMeta := make(FileMeta)
|
||||||
|
for k, v := range fInfo.Meta {
|
||||||
|
fInfoMeta[k] = v
|
||||||
|
}
|
||||||
|
fInfoOpts := fInfo.Opts
|
||||||
|
fInfoCopy := &FileInfo{BlockId: fInfo.BlockId, Name: fInfo.Name, Size: fInfo.Size, CreatedTs: fInfo.CreatedTs, ModTs: fInfo.ModTs, Opts: fInfoOpts, Meta: fInfoMeta}
|
||||||
|
return fInfoCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stat(ctx context.Context, blockId string, name string) (*FileInfo, error) {
|
||||||
|
cacheEntry, found := GetCacheEntry(ctx, blockId, name)
|
||||||
|
if found {
|
||||||
|
return DeepCopyFileInfo(cacheEntry.Info), nil
|
||||||
|
}
|
||||||
|
curCacheEntry := MakeCacheEntry(nil)
|
||||||
|
curCacheEntry.Lock.Lock()
|
||||||
|
defer curCacheEntry.Lock.Unlock()
|
||||||
|
fInfo, err := GetFileInfo(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
curCacheEntry.Info = fInfo
|
||||||
|
SetCacheEntry(ctx, GetCacheId(blockId, name), curCacheEntry)
|
||||||
|
return DeepCopyFileInfo(fInfo), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFlushTimeout(newTimeout time.Duration) {
|
||||||
|
flushTimeout = newTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClockString(t time.Time) string {
|
||||||
|
hour, min, sec := t.Clock()
|
||||||
|
return fmt.Sprintf("%v:%v:%v", hour, min, sec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartFlushTimer(ctx context.Context) {
|
||||||
|
curTime := time.Now()
|
||||||
|
writeTimePassed := curTime.UnixNano() - lastWriteTime.UnixNano()
|
||||||
|
if writeTimePassed >= int64(flushTimeout) {
|
||||||
|
lastWriteTime = curTime
|
||||||
|
go func() {
|
||||||
|
time.Sleep(flushTimeout)
|
||||||
|
FlushCache(ctx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteAt(ctx context.Context, blockId string, name string, p []byte, off int64) (int, error) {
|
||||||
|
return WriteAtHelper(ctx, blockId, name, p, off, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteAtHelper(ctx context.Context, blockId string, name string, p []byte, off int64, flushCache bool) (int, error) {
|
||||||
|
bytesToWrite := len(p)
|
||||||
|
bytesWritten := 0
|
||||||
|
curCacheNum := int(math.Floor(float64(off) / float64(MaxBlockSize)))
|
||||||
|
numCaches := int(math.Ceil(float64(bytesToWrite) / float64(MaxBlockSize)))
|
||||||
|
cacheOffset := off - (int64(curCacheNum) * MaxBlockSize)
|
||||||
|
if (cacheOffset + int64(bytesToWrite)) > MaxBlockSize {
|
||||||
|
numCaches += 1
|
||||||
|
}
|
||||||
|
fInfo, err := Stat(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("Write At err: %v", err)
|
||||||
|
}
|
||||||
|
if off > fInfo.Opts.MaxSize && fInfo.Opts.Circular {
|
||||||
|
numOver := off / fInfo.Opts.MaxSize
|
||||||
|
off = off - (numOver * fInfo.Opts.MaxSize)
|
||||||
|
}
|
||||||
|
for index := curCacheNum; index < curCacheNum+numCaches; index++ {
|
||||||
|
cacheOffset := off - (int64(index) * MaxBlockSize)
|
||||||
|
bytesToWriteToCurCache := int(math.Min(float64(bytesToWrite), float64(MaxBlockSize-cacheOffset)))
|
||||||
|
pullFromDB := true
|
||||||
|
if cacheOffset == 0 && int64(bytesToWriteToCurCache) == MaxBlockSize {
|
||||||
|
pullFromDB = false
|
||||||
|
}
|
||||||
|
_, b, err := WriteToCacheBlockNum(ctx, blockId, name, p, int(cacheOffset), bytesToWriteToCurCache, index, pullFromDB)
|
||||||
|
bytesWritten += b
|
||||||
|
bytesToWrite -= b
|
||||||
|
off += int64(b)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == MaxSizeError {
|
||||||
|
if fInfo.Opts.Circular {
|
||||||
|
p = p[int64(b):]
|
||||||
|
b, err := WriteAtHelper(ctx, blockId, name, p, 0, false)
|
||||||
|
bytesWritten += b
|
||||||
|
if err != nil {
|
||||||
|
return bytesWritten, fmt.Errorf("Write to cache error: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return bytesWritten, fmt.Errorf("Write to cache error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(p) == b {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p = p[int64(b):]
|
||||||
|
}
|
||||||
|
if flushCache {
|
||||||
|
StartFlushTimer(ctx)
|
||||||
|
}
|
||||||
|
return bytesWritten, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllBlockSizes(dataBlocks []*CacheBlock) (int, int) {
|
||||||
|
rtn := 0
|
||||||
|
numNil := 0
|
||||||
|
for idx, block := range dataBlocks {
|
||||||
|
if block == nil {
|
||||||
|
numNil += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rtn += block.size
|
||||||
|
if block.size != len(block.data) {
|
||||||
|
log.Printf("error: block %v has incorrect block size : %v %v", idx, block.size, len(block.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn, numNil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlushCache(ctx context.Context) error {
|
||||||
|
for _, cacheEntry := range cache {
|
||||||
|
err := WriteFileToDB(ctx, *cacheEntry.Info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clearEntry := true
|
||||||
|
cacheEntry.Lock.Lock()
|
||||||
|
for index, block := range cacheEntry.DataBlocks {
|
||||||
|
if block == nil || block.size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !block.dirty {
|
||||||
|
clearEntry = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := WriteDataBlockToDB(ctx, cacheEntry.Info.BlockId, cacheEntry.Info.Name, index, block.data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheEntry.DataBlocks[index] = nil
|
||||||
|
}
|
||||||
|
cacheEntry.Lock.Unlock()
|
||||||
|
if clearEntry && cacheEntry.Refs <= 0 {
|
||||||
|
DeleteCacheEntry(ctx, cacheEntry.Info.BlockId, cacheEntry.Info.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadAt(ctx context.Context, blockId string, name string, p *[]byte, off int64) (int, error) {
|
||||||
|
bytesRead := 0
|
||||||
|
fInfo, err := Stat(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("Read At err: %v", err)
|
||||||
|
}
|
||||||
|
if off > fInfo.Opts.MaxSize && fInfo.Opts.Circular {
|
||||||
|
numOver := off / fInfo.Opts.MaxSize
|
||||||
|
off = off - (numOver * fInfo.Opts.MaxSize)
|
||||||
|
}
|
||||||
|
if off > fInfo.Size {
|
||||||
|
return 0, fmt.Errorf("Read At error: tried to read past the end of the file")
|
||||||
|
}
|
||||||
|
endReadPos := math.Min(float64(int64(len(*p))+off), float64(fInfo.Size))
|
||||||
|
bytesToRead := int64(endReadPos) - off
|
||||||
|
curCacheNum := int(math.Floor(float64(off) / float64(MaxBlockSize)))
|
||||||
|
numCaches := int(math.Ceil(float64(bytesToRead) / float64(MaxBlockSize)))
|
||||||
|
cacheOffset := off - (int64(curCacheNum) * MaxBlockSize)
|
||||||
|
if (cacheOffset + int64(bytesToRead)) > MaxBlockSize {
|
||||||
|
numCaches += 1
|
||||||
|
}
|
||||||
|
for index := curCacheNum; index < curCacheNum+numCaches; index++ {
|
||||||
|
curCacheBlock, err := GetCacheBlock(ctx, blockId, name, index, true)
|
||||||
|
if err != nil {
|
||||||
|
return bytesRead, fmt.Errorf("Error getting cache block: %v", err)
|
||||||
|
}
|
||||||
|
cacheOffset := off - (int64(index) * MaxBlockSize)
|
||||||
|
if cacheOffset < 0 {
|
||||||
|
return bytesRead, nil
|
||||||
|
}
|
||||||
|
bytesToReadFromCurCache := int(math.Min(float64(bytesToRead), float64(MaxBlockSize-cacheOffset)))
|
||||||
|
fileMaxSize := fInfo.Opts.MaxSize
|
||||||
|
maxReadSize := fileMaxSize - (int64(index) * MaxBlockSize)
|
||||||
|
b, err := ReadFromCacheBlock(ctx, blockId, name, curCacheBlock, p, int(cacheOffset), bytesToReadFromCurCache, bytesRead, maxReadSize)
|
||||||
|
if b == 0 {
|
||||||
|
log.Printf("something wrong %v %v %v %v %v %v %v %v", index, off, cacheOffset, curCacheNum, numCaches, bytesRead, bytesToRead, curCacheBlock)
|
||||||
|
cacheEntry, _ := GetCacheEntry(ctx, blockId, name)
|
||||||
|
blockSize, numNil := GetAllBlockSizes(cacheEntry.DataBlocks)
|
||||||
|
maybeDBSize := int64(numNil) * MaxBlockSize
|
||||||
|
maybeFullSize := int64(blockSize) + maybeDBSize
|
||||||
|
log.Printf("block actual sizes: %v %v %v %v %v\n", blockSize, numNil, maybeDBSize, maybeFullSize, len(cacheEntry.DataBlocks))
|
||||||
|
}
|
||||||
|
bytesRead += b
|
||||||
|
bytesToRead -= int64(b)
|
||||||
|
off += int64(b)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == MaxSizeError {
|
||||||
|
if fInfo.Opts.Circular {
|
||||||
|
off = 0
|
||||||
|
newP := (*p)[b:]
|
||||||
|
b, err := ReadAt(ctx, blockId, name, &newP, off)
|
||||||
|
bytesRead += b
|
||||||
|
if err != nil {
|
||||||
|
return bytesRead, err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return bytesRead, fmt.Errorf("Read from cache error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytesRead, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendData(ctx context.Context, blockId string, name string, p []byte) (int, error) {
|
||||||
|
appendLock.Lock()
|
||||||
|
defer appendLock.Unlock()
|
||||||
|
fInfo, err := Stat(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("Append stat error: %v", err)
|
||||||
|
}
|
||||||
|
return WriteAt(ctx, blockId, name, p, fInfo.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFile(ctx context.Context, blockId string, name string) error {
|
||||||
|
DeleteCacheEntry(ctx, blockId, name)
|
||||||
|
err := DeleteFileFromDB(ctx, blockId, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteBlock(ctx context.Context, blockId string) error {
|
||||||
|
for cacheId, _ := range cache {
|
||||||
|
curBlockId, name := GetValuesFromCacheId(cacheId)
|
||||||
|
if curBlockId == blockId {
|
||||||
|
err := DeleteFile(ctx, blockId, name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting %v %v: %v", blockId, name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := DeleteBlockFromDB(ctx, blockId)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(ctx context.Context, blockId string, name string, meta FileMeta, opts FileOptsType, data []byte) (int, error) {
|
||||||
|
MakeFile(ctx, blockId, name, meta, opts)
|
||||||
|
return AppendData(ctx, blockId, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteMeta(ctx context.Context, blockId string, name string, meta FileMeta) error {
|
||||||
|
_, err := Stat(ctx, blockId, name)
|
||||||
|
// stat so that we can make sure cache entry is popuplated
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheEntry, found := GetCacheEntry(ctx, blockId, name)
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("WriteAt error: cache entry not found")
|
||||||
|
}
|
||||||
|
cacheEntry.Lock.Lock()
|
||||||
|
defer cacheEntry.Lock.Unlock()
|
||||||
|
cacheEntry.Info.Meta = meta
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListFiles(ctx context.Context, blockId string) []*FileInfo {
|
||||||
|
fInfoArr, err := GetAllFilesInDBForBlockId(ctx, blockId)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fInfoArr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListAllFiles(ctx context.Context) []*FileInfo {
|
||||||
|
fInfoArr, err := GetAllFilesInDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fInfoArr
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllBlockIds(ctx context.Context) []string {
|
||||||
|
rtn, err := GetAllBlockIdsInDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
242
wavesrv/pkg/blockstore/blockstore_dbops.go
Normal file
242
wavesrv/pkg/blockstore/blockstore_dbops.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
package blockstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/sawka/txwrap"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DBFileName = "blockstore.db"
|
||||||
|
|
||||||
|
type SingleConnDBGetter struct {
|
||||||
|
SingleConnLock *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbWrap *SingleConnDBGetter
|
||||||
|
|
||||||
|
type TxWrap = txwrap.TxWrap
|
||||||
|
|
||||||
|
func InitDBState() {
|
||||||
|
dbWrap = &SingleConnDBGetter{SingleConnLock: &sync.Mutex{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *SingleConnDBGetter) GetDB(ctx context.Context) (*sqlx.DB, error) {
|
||||||
|
db, err := GetDB(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dbg.SingleConnLock.Lock()
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *SingleConnDBGetter) ReleaseDB(db *sqlx.DB) {
|
||||||
|
dbg.SingleConnLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error {
|
||||||
|
return txwrap.DBGWithTx(ctx, dbWrap, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) {
|
||||||
|
var rtn RT
|
||||||
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
temp, err := fn(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rtn = temp
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return rtn, txErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalDBLock = &sync.Mutex{}
|
||||||
|
var globalDB *sqlx.DB
|
||||||
|
var globalDBErr error
|
||||||
|
|
||||||
|
func GetDBName() string {
|
||||||
|
scHome := scbase.GetWaveHomeDir()
|
||||||
|
return path.Join(scHome, DBFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDB(ctx context.Context) (*sqlx.DB, error) {
|
||||||
|
if txwrap.IsTxWrapContext(ctx) {
|
||||||
|
return nil, fmt.Errorf("cannot call GetDB from within a running transaction")
|
||||||
|
}
|
||||||
|
globalDBLock.Lock()
|
||||||
|
defer globalDBLock.Unlock()
|
||||||
|
if globalDB == nil && globalDBErr == nil {
|
||||||
|
dbName := GetDBName()
|
||||||
|
globalDB, globalDBErr = sqlx.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL&_busy_timeout=5000", dbName))
|
||||||
|
if globalDBErr != nil {
|
||||||
|
globalDBErr = fmt.Errorf("opening db[%s]: %w", dbName, globalDBErr)
|
||||||
|
log.Printf("[db] error: %v\n", globalDBErr)
|
||||||
|
} else {
|
||||||
|
log.Printf("[db] successfully opened db %s\n", dbName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return globalDB, globalDBErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDB() {
|
||||||
|
globalDBLock.Lock()
|
||||||
|
defer globalDBLock.Unlock()
|
||||||
|
if globalDB == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := globalDB.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[db] error closing database: %v\n", err)
|
||||||
|
}
|
||||||
|
globalDB = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileInfo) ToMap() map[string]interface{} {
|
||||||
|
rtn := make(map[string]interface{})
|
||||||
|
log.Printf("fileInfo ToMap is unimplemented!")
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fInfo *FileInfo) FromMap(m map[string]interface{}) bool {
|
||||||
|
fileOpts := FileOptsType{}
|
||||||
|
dbutil.QuickSetBool(&fileOpts.Circular, m, "circular")
|
||||||
|
dbutil.QuickSetInt64(&fileOpts.MaxSize, m, "maxsize")
|
||||||
|
|
||||||
|
var metaJson []byte
|
||||||
|
dbutil.QuickSetBytes(&metaJson, m, "meta")
|
||||||
|
var fileMeta FileMeta
|
||||||
|
err := json.Unmarshal(metaJson, &fileMeta)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dbutil.QuickSetStr(&fInfo.BlockId, m, "blockid")
|
||||||
|
dbutil.QuickSetStr(&fInfo.Name, m, "name")
|
||||||
|
dbutil.QuickSetInt64(&fInfo.Size, m, "size")
|
||||||
|
dbutil.QuickSetInt64(&fInfo.CreatedTs, m, "createdts")
|
||||||
|
dbutil.QuickSetInt64(&fInfo.ModTs, m, "modts")
|
||||||
|
fInfo.Opts = fileOpts
|
||||||
|
fInfo.Meta = fileMeta
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileInfo(ctx context.Context, blockId string, name string) (*FileInfo, error) {
|
||||||
|
fInfoArr, txErr := WithTxRtn(ctx, func(tx *TxWrap) ([]*FileInfo, error) {
|
||||||
|
var rtn []*FileInfo
|
||||||
|
query := `SELECT * FROM block_file WHERE name = 'file-1'`
|
||||||
|
marr := tx.SelectMaps(query)
|
||||||
|
for _, m := range marr {
|
||||||
|
rtn = append(rtn, dbutil.FromMap[*FileInfo](m))
|
||||||
|
}
|
||||||
|
return rtn, nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return nil, fmt.Errorf("GetFileInfo database error: %v", txErr)
|
||||||
|
}
|
||||||
|
if len(fInfoArr) > 1 {
|
||||||
|
return nil, fmt.Errorf("GetFileInfo duplicate files in database")
|
||||||
|
}
|
||||||
|
if len(fInfoArr) == 0 {
|
||||||
|
return nil, fmt.Errorf("GetFileInfo: File not found")
|
||||||
|
}
|
||||||
|
fInfo := fInfoArr[0]
|
||||||
|
return fInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCacheFromDB(ctx context.Context, blockId string, name string, off int64, length int64, cacheNum int64) (*[]byte, error) {
|
||||||
|
return WithTxRtn(ctx, func(tx *TxWrap) (*[]byte, error) {
|
||||||
|
var cacheData *[]byte
|
||||||
|
query := `SELECT substr(data,?,?) FROM block_data WHERE blockid = ? AND name = ? and partidx = ?`
|
||||||
|
tx.Get(&cacheData, query, off, length+1, blockId, name, cacheNum)
|
||||||
|
if cacheData == nil {
|
||||||
|
cacheData = &[]byte{}
|
||||||
|
}
|
||||||
|
return cacheData, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFileFromDB(ctx context.Context, blockId string, name string) error {
|
||||||
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `DELETE from block_file where blockid = ? AND name = ?`
|
||||||
|
tx.Exec(query, blockId, name)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return txErr
|
||||||
|
}
|
||||||
|
txErr = WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `DELETE from block_data where blockid = ? AND name = ?`
|
||||||
|
tx.Exec(query, blockId, name)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return txErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteBlockFromDB(ctx context.Context, blockId string) error {
|
||||||
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `DELETE from block_file where blockid = ?`
|
||||||
|
tx.Exec(query, blockId)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return txErr
|
||||||
|
}
|
||||||
|
txErr = WithTx(ctx, func(tx *TxWrap) error {
|
||||||
|
query := `DELETE from block_data where blockid = ?`
|
||||||
|
tx.Exec(query, blockId)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if txErr != nil {
|
||||||
|
return txErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllFilesInDBForBlockId(ctx context.Context, blockId string) ([]*FileInfo, error) {
|
||||||
|
return WithTxRtn(ctx, func(tx *TxWrap) ([]*FileInfo, error) {
|
||||||
|
var rtn []*FileInfo
|
||||||
|
query := `SELECT * FROM block_file where blockid = ?`
|
||||||
|
marr := tx.SelectMaps(query, blockId)
|
||||||
|
for _, m := range marr {
|
||||||
|
rtn = append(rtn, dbutil.FromMap[*FileInfo](m))
|
||||||
|
}
|
||||||
|
return rtn, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllFilesInDB(ctx context.Context) ([]*FileInfo, error) {
|
||||||
|
return WithTxRtn(ctx, func(tx *TxWrap) ([]*FileInfo, error) {
|
||||||
|
var rtn []*FileInfo
|
||||||
|
query := `SELECT * FROM block_file`
|
||||||
|
marr := tx.SelectMaps(query)
|
||||||
|
for _, m := range marr {
|
||||||
|
rtn = append(rtn, dbutil.FromMap[*FileInfo](m))
|
||||||
|
}
|
||||||
|
return rtn, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllBlockIdsInDB(ctx context.Context) ([]string, error) {
|
||||||
|
return WithTxRtn(ctx, func(tx *TxWrap) ([]string, error) {
|
||||||
|
var rtn []string
|
||||||
|
query := `SELECT DISTINCT blockid FROM block_file`
|
||||||
|
marr := tx.SelectMaps(query)
|
||||||
|
for _, m := range marr {
|
||||||
|
var blockId string
|
||||||
|
dbutil.QuickSetStr(&blockId, m, "blockid")
|
||||||
|
rtn = append(rtn, blockId)
|
||||||
|
}
|
||||||
|
return rtn, nil
|
||||||
|
})
|
||||||
|
}
|
1077
wavesrv/pkg/blockstore/blockstore_test.go
Normal file
1077
wavesrv/pkg/blockstore/blockstore_test.go
Normal file
File diff suppressed because it is too large
Load Diff
21
wavesrv/pkg/blockstore/schema.sql
Normal file
21
wavesrv/pkg/blockstore/schema.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE schema_migrations (version uint64,dirty bool);
|
||||||
|
CREATE UNIQUE INDEX version_unique ON schema_migrations (version);
|
||||||
|
CREATE TABLE block_file (
|
||||||
|
blockid varchar(36) NOT NULL,
|
||||||
|
name varchar(200) NOT NULL,
|
||||||
|
maxsize bigint NOT NULL,
|
||||||
|
circular boolean NOT NULL,
|
||||||
|
size bigint NOT NULL,
|
||||||
|
createdts bigint NOT NULL,
|
||||||
|
modts bigint NOT NULL,
|
||||||
|
meta json NOT NULL,
|
||||||
|
PRIMARY KEY (blockid, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE block_data (
|
||||||
|
blockid varchar(36) NOT NULL,
|
||||||
|
name varchar(200) NOT NULL,
|
||||||
|
partidx int NOT NULL,
|
||||||
|
data blob NOT NULL,
|
||||||
|
PRIMARY KEY(blockid, name, partidx)
|
||||||
|
);
|
@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/waveenc"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/waveenc"
|
||||||
)
|
)
|
||||||
@ -110,6 +111,7 @@ func (pipe *BufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
|
|||||||
|
|
||||||
// Close the pipe. This will cause any blocking WriteTo calls to return.
|
// Close the pipe. This will cause any blocking WriteTo calls to return.
|
||||||
func (pipe *BufferedPipe) Close() error {
|
func (pipe *BufferedPipe) Close() error {
|
||||||
|
wlog.Logf("closing buffered pipe %s", pipe.Key)
|
||||||
defer pipe.bufferDataCond.Broadcast()
|
defer pipe.bufferDataCond.Broadcast()
|
||||||
pipe.closed.Store(true)
|
pipe.closed.Store(true)
|
||||||
return nil
|
return nil
|
||||||
|
@ -6091,6 +6091,26 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
|||||||
if pk.UIContext != nil && pk.UIContext.Build != "" {
|
if pk.UIContext != nil && pk.UIContext.Build != "" {
|
||||||
clientVersion = pk.UIContext.Build
|
clientVersion = pk.UIContext.Build
|
||||||
}
|
}
|
||||||
|
aiModel := clientData.OpenAIOpts.Model
|
||||||
|
if aiModel == "" {
|
||||||
|
aiModel = "(default) " + openai.DefaultModel
|
||||||
|
}
|
||||||
|
aiMaxTokens := fmt.Sprintf("%d", clientData.OpenAIOpts.MaxTokens)
|
||||||
|
if clientData.OpenAIOpts.MaxTokens == 0 {
|
||||||
|
aiMaxTokens = fmt.Sprintf("(default) %d", openai.DefaultMaxTokens)
|
||||||
|
}
|
||||||
|
aiMaxChoices := fmt.Sprintf("%d", clientData.OpenAIOpts.MaxChoices)
|
||||||
|
if clientData.OpenAIOpts.MaxChoices == 0 {
|
||||||
|
aiMaxChoices = "(not set)"
|
||||||
|
}
|
||||||
|
aiBaseUrl := clientData.OpenAIOpts.BaseURL
|
||||||
|
if aiBaseUrl == "" {
|
||||||
|
aiBaseUrl = "(openai default)"
|
||||||
|
}
|
||||||
|
aiTimeout := fmt.Sprintf("(default) %d", (OpenAIPacketTimeout / 1000))
|
||||||
|
if clientData.OpenAIOpts.Timeout != 0 {
|
||||||
|
aiTimeout = strconv.FormatFloat((float64(clientData.OpenAIOpts.Timeout) / 1000.0), 'f', -1, 64)
|
||||||
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId))
|
||||||
@ -6104,11 +6124,11 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
|||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termfontfamily", clientData.FeOpts.TermFontFamily))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termfontfamily", clientData.FeOpts.TermFontFamily))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termfontfamily", clientData.FeOpts.Theme))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termfontfamily", clientData.FeOpts.Theme))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aiapitoken", clientData.OpenAIOpts.APIToken))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aiapitoken", clientData.OpenAIOpts.APIToken))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimodel", clientData.OpenAIOpts.Model))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimodel", aiModel))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "aimaxtokens", clientData.OpenAIOpts.MaxTokens))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimaxtokens", aiMaxTokens))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "aimaxchoices", clientData.OpenAIOpts.MaxChoices))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimaxchoices", aiMaxChoices))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aibaseurl", clientData.OpenAIOpts.BaseURL))
|
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aibaseurl", aiBaseUrl))
|
||||||
buf.WriteString(fmt.Sprintf(" %-15s %ss\n", "aitimeout", strconv.FormatFloat((float64(clientData.OpenAIOpts.Timeout)/1000.0), 'f', -1, 64)))
|
buf.WriteString(fmt.Sprintf(" %-15s %ss\n", "aitimeout", aiTimeout))
|
||||||
update := scbus.MakeUpdatePacket()
|
update := scbus.MakeUpdatePacket()
|
||||||
update.AddUpdate(sstore.InfoMsgType{
|
update.AddUpdate(sstore.InfoMsgType{
|
||||||
InfoTitle: fmt.Sprintf("client info"),
|
InfoTitle: fmt.Sprintf("client info"),
|
||||||
|
@ -1987,9 +1987,22 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
|||||||
// Setting UsePty to false will ensure that the outputs get written to the correct file descriptors to extract stdout and stderr
|
// Setting UsePty to false will ensure that the outputs get written to the correct file descriptors to extract stdout and stderr
|
||||||
runPacket.UsePty = rcOpts.EphemeralOpts.UsePty
|
runPacket.UsePty = rcOpts.EphemeralOpts.UsePty
|
||||||
|
|
||||||
// Ephemeral commands can override the cwd without persisting it to the DB
|
// Ephemeral commands can override the current working directory. We need to expand the home dir if it's relative.
|
||||||
if rcOpts.EphemeralOpts.OverrideCwd != "" {
|
if rcOpts.EphemeralOpts.OverrideCwd != "" {
|
||||||
currentState.Cwd = rcOpts.EphemeralOpts.OverrideCwd
|
overrideCwd := rcOpts.EphemeralOpts.OverrideCwd
|
||||||
|
if !strings.HasPrefix(overrideCwd, "/") {
|
||||||
|
expandedCwd, err := msh.GetRemoteRuntimeState().ExpandHomeDir(overrideCwd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot expand home dir for cwd: %w", err)
|
||||||
|
}
|
||||||
|
overrideCwd = expandedCwd
|
||||||
|
}
|
||||||
|
currentState.Cwd = overrideCwd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ephemeral commands can override the timeout
|
||||||
|
if rcOpts.EphemeralOpts.TimeoutMs > 0 {
|
||||||
|
runPacket.Timeout = time.Duration(rcOpts.EphemeralOpts.TimeoutMs) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ephemeral commands can override the env without persisting it to the DB
|
// Ephemeral commands can override the env without persisting it to the DB
|
||||||
@ -2405,6 +2418,7 @@ func (msh *MShellProc) handleCmdStartError(rct *RunCmdType, startErr error) {
|
|||||||
defer msh.RemoveRunningCmd(rct.CK)
|
defer msh.RemoveRunningCmd(rct.CK)
|
||||||
if rct.EphemeralOpts != nil {
|
if rct.EphemeralOpts != nil {
|
||||||
// nothing to do for ephemeral commands besides remove the running command
|
// nothing to do for ephemeral commands besides remove the running command
|
||||||
|
log.Printf("ephemeral command start error: %v\n", startErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
@ -2472,6 +2486,11 @@ func (msh *MShellProc) handleCmdDonePacket(rct *RunCmdType, donePk *packet.CmdDo
|
|||||||
|
|
||||||
// Close the ephemeral response writer if it exists
|
// Close the ephemeral response writer if it exists
|
||||||
if rct.EphemeralOpts != nil && rct.EphemeralOpts.ExpectsResponse {
|
if rct.EphemeralOpts != nil && rct.EphemeralOpts.ExpectsResponse {
|
||||||
|
if donePk.ExitCode != 0 {
|
||||||
|
// if the command failed, we need to write the error to the response writer
|
||||||
|
log.Printf("writing error to ephemeral response writer\n")
|
||||||
|
rct.EphemeralOpts.StderrWriter.Write([]byte(fmt.Sprintf("error: %d\n", donePk.ExitCode)))
|
||||||
|
}
|
||||||
log.Printf("closing ephemeral response writers\n")
|
log.Printf("closing ephemeral response writers\n")
|
||||||
defer rct.EphemeralOpts.StdoutWriter.Close()
|
defer rct.EphemeralOpts.StdoutWriter.Close()
|
||||||
defer rct.EphemeralOpts.StderrWriter.Close()
|
defer rct.EphemeralOpts.StderrWriter.Close()
|
||||||
@ -2577,6 +2596,7 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rct.EphemeralOpts != nil {
|
if rct.EphemeralOpts != nil {
|
||||||
|
log.Printf("ephemeral data packet: %s\n", dataPk.CK)
|
||||||
// Write to the response writer if it's set
|
// Write to the response writer if it's set
|
||||||
if len(realData) > 0 && rct.EphemeralOpts.ExpectsResponse {
|
if len(realData) > 0 && rct.EphemeralOpts.ExpectsResponse {
|
||||||
switch dataPk.FdNum {
|
switch dataPk.FdNum {
|
||||||
@ -2594,6 +2614,9 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack
|
|||||||
log.Printf("error handling data packet: invalid fdnum %d\n", dataPk.FdNum)
|
log.Printf("error handling data packet: invalid fdnum %d\n", dataPk.FdNum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if dataPk.Error != "" {
|
||||||
|
log.Printf("ephemeral data packet error: %s\n", dataPk.Error)
|
||||||
|
}
|
||||||
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
|
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
|
||||||
msh.ServerProc.Input.SendPacket(ack)
|
msh.ServerProc.Input.SendPacket(ack)
|
||||||
return
|
return
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const {webDev, webProd} = require("./webpack/webpack.web.js");
|
const { webDev, webProd } = require("./webpack/webpack.web.js");
|
||||||
const {electronDev, electronProd} = require("./webpack/webpack.electron.js");
|
const { electronDev, electronProd } = require("./webpack/webpack.electron.js");
|
||||||
|
|
||||||
module.exports = (env) => {
|
module.exports = (env) => {
|
||||||
if (env.prod) {
|
if (env.prod) {
|
||||||
|
Loading…
Reference in New Issue
Block a user