mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38: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:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "0 21 * * *" # every day at 9pm
|
||||
workflow_dispatch:
|
||||
- cron: 0 21 * * *
|
||||
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:
|
||||
test:
|
||||
name: "TestDriver"
|
||||
name: TestDriver
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dashcamio/testdriver@main
|
||||
id: testdriver
|
||||
# note that .testdriver/prerun.sh runs before this, so the app has launched already
|
||||
with:
|
||||
version: v2.9.4
|
||||
version: v2.10.2
|
||||
prerun: |
|
||||
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
|
||||
cd ~/actions-runner/_work/testdriver/testdriver/
|
||||
@ -38,8 +42,24 @@ jobs:
|
||||
echo "Electron Done"
|
||||
exit
|
||||
prompt: |
|
||||
2. click "Create new tab"
|
||||
2. focus the Wave input with the keyboard shorcut Command + I
|
||||
3. type 'ls' into the input
|
||||
4. press return
|
||||
5. validate Wave shows the result of 'ls'
|
||||
1. wait 10 seconds
|
||||
1. click "Continue"
|
||||
1. click "Create new tab"
|
||||
1. validate that overlapping text does not appear in the application
|
||||
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
|
||||
|
||||
[![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 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)
|
||||
- [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
|
||||
|
||||
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)
|
||||
- [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
|
||||
```
|
||||
[![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)
|
||||
|
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-thead-border-top-color: rgba(250, 250, 250, 0.1);
|
||||
--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-hover-bg-color: rgba(255, 255, 255, 0.06);
|
||||
--table-tr-selected-bg-color: rgb(34, 34, 34);
|
||||
|
@ -9,8 +9,8 @@
|
||||
--app-accent-color: rgb(75, 166, 57);
|
||||
--app-accent-bg-color: rgba(75, 166, 57, 0.2);
|
||||
--app-text-color: rgb(0, 0, 0);
|
||||
--app-text-primary-color: rgb(0, 0, 0, 0.9);
|
||||
--app-text-secondary-color: rgb(0, 0, 0, 0.7);
|
||||
--app-text-primary-color: rgb(23, 23, 23);
|
||||
--app-text-secondary-color: rgb(76, 76, 76);
|
||||
--app-border-color: rgb(139 145 138);
|
||||
--app-panel-bg-color: 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-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-actions-bg-color: rgba(0, 0, 0, 0.1);
|
||||
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
|
||||
|
@ -1,6 +1,7 @@
|
||||
.clientsettings-view {
|
||||
.content {
|
||||
padding: 14px 18px 0 30px;
|
||||
padding: 14px 18px 14px 30px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.wave-dropdown {
|
||||
|
@ -14,6 +14,7 @@ import * as appconst from "@/app/appconst";
|
||||
|
||||
import "./clientsettings.less";
|
||||
import { MainView } from "../common/elements/mainview";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
class ClientSettingsKeybindings extends React.Component<{}, {}> {
|
||||
componentDidMount() {
|
||||
@ -251,7 +252,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
|
||||
const curSudoPwClearOnSleep = GlobalModel.getSudoPwClearOnSleep();
|
||||
|
||||
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}>
|
||||
<ClientSettingsKeybindings></ClientSettingsKeybindings>
|
||||
</If>
|
||||
|
@ -40,7 +40,8 @@
|
||||
}
|
||||
|
||||
.mainview-content {
|
||||
padding-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ import cn from "classnames";
|
||||
import { GlobalModel } from "@/models";
|
||||
|
||||
import "./mainview.less";
|
||||
import { Choose, If, Otherwise, When } from "tsx-control-statements/components";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
@mobxReact.observer
|
||||
class MainView extends React.Component<{
|
||||
@ -14,6 +16,8 @@ class MainView extends React.Component<{
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollable?: boolean;
|
||||
onScrollbarInitialized?: () => void;
|
||||
}> {
|
||||
render() {
|
||||
const sidebarModel = GlobalModel.mainSidebarModel;
|
||||
@ -31,7 +35,21 @@ class MainView extends React.Component<{
|
||||
</div>
|
||||
</header>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -52,6 +52,7 @@
|
||||
}
|
||||
|
||||
button {
|
||||
padding-right: 4px !important;
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, keybindings, title }
|
||||
</If>
|
||||
{<div className="wave-modal-title">{title}</div>}
|
||||
<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>
|
||||
</Button>
|
||||
</If>
|
||||
|
@ -1,25 +1,24 @@
|
||||
.rconndetail-modal {
|
||||
width: auto;
|
||||
max-width: 80vw;
|
||||
min-height: 565px;
|
||||
|
||||
.wave-modal-content {
|
||||
display: flex;
|
||||
padding-bottom: 0px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
max-width: 80vw;
|
||||
max-height: 90vh;
|
||||
|
||||
.wave-modal-body {
|
||||
display: flex;
|
||||
padding: 0px 20px;
|
||||
padding: 20px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
overflow-y: scroll;
|
||||
|
||||
.name-header-actions-wrapper {
|
||||
display: flex;
|
||||
@ -53,6 +52,7 @@
|
||||
|
||||
.remote-detail {
|
||||
width: 100%;
|
||||
padding-top: 16px;
|
||||
|
||||
.settings-field {
|
||||
display: flex;
|
||||
|
@ -15,6 +15,7 @@ import * as appconst from "@/app/appconst";
|
||||
|
||||
import "./viewremoteconndetail.less";
|
||||
import { ModalKeybindings } from "../elements/modal";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
@mobxReact.observer
|
||||
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
@ -299,15 +300,33 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
|
||||
return (
|
||||
<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} />
|
||||
<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 text-primary name-wrapper">
|
||||
{util.getRemoteName(remote)} {getImportTooltip(remote)}
|
||||
</div>
|
||||
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
||||
</div>
|
||||
<div className="remote-detail" style={{ overflow: "hidden" }}>
|
||||
<div className="remote-detail">
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Conn Id</div>
|
||||
<div className="settings-input">{remote.remoteid}</div>
|
||||
@ -381,33 +400,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<ModalKeybindings
|
||||
onOk={() => {
|
||||
if (selectedRemoteStatus == "connecting") {
|
||||
return;
|
||||
}
|
||||
this.handleClose();
|
||||
}}
|
||||
onCancel={() => {
|
||||
if (selectedRemoteStatus == "connecting") {
|
||||
return;
|
||||
}
|
||||
this.handleClose();
|
||||
}}
|
||||
></ModalKeybindings>
|
||||
<Button
|
||||
className="secondary"
|
||||
disabled={selectedRemoteStatus == "connecting"}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={selectedRemoteStatus == "connecting"} onClick={this.handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -9,10 +9,20 @@
|
||||
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 {
|
||||
margin: 0px 10px 10px 10px;
|
||||
table-layout: fixed;
|
||||
max-width: 970px;
|
||||
position: relative;
|
||||
|
||||
colgroup {
|
||||
.first-col {
|
||||
@ -28,13 +38,16 @@
|
||||
|
||||
thead {
|
||||
border-radius: var(--sizing-2-xs, 4px);
|
||||
border-bottom: 2px solid var(--table-thead-bright-border-color);
|
||||
user-select: none;
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 32px;
|
||||
padding: 5px 15px 5px 10px;
|
||||
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 {
|
||||
margin-left: 10px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import * as util from "@/util/util";
|
||||
|
||||
import "./connections.less";
|
||||
import { MainView } from "../common/elements/mainview";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
class ConnectionsKeybindings extends React.Component<{}, {}> {
|
||||
componentDidMount() {
|
||||
@ -149,57 +150,63 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
||||
<If condition={!isHidden}>
|
||||
<ConnectionsKeybindings></ConnectionsKeybindings>
|
||||
</If>
|
||||
<table
|
||||
className="connections-table"
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
border={0}
|
||||
ref={this.tableRef}
|
||||
onMouseLeave={this.handleTableHoverLeave}
|
||||
<OverlayScrollbarsComponent
|
||||
className="connections-table-container"
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
defer={true}
|
||||
>
|
||||
<colgroup>
|
||||
<col className="first-col" />
|
||||
<col className="second-col" />
|
||||
<col className="third-col" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-standard col-name">
|
||||
<div>Name</div>
|
||||
</th>
|
||||
<th className="text-standard col-type">
|
||||
<div>Type</div>
|
||||
</th>
|
||||
<th className="text-standard col-status">
|
||||
<div>Status</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<table
|
||||
className="connections-table"
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
border={0}
|
||||
ref={this.tableRef}
|
||||
onMouseLeave={this.handleTableHoverLeave}
|
||||
>
|
||||
<colgroup>
|
||||
<col className="first-col" />
|
||||
<col className="second-col" />
|
||||
<col className="third-col" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-standard col-name">
|
||||
<div>Name</div>
|
||||
</th>
|
||||
<th className="text-standard col-type">
|
||||
<div>Type</div>
|
||||
</th>
|
||||
<th className="text-standard col-status">
|
||||
<div>Status</div>
|
||||
</th>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<Button
|
||||
className="secondary"
|
||||
|
@ -260,8 +260,8 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
||||
<div className="logo">
|
||||
<WaveLogo />
|
||||
</div>
|
||||
<div className="close-button">
|
||||
<i className="fa-sharp fa-solid fa-xmark-large" onClick={toggleCollapse} />
|
||||
<div className="close-button" onClick={toggleCollapse}>
|
||||
<i className="fa-sharp fa-solid fa-xmark-large" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="contents">
|
||||
|
@ -61,9 +61,11 @@ class AIChat extends React.Component<{}, {}> {
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
this.chatWindowScrollRef = React.createRef();
|
||||
this.textAreaRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (this.chatWindowScrollRef?.current != null) {
|
||||
@ -88,7 +90,7 @@ class AIChat extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
submitChatMessage(messageStr: string) {
|
||||
const curLine = GlobalModel.inputModel.getCurLine();
|
||||
const curLine = GlobalModel.inputModel.curLine;
|
||||
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
|
||||
prtn.then((rtn) => {
|
||||
if (!rtn.success) {
|
||||
@ -103,15 +105,19 @@ class AIChat extends React.Component<{}, {}> {
|
||||
return { numLines, linePos };
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onTextAreaFocused(e: any) {
|
||||
GlobalModel.inputModel.setAuxViewFocus(true);
|
||||
this.onTextAreaChange(e);
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onTextAreaBlur(e: any) {
|
||||
GlobalModel.inputModel.setAuxViewFocus(false);
|
||||
//GlobalModel.inputModel.setAuxViewFocus(false);
|
||||
}
|
||||
|
||||
// Adjust the height of the textarea to fit the text
|
||||
@boundMethod
|
||||
onTextAreaChange(e: any) {
|
||||
// Calculate the bounding height of the text area
|
||||
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.
|
||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||
this.textAreaRef.current.style.height = newHeight + "px";
|
||||
}
|
||||
|
||||
onTextAreaInput(e: any) {
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
@ -140,8 +149,10 @@ class AIChat extends React.Component<{}, {}> {
|
||||
this.submitChatMessage(messageStr);
|
||||
currentRef.value = "";
|
||||
} else {
|
||||
inputModel.grabCodeSelectSelection();
|
||||
inputModel.setAuxViewFocus(false);
|
||||
mobx.action(() => {
|
||||
inputModel.grabCodeSelectSelection();
|
||||
inputModel.setAuxViewFocus(false);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +193,6 @@ class AIChat extends React.Component<{}, {}> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
@boundMethod
|
||||
onKeyDown(e: any) {}
|
||||
|
||||
@ -254,9 +264,10 @@ class AIChat extends React.Component<{}, {}> {
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
id="chat-cmd-input"
|
||||
onFocus={this.onTextAreaFocused.bind(this)}
|
||||
onBlur={this.onTextAreaBlur.bind(this)}
|
||||
onChange={this.onTextAreaChange.bind(this)}
|
||||
onFocus={this.onTextAreaFocused}
|
||||
onBlur={this.onTextAreaBlur}
|
||||
onChange={this.onTextAreaChange}
|
||||
onInput={this.onTextAreaInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
style={{ fontSize: this.termFontSize }}
|
||||
className="chat-textarea"
|
||||
|
@ -193,8 +193,8 @@
|
||||
}
|
||||
|
||||
// This aligns the icons with the prompt field.
|
||||
// We don't need right padding because the whole input field is already padded.
|
||||
padding: 2px 0 0 12px;
|
||||
// We don't need right margin because the whole input field is already padded.
|
||||
margin: 2px 0 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,11 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
cmdInputRef: React.RefObject<any> = React.createRef();
|
||||
promptRef: React.RefObject<any> = React.createRef();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateCmdInputHeight();
|
||||
}
|
||||
@ -56,7 +61,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
this.updateCmdInputHeight();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
clickFocusInputHint(): void {
|
||||
GlobalModel.inputModel.giveFocus();
|
||||
}
|
||||
@ -75,7 +80,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
GlobalModel.inputModel.setAuxViewFocus(false);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
clickAIAction(e: any): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -87,7 +92,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
clickHistoryAction(e: any): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -105,11 +110,9 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
GlobalCommandRunner.connectRemote(remoteId);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
toggleFilter(screen: Screen) {
|
||||
mobx.action(() => {
|
||||
screen.filterRunning.set(!screen.filterRunning.get());
|
||||
})();
|
||||
screen.filterRunning.set(!screen.filterRunning.get());
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
@ -22,6 +22,7 @@
|
||||
color: var(--app-text-color);
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
min-height: 100%;
|
||||
|
||||
.history-item {
|
||||
cursor: pointer;
|
||||
|
@ -42,6 +42,11 @@ class HItem extends React.Component<
|
||||
},
|
||||
{}
|
||||
> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
}
|
||||
|
||||
renderRemote(hitem: HistoryItem): any {
|
||||
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
|
||||
return sprintf("%-15s ", "");
|
||||
@ -169,12 +174,12 @@ class HistoryInfo extends React.Component<{}, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
handleClose() {
|
||||
GlobalModel.inputModel.closeAuxView();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
handleItemClick(hitem: HistoryItem) {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
const selItem = inputModel.getHistorySelectedItem();
|
||||
@ -195,14 +200,14 @@ class HistoryInfo extends React.Component<{}, {}> {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
handleClickType() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
inputModel.setAuxViewFocus(true);
|
||||
inputModel.toggleHistoryType();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
handleClickRemote() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
inputModel.setAuxViewFocus(true);
|
||||
@ -229,7 +234,7 @@ class HistoryInfo extends React.Component<{}, {}> {
|
||||
render() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
const selItem = inputModel.getHistorySelectedItem();
|
||||
const hitems = inputModel.getFilteredHistoryItems();
|
||||
const hitems = inputModel.filteredHistoryItems;
|
||||
const opts = inputModel.historyQueryOpts.get();
|
||||
let hitem: HistoryItem = null;
|
||||
let snames: Record<string, string> = {};
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
@ -17,6 +18,11 @@ dayjs.extend(localizedFormat);
|
||||
|
||||
@mobxReact.observer
|
||||
class InfoMsg extends React.Component<{}, {}> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
}
|
||||
|
||||
getAfterSlash(s: string): string {
|
||||
if (s.startsWith("^/")) {
|
||||
return s.substring(1);
|
||||
|
@ -117,7 +117,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
|
||||
const lastTab = this.lastTab;
|
||||
this.lastTab = true;
|
||||
this.curPress = "tab";
|
||||
const curLine = inputModel.getCurLine();
|
||||
const curLine = inputModel.curLine;
|
||||
if (lastTab) {
|
||||
GlobalModel.submitCommand(
|
||||
"_compgen",
|
||||
@ -250,9 +250,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
lastSP: StrWithPos = { str: "", pos: appconst.NoStrPos };
|
||||
version: OV<number> = mobx.observable.box(0, { name: "textAreaInput-version" }); // forces render updates
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
incVersion(): void {
|
||||
const v = this.version.get();
|
||||
mobx.action(() => this.version.set(v + 1))();
|
||||
this.version.set(v + 1);
|
||||
}
|
||||
|
||||
getCurSP(): StrWithPos {
|
||||
@ -278,6 +284,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setFocus(): void {
|
||||
GlobalModel.inputModel.giveFocus();
|
||||
}
|
||||
@ -311,7 +318,8 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@mobx.action.bound
|
||||
handleComponentDidMount() {
|
||||
const activeScreen = GlobalModel.getActiveScreen();
|
||||
if (activeScreen != null) {
|
||||
const focusType = activeScreen.focusType.get();
|
||||
@ -324,6 +332,24 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
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() {
|
||||
const activeScreen = GlobalModel.getActiveScreen();
|
||||
if (activeScreen != null) {
|
||||
@ -334,14 +360,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
this.lastFocusType = focusType;
|
||||
}
|
||||
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;
|
||||
}
|
||||
mobx.action(() => inputModel.forceCursorPos.set(null))();
|
||||
}
|
||||
this.updateCursorPosIfForced();
|
||||
if (inputModel.forceInputFocus) {
|
||||
inputModel.forceInputFocus = false;
|
||||
this.setFocus();
|
||||
@ -414,21 +433,18 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
return;
|
||||
}
|
||||
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
||||
GlobalModel.inputModel.setCurLine(currentRef.value);
|
||||
GlobalModel.inputModel.curLine = currentRef.value;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
@boundMethod
|
||||
onKeyDown(e: any) {}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
onChange(e: any) {
|
||||
mobx.action(() => {
|
||||
GlobalModel.inputModel.setCurLine(e.target.value);
|
||||
})();
|
||||
GlobalModel.inputModel.curLine = e.target.value;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
onSelect(e: any) {
|
||||
this.incVersion();
|
||||
}
|
||||
@ -453,7 +469,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
controlP() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (!inputModel.isHistoryLoaded()) {
|
||||
@ -465,7 +481,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
this.lastHistoryUpDown = true;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
controlN() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
inputModel.moveHistorySelection(-1);
|
||||
@ -526,17 +542,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
handleHistoryInput(e: any) {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
mobx.action(() => {
|
||||
const opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||
opts.queryStr = e.target.value;
|
||||
inputModel.setHistoryQueryOpts(opts);
|
||||
})();
|
||||
const opts = mobx.toJS(inputModel.historyQueryOpts.get());
|
||||
opts.queryStr = e.target.value;
|
||||
inputModel.setHistoryQueryOpts(opts);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action.bound
|
||||
handleFocus(e: any) {
|
||||
e.preventDefault();
|
||||
GlobalModel.inputModel.giveFocus();
|
||||
@ -561,7 +575,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
render() {
|
||||
const model = GlobalModel;
|
||||
const inputModel = model.inputModel;
|
||||
const curLine = inputModel.getCurLine();
|
||||
const curLine = inputModel.curLine;
|
||||
let displayLines = 1;
|
||||
const numLines = curLine.split("\n").length;
|
||||
const maxCols = this.getTextAreaMaxCols();
|
||||
@ -606,7 +620,6 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
|
||||
const renderCmdInputKeybindings = inputModel.shouldRenderAuxViewKeybindings(null);
|
||||
const renderHistoryKeybindings = inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_History);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="textareainput-div control is-expanded"
|
||||
|
@ -81,7 +81,7 @@ class BookmarksModel {
|
||||
mobx.action(() => {
|
||||
this.reset();
|
||||
this.globalModel.showSessionView();
|
||||
this.globalModel.inputModel.setCurLine(bm.cmdstr);
|
||||
this.globalModel.inputModel.curLine = bm.cmdstr;
|
||||
setTimeout(() => this.globalModel.inputModel.giveFocus(), 50);
|
||||
})();
|
||||
}
|
||||
|
@ -3,12 +3,10 @@
|
||||
|
||||
import type React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { isBlank } from "@/util/util";
|
||||
import * as appconst from "@/app/appconst";
|
||||
import type { Model } from "./model";
|
||||
import { GlobalCommandRunner, GlobalModel } from "./global";
|
||||
import { app } from "electron";
|
||||
|
||||
function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
|
||||
return {
|
||||
@ -48,7 +46,6 @@ class InputModel {
|
||||
name: "history-items",
|
||||
deep: false,
|
||||
}); // sorted in reverse (most recent is index 0)
|
||||
filteredHistoryItems: mobx.IComputedValue<HistoryItem[]> = null;
|
||||
historyIndex: mobx.IObservableValue<number> = mobx.observable.box(0, {
|
||||
name: "history-index",
|
||||
}); // 1-indexed (because 0 is current)
|
||||
@ -73,11 +70,11 @@ class InputModel {
|
||||
physicalInputFocused: OV<boolean> = mobx.observable.box(false);
|
||||
forceInputFocus: boolean = false;
|
||||
|
||||
lastCurLine: string = "";
|
||||
|
||||
constructor(globalModel: Model) {
|
||||
this.globalModel = globalModel;
|
||||
this.filteredHistoryItems = mobx.computed(() => {
|
||||
return this._getFilteredHistoryItems();
|
||||
});
|
||||
mobx.makeObservable(this);
|
||||
mobx.action(() => {
|
||||
this.codeSelectSelectedIndex.set(-1);
|
||||
this.codeSelectBlockRefArray = [];
|
||||
@ -85,12 +82,12 @@ class InputModel {
|
||||
this.codeSelectUuid = "";
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setInputMode(inputMode: null | "comment" | "global"): void {
|
||||
mobx.action(() => {
|
||||
this.inputMode.set(inputMode);
|
||||
})();
|
||||
this.inputMode.set(inputMode);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
toggleHistoryType(): void {
|
||||
const opts = mobx.toJS(this.historyQueryOpts.get());
|
||||
let htype = opts.queryType;
|
||||
@ -104,6 +101,7 @@ class InputModel {
|
||||
this.setHistoryType(htype);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
toggleRemoteType(): void {
|
||||
const opts = mobx.toJS(this.historyQueryOpts.get());
|
||||
if (opts.limitRemote) {
|
||||
@ -116,67 +114,63 @@ class InputModel {
|
||||
this.setHistoryQueryOpts(opts);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
onInputFocus(isFocused: boolean): void {
|
||||
mobx.action(() => {
|
||||
if (isFocused) {
|
||||
this.inputFocused.set(true);
|
||||
this.lineFocused.set(false);
|
||||
} else if (this.inputFocused.get()) {
|
||||
this.inputFocused.set(false);
|
||||
}
|
||||
})();
|
||||
if (isFocused) {
|
||||
this.inputFocused.set(true);
|
||||
this.lineFocused.set(false);
|
||||
} else if (this.inputFocused.get()) {
|
||||
this.inputFocused.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
onLineFocus(isFocused: boolean): void {
|
||||
mobx.action(() => {
|
||||
if (isFocused) {
|
||||
this.inputFocused.set(false);
|
||||
this.lineFocused.set(true);
|
||||
} else if (this.lineFocused.get()) {
|
||||
this.lineFocused.set(false);
|
||||
}
|
||||
})();
|
||||
if (isFocused) {
|
||||
this.inputFocused.set(false);
|
||||
this.lineFocused.set(true);
|
||||
} else if (this.lineFocused.get()) {
|
||||
this.lineFocused.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Focuses the main input or the auxiliary view, depending on the active auxiliary view
|
||||
@mobx.action
|
||||
giveFocus(): void {
|
||||
// Override active view to the main input if aux view does not have focus
|
||||
const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null;
|
||||
mobx.action(() => {
|
||||
switch (activeAuxView) {
|
||||
case appconst.InputAuxView_History: {
|
||||
const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
|
||||
if (elem != null) {
|
||||
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;
|
||||
switch (activeAuxView) {
|
||||
case appconst.InputAuxView_History: {
|
||||
const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
|
||||
if (elem != null) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setPhysicalInputFocused(isFocused: boolean): void {
|
||||
mobx.action(() => {
|
||||
this.physicalInputFocused.set(isFocused);
|
||||
})();
|
||||
this.physicalInputFocused.set(isFocused);
|
||||
if (isFocused) {
|
||||
const screen = this.globalModel.getActiveScreen();
|
||||
if (screen != null) {
|
||||
@ -203,6 +197,7 @@ class InputModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setHistoryType(htype: HistoryTypeStrs): void {
|
||||
if (this.historyQueryOpts.get().queryType == htype) {
|
||||
return;
|
||||
@ -214,7 +209,7 @@ class InputModel {
|
||||
if (oldItem == null) {
|
||||
return 0;
|
||||
}
|
||||
const newItems = this.getFilteredHistoryItems();
|
||||
const newItems = this.filteredHistoryItems;
|
||||
if (newItems.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
@ -234,15 +229,15 @@ class InputModel {
|
||||
return bestIdx + 1;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setHistoryQueryOpts(opts: HistoryQueryOpts): void {
|
||||
mobx.action(() => {
|
||||
const oldItem = this.getHistorySelectedItem();
|
||||
this.historyQueryOpts.set(opts);
|
||||
const bestIndex = this.findBestNewIndex(oldItem);
|
||||
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
|
||||
})();
|
||||
const oldItem = this.getHistorySelectedItem();
|
||||
this.historyQueryOpts.set(opts);
|
||||
const bestIndex = this.findBestNewIndex(oldItem);
|
||||
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
|
||||
this.AICmdInfoChatItems.replace(chat);
|
||||
this.codeSelectBlockRefArray = [];
|
||||
@ -256,6 +251,7 @@ class InputModel {
|
||||
return hitems != null;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
loadHistory(show: boolean, afterLoadIndex: number, htype: HistoryTypeStrs) {
|
||||
if (this.historyLoading.get()) {
|
||||
return;
|
||||
@ -266,12 +262,11 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
this.historyAfterLoadIndex = afterLoadIndex;
|
||||
mobx.action(() => {
|
||||
this.historyLoading.set(true);
|
||||
})();
|
||||
this.historyLoading.set(true);
|
||||
GlobalCommandRunner.loadHistory(show, htype);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
openHistory(): void {
|
||||
if (this.historyLoading.get()) {
|
||||
return;
|
||||
@ -287,13 +282,12 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
updateCmdLine(cmdLine: StrWithPos): void {
|
||||
mobx.action(() => {
|
||||
this.setCurLine(cmdLine.str);
|
||||
if (cmdLine.pos != appconst.NoStrPos) {
|
||||
this.forceCursorPos.set(cmdLine.pos);
|
||||
}
|
||||
})();
|
||||
this.curLine = cmdLine.str;
|
||||
if (cmdLine.pos != appconst.NoStrPos) {
|
||||
this.forceCursorPos.set(cmdLine.pos);
|
||||
}
|
||||
}
|
||||
|
||||
getHistorySelectedItem(): HistoryItem {
|
||||
@ -301,7 +295,7 @@ class InputModel {
|
||||
if (hidx == 0) {
|
||||
return null;
|
||||
}
|
||||
const hitems = this.getFilteredHistoryItems();
|
||||
const hitems = this.filteredHistoryItems;
|
||||
if (hidx > hitems.length) {
|
||||
return null;
|
||||
}
|
||||
@ -309,15 +303,16 @@ class InputModel {
|
||||
}
|
||||
|
||||
getFirstHistoryItem(): HistoryItem {
|
||||
const hitems = this.getFilteredHistoryItems();
|
||||
const hitems = this.filteredHistoryItems;
|
||||
if (hitems.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return hitems[0];
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setHistorySelectionNum(hnum: string): void {
|
||||
const hitems = this.getFilteredHistoryItems();
|
||||
const hitems = this.filteredHistoryItems;
|
||||
for (const [i, hitem] of hitems.entries()) {
|
||||
if (hitem.historynum == hnum) {
|
||||
this.setHistoryIndex(i + 1);
|
||||
@ -326,37 +321,33 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setHistoryInfo(hinfo: HistoryInfoType): void {
|
||||
mobx.action(() => {
|
||||
const oldItem = this.getHistorySelectedItem();
|
||||
const hitems: HistoryItem[] = hinfo.items ?? [];
|
||||
this.historyItems.set(hitems);
|
||||
this.historyLoading.set(false);
|
||||
this.historyQueryOpts.get().queryType = hinfo.historytype;
|
||||
if (hinfo.historytype == "session" || hinfo.historytype == "global") {
|
||||
this.historyQueryOpts.get().limitRemote = false;
|
||||
this.historyQueryOpts.get().limitRemoteInstance = false;
|
||||
const oldItem = this.getHistorySelectedItem();
|
||||
const hitems: HistoryItem[] = hinfo.items ?? [];
|
||||
this.historyItems.set(hitems);
|
||||
this.historyLoading.set(false);
|
||||
this.historyQueryOpts.get().queryType = hinfo.historytype;
|
||||
if (hinfo.historytype == "session" || hinfo.historytype == "global") {
|
||||
this.historyQueryOpts.get().limitRemote = 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);
|
||||
setTimeout(() => this.setHistoryIndex(bestIndex, true), 100);
|
||||
} else if (this.historyAfterLoadIndex) {
|
||||
if (hitems.length >= this.historyAfterLoadIndex) {
|
||||
this.setHistoryIndex(this.historyAfterLoadIndex);
|
||||
}
|
||||
}
|
||||
this.historyAfterLoadIndex = 0;
|
||||
if (hinfo.show) {
|
||||
this.openHistory();
|
||||
}
|
||||
})();
|
||||
}
|
||||
this.historyAfterLoadIndex = 0;
|
||||
if (hinfo.show) {
|
||||
this.openHistory();
|
||||
}
|
||||
}
|
||||
|
||||
getFilteredHistoryItems(): HistoryItem[] {
|
||||
return this.filteredHistoryItems.get();
|
||||
}
|
||||
|
||||
_getFilteredHistoryItems(): HistoryItem[] {
|
||||
@mobx.computed
|
||||
get filteredHistoryItems(): HistoryItem[] {
|
||||
const hitems: HistoryItem[] = this.historyItems.get() ?? [];
|
||||
const rtn: HistoryItem[] = [];
|
||||
const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get());
|
||||
@ -416,16 +407,15 @@ class InputModel {
|
||||
elem.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
grabSelectedHistoryItem(): void {
|
||||
const hitem = this.getHistorySelectedItem();
|
||||
if (hitem == null) {
|
||||
this.resetHistory();
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.resetInput();
|
||||
this.setCurLine(hitem.cmdstr);
|
||||
})();
|
||||
this.resetInput();
|
||||
this.curLine = hitem.cmdstr;
|
||||
}
|
||||
|
||||
// Closes the auxiliary view if it is open, focuses the main input
|
||||
@ -449,8 +439,8 @@ class InputModel {
|
||||
mobx.action(() => {
|
||||
this.auxViewFocus.set(view != null);
|
||||
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.
|
||||
@ -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.
|
||||
@mobx.action
|
||||
setAuxViewFocus(focus: boolean): void {
|
||||
mobx.action(() => {
|
||||
this.auxViewFocus.set(focus);
|
||||
})();
|
||||
this.auxViewFocus.set(focus);
|
||||
this.giveFocus();
|
||||
}
|
||||
|
||||
shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean {
|
||||
return mobx
|
||||
.computed(() => {
|
||||
if (view != null && this.getActiveAuxView() != view) {
|
||||
return false;
|
||||
}
|
||||
if (view != null && !this.getAuxViewFocus()) {
|
||||
return false;
|
||||
}
|
||||
if (view == null && this.hasFocus() && !this.getAuxViewFocus()) {
|
||||
return true;
|
||||
}
|
||||
if (view != null && this.getAuxViewFocus()) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
GlobalModel.getActiveScreen().getFocusType() == "input" &&
|
||||
GlobalModel.activeMainView.get() == "session"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.get();
|
||||
if (view != null && this.getActiveAuxView() != view) {
|
||||
return false;
|
||||
}
|
||||
if (view != null && !this.getAuxViewFocus()) {
|
||||
return false;
|
||||
}
|
||||
if (view == null && this.hasFocus() && !this.getAuxViewFocus()) {
|
||||
return true;
|
||||
}
|
||||
if (view != null && this.getAuxViewFocus()) {
|
||||
return true;
|
||||
}
|
||||
if (GlobalModel.getActiveScreen().getFocusType() == "input" && GlobalModel.activeMainView.get() == "session") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setHistoryIndex(hidx: number, force?: boolean): void {
|
||||
if (hidx < 0) {
|
||||
return;
|
||||
@ -503,18 +486,16 @@ class InputModel {
|
||||
if (!force && this.historyIndex.get() == hidx) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.historyIndex.set(hidx);
|
||||
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
||||
let hitem = this.getHistorySelectedItem();
|
||||
if (hitem == null) {
|
||||
hitem = this.getFirstHistoryItem();
|
||||
}
|
||||
if (hitem != null) {
|
||||
this.scrollHistoryItemIntoView(hitem.historynum);
|
||||
}
|
||||
this.historyIndex.set(hidx);
|
||||
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
||||
let hitem = this.getHistorySelectedItem();
|
||||
if (hitem == null) {
|
||||
hitem = this.getFirstHistoryItem();
|
||||
}
|
||||
})();
|
||||
if (hitem != null) {
|
||||
this.scrollHistoryItemIntoView(hitem.historynum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveHistorySelection(amt: number): void {
|
||||
@ -524,7 +505,7 @@ class InputModel {
|
||||
if (!this.isHistoryLoaded()) {
|
||||
return;
|
||||
}
|
||||
const hitems = this.getFilteredHistoryItems();
|
||||
const hitems = this.filteredHistoryItems;
|
||||
let idx = this.historyIndex.get() + amt;
|
||||
if (idx < 0) {
|
||||
idx = 0;
|
||||
@ -535,11 +516,10 @@ class InputModel {
|
||||
this.setHistoryIndex(idx);
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
flashInfoMsg(info: InfoType, timeoutMs: number): void {
|
||||
this._clearInfoTimeout();
|
||||
mobx.action(() => {
|
||||
this.infoMsg.set(info);
|
||||
})();
|
||||
this.infoMsg.set(info);
|
||||
|
||||
if (info == null && this.getActiveAuxView() == appconst.InputAuxView_Info) {
|
||||
this.setActiveAuxView(null);
|
||||
@ -578,7 +558,7 @@ class InputModel {
|
||||
) {
|
||||
const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
|
||||
const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline
|
||||
this.setCurLine(codeText);
|
||||
this.curLine = codeText;
|
||||
this.giveFocus();
|
||||
}
|
||||
}
|
||||
@ -594,72 +574,69 @@ class InputModel {
|
||||
return rtn;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
setCodeSelectSelectedCodeBlock(blockIndex: number) {
|
||||
mobx.action(() => {
|
||||
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
|
||||
this.codeSelectSelectedIndex.set(blockIndex);
|
||||
const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
||||
if (currentRef != null && this.aiChatWindowRef?.current != null) {
|
||||
const chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
||||
const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
||||
const elemTop = currentRef.offsetTop;
|
||||
let elemBottom = elemTop - currentRef.offsetHeight;
|
||||
const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
|
||||
if (!elementIsInView) {
|
||||
this.aiChatWindowRef.current.scrollTop =
|
||||
elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
|
||||
}
|
||||
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
|
||||
this.codeSelectSelectedIndex.set(blockIndex);
|
||||
const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
||||
if (currentRef != null && this.aiChatWindowRef?.current != null) {
|
||||
const chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
||||
const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
||||
const elemTop = currentRef.offsetTop;
|
||||
let elemBottom = elemTop - currentRef.offsetHeight;
|
||||
const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
|
||||
if (!elementIsInView) {
|
||||
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() {
|
||||
// oldest code block = index 0 in array
|
||||
// this decrements codeSelectSelected index
|
||||
mobx.action(() => {
|
||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
|
||||
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||
return;
|
||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
|
||||
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||
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) {
|
||||
this.codeSelectDeselectAll();
|
||||
if (this.aiChatWindowRef?.current != null) {
|
||||
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
||||
}
|
||||
}
|
||||
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
|
||||
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
|
||||
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
codeSelectSelectNextOldestCodeBlock() {
|
||||
mobx.action(() => {
|
||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||
if (this.codeSelectBlockRefArray.length > 0) {
|
||||
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||
if (this.codeSelectBlockRefArray.length > 0) {
|
||||
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
||||
if (decBlockIndex < 0) {
|
||||
this.codeSelectDeselectAll(this.codeSelectTop);
|
||||
if (this.aiChatWindowRef?.current != null) {
|
||||
this.aiChatWindowRef.current.scrollTop = 0;
|
||||
}
|
||||
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||
return;
|
||||
}
|
||||
const decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
||||
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() {
|
||||
@ -684,6 +661,7 @@ class InputModel {
|
||||
})();
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
openAIAssistantChat(): void {
|
||||
this.setActiveAuxView(appconst.InputAuxView_AIChat);
|
||||
this.setAuxViewFocus(true);
|
||||
@ -723,19 +701,19 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
clearInfoMsg(setNull: boolean): void {
|
||||
this._clearInfoTimeout();
|
||||
|
||||
if (this.getActiveAuxView() == appconst.InputAuxView_Info) {
|
||||
this.setActiveAuxView(null);
|
||||
}
|
||||
mobx.action(() => {
|
||||
if (setNull) {
|
||||
this.infoMsg.set(null);
|
||||
}
|
||||
})();
|
||||
if (setNull) {
|
||||
this.infoMsg.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
toggleInfoMsg(): void {
|
||||
this._clearInfoTimeout();
|
||||
if (this.activeAuxView.get() == appconst.InputAuxView_Info) {
|
||||
@ -745,65 +723,51 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
uiSubmitCommand(): void {
|
||||
const commandStr = this.curLine;
|
||||
if (commandStr.trim() == "") {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
const commandStr = this.getCurLine();
|
||||
if (commandStr.trim() == "") {
|
||||
return;
|
||||
}
|
||||
this.resetInput();
|
||||
this.globalModel.submitRawCommand(commandStr, true, true);
|
||||
})();
|
||||
this.globalModel.submitRawCommand(commandStr, true, true);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.getCurLine().trim() == "";
|
||||
return this.curLine.trim() == "";
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
resetInputMode(): void {
|
||||
mobx.action(() => {
|
||||
this.setInputMode(null);
|
||||
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;
|
||||
})();
|
||||
this.setInputMode(null);
|
||||
this.curLine = "";
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
resetInput(): void {
|
||||
mobx.action(() => {
|
||||
this.setActiveAuxView(null);
|
||||
this.inputMode.set(null);
|
||||
this.resetHistory();
|
||||
this.dropModHistory(false);
|
||||
this.infoMsg.set(null);
|
||||
this.inputExpanded.set(false);
|
||||
this._clearInfoTimeout();
|
||||
})();
|
||||
this.setActiveAuxView(null);
|
||||
this.inputMode.set(null);
|
||||
this.resetHistory();
|
||||
this.dropModHistory(false);
|
||||
this.infoMsg.set(null);
|
||||
this.inputExpanded.set(false);
|
||||
this._clearInfoTimeout();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@mobx.action
|
||||
toggleExpandInput(): void {
|
||||
mobx.action(() => {
|
||||
this.inputExpanded.set(!this.inputExpanded.get());
|
||||
this.forceInputFocus = true;
|
||||
})();
|
||||
this.inputExpanded.set(!this.inputExpanded.get());
|
||||
this.forceInputFocus = true;
|
||||
}
|
||||
|
||||
getCurLine(): string {
|
||||
@mobx.computed
|
||||
get curLine(): string {
|
||||
const hidx = this.historyIndex.get();
|
||||
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
|
||||
return this.modHistory[hidx];
|
||||
}
|
||||
const hitems = this.getFilteredHistoryItems();
|
||||
const hitems = this.filteredHistoryItems;
|
||||
if (hidx == 0 || hitems == null || hidx > hitems.length) {
|
||||
return "";
|
||||
}
|
||||
@ -814,31 +778,40 @@ class InputModel {
|
||||
return hitem.cmdstr;
|
||||
}
|
||||
|
||||
dropModHistory(keepLine0: boolean): void {
|
||||
set curLine(val: string) {
|
||||
this.lastCurLine = this.curLine;
|
||||
const hidx = this.historyIndex.get();
|
||||
mobx.action(() => {
|
||||
if (keepLine0) {
|
||||
if (this.modHistory.length > 1) {
|
||||
this.modHistory.splice(1, this.modHistory.length - 1);
|
||||
}
|
||||
} else {
|
||||
this.modHistory.replace([""]);
|
||||
if (this.modHistory.length <= hidx) {
|
||||
this.modHistory.length = hidx + 1;
|
||||
}
|
||||
this.modHistory[hidx] = val;
|
||||
})();
|
||||
}
|
||||
|
||||
resetHistory(): void {
|
||||
mobx.action(() => {
|
||||
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
|
||||
this.setActiveAuxView(null);
|
||||
@mobx.action
|
||||
dropModHistory(keepLine0: boolean): void {
|
||||
if (keepLine0) {
|
||||
if (this.modHistory.length > 1) {
|
||||
this.modHistory.splice(1, this.modHistory.length - 1);
|
||||
}
|
||||
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);
|
||||
})();
|
||||
} else {
|
||||
this.modHistory.replace([""]);
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
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 type { TermWrap } from "@/plugins/terminal/term";
|
||||
import * as util from "@/util/util";
|
||||
import { url } from "node:inspector";
|
||||
|
||||
type SWLinePtr = {
|
||||
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) {
|
||||
console.log("[cmd]", cmdPacketString(cmdPk));
|
||||
if (this.debugCmds > 1) {
|
||||
@ -1345,16 +1358,20 @@ class Model {
|
||||
})
|
||||
.then((resp) => handleJsonFetchResponse(url, resp))
|
||||
.then((data) => {
|
||||
mobx.action(() => {
|
||||
return mobx.action(() => {
|
||||
const update = data.data;
|
||||
if (update != null) {
|
||||
this.runUpdate(update, interactive);
|
||||
if (runUpdate) {
|
||||
this.runUpdate(update, interactive);
|
||||
} else {
|
||||
return { success: true, update: update };
|
||||
}
|
||||
}
|
||||
if (interactive && !this.isInfoUpdate(update)) {
|
||||
this.inputModel.clearInfoMsg(true);
|
||||
}
|
||||
return { success: true };
|
||||
})();
|
||||
return { success: true };
|
||||
})
|
||||
.catch((err) => {
|
||||
this.errorHandler("calling run-command", err, interactive);
|
||||
@ -1367,12 +1384,23 @@ class Model {
|
||||
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(
|
||||
metaCmd: string,
|
||||
metaSubCmd: string,
|
||||
args: string[],
|
||||
kwargs: Record<string, string>,
|
||||
interactive: boolean
|
||||
interactive: boolean,
|
||||
runUpdate: boolean = true
|
||||
): Promise<CommandRtnType> {
|
||||
const pk: FeCmdPacketType = {
|
||||
type: "fecmd",
|
||||
@ -1393,7 +1421,7 @@ class Model {
|
||||
pk.interactive
|
||||
);
|
||||
*/
|
||||
return this.submitCommandPacket(pk, interactive);
|
||||
return this.submitCommandPacket(pk, interactive, runUpdate);
|
||||
}
|
||||
|
||||
getSingleEphemeralCommandOutput(url: URL): Promise<string> {
|
||||
@ -1412,12 +1440,10 @@ class Model {
|
||||
let stderr = "";
|
||||
if (ephemeralCommandResponse.stdouturl) {
|
||||
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stdouturl);
|
||||
console.log("stdouturl", url);
|
||||
stdout = await this.getSingleEphemeralCommandOutput(url);
|
||||
}
|
||||
if (ephemeralCommandResponse.stderrurl) {
|
||||
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stderrurl);
|
||||
console.log("stderrurl", url);
|
||||
stderr = await this.getSingleEphemeralCommandOutput(url);
|
||||
}
|
||||
return { stdout: stdout, stderr: stderr };
|
||||
@ -1476,14 +1502,14 @@ class Model {
|
||||
interactive: interactive,
|
||||
ephemeralopts: ephemeralopts,
|
||||
};
|
||||
console.log(
|
||||
"CMD",
|
||||
pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
|
||||
pk.args,
|
||||
pk.kwargs,
|
||||
pk.interactive,
|
||||
pk.ephemeralopts
|
||||
);
|
||||
// console.log(
|
||||
// "CMD",
|
||||
// pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
|
||||
// pk.args,
|
||||
// pk.kwargs,
|
||||
// pk.interactive,
|
||||
// pk.ephemeralopts
|
||||
// );
|
||||
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 = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
update?: UpdatePacket;
|
||||
};
|
||||
|
||||
type EphemeralCommandOutputType = {
|
||||
|
@ -833,6 +833,7 @@ type RunPacketType struct {
|
||||
Detached bool `json:"detached,omitempty"`
|
||||
ReturnState bool `json:"returnstate,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 {
|
||||
|
@ -926,12 +926,11 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
|
||||
rcFileName = fmt.Sprintf("/dev/fd/%d", rcFileFdNum)
|
||||
}
|
||||
if cmd.TmpRcFileName != "" {
|
||||
go func() {
|
||||
time.AfterFunc(2*time.Second, func() {
|
||||
// cmd.Close() will also remove rcFileName
|
||||
// adding this to also try to proactively clean up after 2-seconds.
|
||||
time.Sleep(2 * time.Second)
|
||||
os.Remove(cmd.TmpRcFileName)
|
||||
}()
|
||||
})
|
||||
}
|
||||
fullCmdStr := pk.Command
|
||||
if pk.ReturnState {
|
||||
@ -1109,6 +1108,14 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ go 1.22
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
|
||||
github.com/alessio/shellescape v1.4.1
|
||||
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
|
||||
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/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
||||
"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.
|
||||
func (pipe *BufferedPipe) Close() error {
|
||||
wlog.Logf("closing buffered pipe %s", pipe.Key)
|
||||
defer pipe.bufferDataCond.Broadcast()
|
||||
pipe.closed.Store(true)
|
||||
return nil
|
||||
|
@ -6091,6 +6091,26 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
||||
if pk.UIContext != nil && 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
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId))
|
||||
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.Theme))
|
||||
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 %d\n", "aimaxtokens", clientData.OpenAIOpts.MaxTokens))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "aimaxchoices", clientData.OpenAIOpts.MaxChoices))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aibaseurl", clientData.OpenAIOpts.BaseURL))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %ss\n", "aitimeout", strconv.FormatFloat((float64(clientData.OpenAIOpts.Timeout)/1000.0), 'f', -1, 64)))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimodel", aiModel))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimaxtokens", aiMaxTokens))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimaxchoices", aiMaxChoices))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aibaseurl", aiBaseUrl))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %ss\n", "aitimeout", aiTimeout))
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(sstore.InfoMsgType{
|
||||
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
|
||||
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 != "" {
|
||||
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
|
||||
@ -2405,6 +2418,7 @@ func (msh *MShellProc) handleCmdStartError(rct *RunCmdType, startErr error) {
|
||||
defer msh.RemoveRunningCmd(rct.CK)
|
||||
if rct.EphemeralOpts != nil {
|
||||
// nothing to do for ephemeral commands besides remove the running command
|
||||
log.Printf("ephemeral command start error: %v\n", startErr)
|
||||
return
|
||||
}
|
||||
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
|
||||
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")
|
||||
defer rct.EphemeralOpts.StdoutWriter.Close()
|
||||
defer rct.EphemeralOpts.StderrWriter.Close()
|
||||
@ -2577,6 +2596,7 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack
|
||||
return
|
||||
}
|
||||
if rct.EphemeralOpts != nil {
|
||||
log.Printf("ephemeral data packet: %s\n", dataPk.CK)
|
||||
// Write to the response writer if it's set
|
||||
if len(realData) > 0 && rct.EphemeralOpts.ExpectsResponse {
|
||||
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)
|
||||
}
|
||||
}
|
||||
if dataPk.Error != "" {
|
||||
log.Printf("ephemeral data packet error: %s\n", dataPk.Error)
|
||||
}
|
||||
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
|
||||
msh.ServerProc.Input.SendPacket(ack)
|
||||
return
|
||||
|
@ -1,5 +1,5 @@
|
||||
const {webDev, webProd} = require("./webpack/webpack.web.js");
|
||||
const {electronDev, electronProd} = require("./webpack/webpack.electron.js");
|
||||
const { webDev, webProd } = require("./webpack/webpack.web.js");
|
||||
const { electronDev, electronProd } = require("./webpack/webpack.electron.js");
|
||||
|
||||
module.exports = (env) => {
|
||||
if (env.prod) {
|
||||
|
Loading…
Reference in New Issue
Block a user