mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
reinit updates (#500)
* working on re-init when you create a tab. some refactoring of existing reinit to make the messaging clearer. auto-connect, etc. * working to remove the 'default' shell states out of MShellProc. each tab should have its own state that gets set on open. * refactor newtab settings into individual components (and move to a new file) * more refactoring of tab settings -- use same control in settings and newtab * have screensettings use the same newtab settings components * use same conn dropdown, fix classes, update some of the confirm messages to be less confusing (replace screen with tab) * force a cr on a new tab to initialize state in a new line. poc right now, need to add to new workspace workflow as well * small fixups * remove nohist from GetRawStr, make const * update hover behavior for tabs * fix interaction between change remote dropdown, cmdinput, error handling, and selecting a remote * only switch screen remote if the activemainview is session (new remote flow). don't switch it if we're on the connections page which is confusing. also make it interactive * fix wording on tos modal * allow empty workspaces. also allow the last workspace to be deleted. (prep for new startup sequence where we initialize the first session after tos modal) * add some dead code that might come in use later (when we change how we show connection in cmdinput) * working a cople different angles. new settings tab-pulldown (likely orphaned). and then allowing null activeScreen and null activeSession in workspaceview (show appropriate messages, and give buttons to create new tabs/workspaces). prep for new startup flow * don't call initActiveShells anymore. also call ensureWorkspace() on TOS close * trying to use new pulldown screen settings * experiment with an escape keybinding * working on tab settings close triggers * close tab settings on tab switch * small updates to tos popup, reorder, update button text/size, small wording updates * when deleting a screen, send SIGHUP to all running commands * not sure how this happened, lineid should not be passed to setLineFocus * remove context timeouts for ReInit (it is now interactive, so it gets canceled like a normal command -- via ^C, and should not timeout on its own) * deal with screen/session tombstones updates (ignore to quite warning) * remove defaultfestate from remote * fix issue with removing default ris * remove dead code * open the settings pulldown for new screens * update prompt to show when the shell is still initializing (or if it failed) * switch buttons to use wave button class, update messages, and add warning for no shell state * all an override of rptr for dyncmds. needed for the 'connect' command (we need to set the rptr to the *new* connection rather than the old one) * remove old commented out code
This commit is contained in:
parent
6065ee931f
commit
3c3eec73aa
116
src/app/app.less
116
src/app/app.less
@ -251,6 +251,13 @@ input[type="checkbox"] {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-centered-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a.a-block {
|
||||
display: block;
|
||||
}
|
||||
@ -786,6 +793,75 @@ a.a-block {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.conn-dropdown {
|
||||
width: 412px;
|
||||
|
||||
.lefticon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 16px;
|
||||
transform: translateY(-50%);
|
||||
|
||||
.globe-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
fill: var(--app-text-secondary-color);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 8px;
|
||||
circle {
|
||||
stroke: var(--app-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wave-dropdown-display {
|
||||
bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-colors,
|
||||
.tab-icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.tab-color-sep,
|
||||
.tab-icon-sep {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-color-icon,
|
||||
.tab-icon-icon {
|
||||
width: 1.1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tab-color-name,
|
||||
.tab-icon-name {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
min-width: 70px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-color-select,
|
||||
.tab-icon-select {
|
||||
cursor: pointer;
|
||||
margin: 3px;
|
||||
&:hover {
|
||||
outline: 2px solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -847,7 +923,7 @@ a.a-block {
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
input:not(.wave-input) {
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@ -858,44 +934,6 @@ a.a-block {
|
||||
}
|
||||
}
|
||||
|
||||
.tab-colors,
|
||||
.tab-icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.tab-color-sep,
|
||||
.tab-icon-sep {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-color-icon,
|
||||
.tab-icon-icon {
|
||||
width: 1.1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tab-color-name,
|
||||
.tab-icon-name {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
min-width: 70px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-color-select,
|
||||
.tab-icon-select {
|
||||
cursor: pointer;
|
||||
margin: 3px;
|
||||
&:hover {
|
||||
outline: 2px solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-text {
|
||||
margin-left: 20px;
|
||||
|
||||
|
@ -159,7 +159,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
||||
</label>
|
||||
</If>
|
||||
<input
|
||||
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
||||
className={cn("wave-textfield-inner-input", "wave-input", {
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
ref={this.inputRef}
|
||||
id={label}
|
||||
value={inputValue}
|
||||
|
@ -107,15 +107,17 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||
this.model.setRecentConnAdded(true);
|
||||
this.model.closeModal();
|
||||
|
||||
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||
crRtn.then((crcrtn) => {
|
||||
if (crcrtn.success) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.errorStr.set(crcrtn.error);
|
||||
})();
|
||||
});
|
||||
if (GlobalModel.activeMainView.get() == "session") {
|
||||
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, true);
|
||||
crRtn.then((crcrtn) => {
|
||||
if (crcrtn.success) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.errorStr.set(crcrtn.error);
|
||||
})();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
|
@ -8,19 +8,22 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
|
||||
import { SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
|
||||
import * as util from "@/util/util";
|
||||
import { TabIcon, Button } from "@/elements";
|
||||
import { Button } from "@/elements";
|
||||
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
|
||||
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
|
||||
import * as appconst from "@/app/appconst";
|
||||
import {
|
||||
TabColorSelector,
|
||||
TabIconSelector,
|
||||
TabNameTextField,
|
||||
TabRemoteSelector,
|
||||
} from "@/app/workspace/screen/newtabsettings";
|
||||
|
||||
import "./screensettings.less";
|
||||
|
||||
const ScreenDeleteMessage = `
|
||||
Are you sure you want to delete this tab?
|
||||
|
||||
All commands and output will be deleted. To hide the tab, and retain the commands and output, use 'archive'.
|
||||
`.trim();
|
||||
|
||||
const WebShareConfirmMarkdown = `
|
||||
@ -82,39 +85,6 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
GlobalModel.modalsModel.popModal();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectTabColor(color: string): void {
|
||||
if (this.screen == null) {
|
||||
return;
|
||||
}
|
||||
if (this.screen.getTabColor() == color) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { tabcolor: color }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectTabIcon(icon: string): void {
|
||||
if (this.screen.getTabIcon() == icon) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.screenSetSettings(this.screen.screenId, { tabicon: icon }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeArchived(val: boolean): void {
|
||||
if (this.screen == null) {
|
||||
return;
|
||||
}
|
||||
if (this.screen.archived.get() == val) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.screenArchive(this.screenId, val);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeWebShare(val: boolean): void {
|
||||
if (this.screen == null) {
|
||||
@ -154,30 +124,6 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
}, 600);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
inlineUpdateName(val: string): void {
|
||||
if (this.screen == null) {
|
||||
return;
|
||||
}
|
||||
if (util.isStrEq(val, this.screen.name.get())) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { name: val }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
inlineUpdateShareName(val: string): void {
|
||||
if (this.screen == null) {
|
||||
return;
|
||||
}
|
||||
if (util.isStrEq(val, this.screen.getShareName())) {
|
||||
return;
|
||||
}
|
||||
const prtn = GlobalCommandRunner.screenSetSettings(this.screenId, { sharename: val }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
dismissError(): void {
|
||||
mobx.action(() => {
|
||||
@ -207,119 +153,37 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectRemote(cname: string): void {
|
||||
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
const screen = this.screen;
|
||||
if (screen == null) {
|
||||
return null;
|
||||
}
|
||||
let color: string = null;
|
||||
let icon: string = null;
|
||||
let index: number = 0;
|
||||
const curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
|
||||
|
||||
return (
|
||||
<Modal className="screen-settings-modal">
|
||||
<Modal.Header onClose={this.closeModal} title={`Tab Settings (${screen.name.get()})`} />
|
||||
<div className="wave-modal-body">
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Tab Id</div>
|
||||
<div className="settings-input">{screen.screenId}</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Name</div>
|
||||
<div className="settings-input">
|
||||
<InlineSettingsTextEdit
|
||||
placeholder="name"
|
||||
text={screen.name.get() ?? "(none)"}
|
||||
value={screen.name.get() ?? ""}
|
||||
onChange={this.inlineUpdateName}
|
||||
maxLength={50}
|
||||
showIcon={true}
|
||||
/>
|
||||
<TabNameTextField screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Connection</div>
|
||||
<div className="settings-input">
|
||||
<Dropdown
|
||||
className="screen-settings-dropdown"
|
||||
options={this.getOptions()}
|
||||
defaultValue={curRemote.remotecanonicalname}
|
||||
onChange={this.selectRemote}
|
||||
decoration={{
|
||||
startDecoration: (
|
||||
<div className="lefticon">
|
||||
<GlobeIcon className="globe-icon" />
|
||||
<StatusCircleIcon
|
||||
className={cn("status-icon", "status-" + curRemote.status)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TabRemoteSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Tab Color</div>
|
||||
<div className="settings-input">
|
||||
<div className="tab-colors">
|
||||
<div className="tab-color-cur">
|
||||
<TabIcon icon={screen.getTabIcon()} color={screen.getTabColor()} />
|
||||
<div className="tab-color-name">{screen.getTabColor()}</div>
|
||||
</div>
|
||||
<div className="tab-color-sep">|</div>
|
||||
<For each="color" of={appconst.TabColors}>
|
||||
<div
|
||||
key={color}
|
||||
className="tab-color-select"
|
||||
onClick={() => this.selectTabColor(color)}
|
||||
>
|
||||
<TabIcon icon="square" color={color} />
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
<TabColorSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">Tab Icon</div>
|
||||
<div className="settings-input">
|
||||
<div className="tab-icons">
|
||||
<div className="tab-icon-cur">
|
||||
<TabIcon icon={screen.getTabIcon()} color="white" />
|
||||
<div className="tab-icon-name">{screen.getTabIcon()}</div>
|
||||
</div>
|
||||
<div className="tab-icon-sep">|</div>
|
||||
<For each="icon" index="index" of={appconst.TabIcons}>
|
||||
<div
|
||||
key={`${color}-${index}`}
|
||||
className="tab-icon-select"
|
||||
onClick={() => this.selectTabIcon(icon)}
|
||||
>
|
||||
<TabIcon icon={icon} color="white" />
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
<div className="settings-label archived-label">
|
||||
<div className="">Archived</div>
|
||||
<Tooltip
|
||||
message={`Archive will hide the tab. Commands and output will be retained, but hidden.`}
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
className="screen-settings-tooltip"
|
||||
>
|
||||
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="settings-input">
|
||||
<Toggle checked={screen.archived.get()} onChange={this.handleChangeArchived} />
|
||||
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-field">
|
||||
|
@ -14,8 +14,6 @@ import "./sessionsettings.less";
|
||||
|
||||
const SessionDeleteMessage = `
|
||||
Are you sure you want to delete this workspace?
|
||||
|
||||
All commands and output will be deleted. To hide the workspace, and retain the commands and output, use 'archive'.
|
||||
`.trim();
|
||||
|
||||
@mobxReact.observer
|
||||
|
@ -86,7 +86,7 @@
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
font-size: 12.5px !important;
|
||||
font-size: 15px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ class TosModal extends React.Component<{}, {}> {
|
||||
acceptTos(): void {
|
||||
GlobalCommandRunner.clientAcceptTos();
|
||||
GlobalModel.modalsModel.popModal();
|
||||
GlobalCommandRunner.ensureWorkspace();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -40,32 +41,28 @@ class TosModal extends React.Component<{}, {}> {
|
||||
<div className="wave-modal-body-inner">
|
||||
<header className="tos-header unselectable">
|
||||
<div className="modal-title">Welcome to Wave Terminal!</div>
|
||||
<div className="modal-subtitle">Lets set everything for you</div>
|
||||
</header>
|
||||
<div className="content tos-content unselectable">
|
||||
<div className="item">
|
||||
<img src={shield} alt="Privacy" />
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
<img src={github} alt="Github" />
|
||||
</a>
|
||||
<div className="item-inner">
|
||||
<div className="item-title">Telemetry</div>
|
||||
<div className="item-title">Support us on GitHub</div>
|
||||
<div className="item-text">
|
||||
We only collect minimal <i>anonymous</i>
|
||||
We're <i>open source</i> and committed to providing a free terminal for
|
||||
individual users. Please show your support us by giving us a star on{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://docs.waveterm.dev/reference/telemetry")}
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
telemetry data
|
||||
Github (wavetermdev/waveterm)
|
||||
</a>
|
||||
to help us understand how many people are using Wave.
|
||||
</div>
|
||||
<div className="item-field" style={{ marginTop: 2 }}>
|
||||
<Toggle
|
||||
checked={!cdata.clientopts.notelemetry}
|
||||
onChange={this.handleChangeTelemetry}
|
||||
/>
|
||||
<div className="item-label">
|
||||
Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,25 +91,28 @@ class TosModal extends React.Component<{}, {}> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="item">
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
<img src={github} alt="Github" />
|
||||
</a>
|
||||
<img src={shield} alt="Privacy" />
|
||||
<div className="item-inner">
|
||||
<div className="item-title">Support us on GitHub</div>
|
||||
<div className="item-title">Telemetry</div>
|
||||
<div className="item-text">
|
||||
We're <i>open source</i> and committed to providing a free terminal for
|
||||
individual users. Please show your support us by giving us a star on{" "}
|
||||
We collect minimal anonymous
|
||||
<a
|
||||
target="_blank"
|
||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||
href={util.makeExternLink("https://docs.waveterm.dev/reference/telemetry")}
|
||||
rel={"noopener"}
|
||||
>
|
||||
Github (wavetermdev/waveterm)
|
||||
telemetry data
|
||||
</a>
|
||||
to help us understand how people are using Wave.
|
||||
</div>
|
||||
<div className="item-field" style={{ marginTop: 2 }}>
|
||||
<Toggle
|
||||
checked={!cdata.clientopts.notelemetry}
|
||||
onChange={this.handleChangeTelemetry}
|
||||
/>
|
||||
<div className="item-label">
|
||||
Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,7 +123,7 @@ class TosModal extends React.Component<{}, {}> {
|
||||
<a href="https://www.waveterm.dev/tos">Terms of Service</a>
|
||||
</div>
|
||||
<div className="button-wrapper">
|
||||
<Button onClick={this.acceptTos}>Continue</Button>
|
||||
<Button onClick={this.acceptTos}>Get Started</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -21,6 +21,10 @@
|
||||
color: var(--term-bright-green);
|
||||
}
|
||||
|
||||
.term-prompt-shellmsg {
|
||||
color: var(--term-bright-green);
|
||||
}
|
||||
|
||||
.term-prompt-cwd {
|
||||
color: var(--term-bright-green);
|
||||
}
|
||||
@ -34,6 +38,10 @@
|
||||
color: var(--term-black);
|
||||
}
|
||||
|
||||
.term-prompt-shellmsg {
|
||||
color: var(--term-green);
|
||||
}
|
||||
|
||||
.term-prompt-remote {
|
||||
color: var(--term-green);
|
||||
}
|
||||
|
@ -26,6 +26,20 @@ function makeFullRemoteRef(ownerName: string, remoteRef: string, name: string):
|
||||
return ownerName + ":" + remoteRef + ":" + name;
|
||||
}
|
||||
|
||||
function getRemoteStrWithAlias(rptr: RemotePtrType): string {
|
||||
if (rptr == null || isBlank(rptr.remoteid)) {
|
||||
return "(null)";
|
||||
}
|
||||
let remote = GlobalModel.getRemote(rptr.remoteid);
|
||||
if (remote == null) {
|
||||
return "(invalid)";
|
||||
}
|
||||
if (!isBlank(remote.remotealias)) {
|
||||
return `${remote.remotealias} (${remote.remotecanonicalname})`;
|
||||
}
|
||||
return `${remote.remotecanonicalname}`;
|
||||
}
|
||||
|
||||
function getRemoteStr(rptr: RemotePtrType): string {
|
||||
if (rptr == null || isBlank(rptr.remoteid)) {
|
||||
return "(invalid remote)";
|
||||
@ -69,17 +83,16 @@ function getCwdStr(remote: RemoteType, state: Record<string, string>): string {
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<string, string>; color: boolean }, {}> {
|
||||
render() {
|
||||
class Prompt extends React.Component<
|
||||
{ rptr: RemotePtrType; festate: Record<string, string>; color: boolean; shellInitMsg?: string },
|
||||
{}
|
||||
> {
|
||||
getRemoteElem() {
|
||||
const rptr = this.props.rptr;
|
||||
if (rptr == null || isBlank(rptr.remoteid)) {
|
||||
return <span className={cn("term-prompt", "color-green")}> </span>;
|
||||
}
|
||||
const remote = GlobalModel.getRemote(this.props.rptr.remoteid);
|
||||
const remoteStr = getRemoteStr(rptr);
|
||||
const festate = this.props.festate ?? {};
|
||||
const cwd = getCwdStr(remote, festate);
|
||||
let remoteTitle: string = null;
|
||||
let isRoot = false;
|
||||
let remote = this.getRemote();
|
||||
if (remote?.remotevars) {
|
||||
if (remote.remotevars["sudo"] || remote.remotevars["bestuser"] == "root") {
|
||||
isRoot = true;
|
||||
@ -89,11 +102,9 @@ class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<stri
|
||||
if (remote?.remoteopts?.color) {
|
||||
remoteColorClass = "color-" + remote.remoteopts.color;
|
||||
}
|
||||
let remoteTitle: string = null;
|
||||
if (remote?.remotecanonicalname) {
|
||||
remoteTitle = "connected to " + remote.remotecanonicalname;
|
||||
}
|
||||
const cwdElem = <span className="term-prompt-cwd">{cwd}</span>;
|
||||
let remoteElem = null;
|
||||
if (remoteStr != "local") {
|
||||
remoteElem = (
|
||||
@ -102,6 +113,36 @@ class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<stri
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return { remoteElem, isRoot };
|
||||
}
|
||||
|
||||
getRemote(): RemoteType {
|
||||
const remote = GlobalModel.getRemote(this.props.rptr.remoteid);
|
||||
return remote;
|
||||
}
|
||||
|
||||
render() {
|
||||
const rptr = this.props.rptr;
|
||||
if (rptr == null || isBlank(rptr.remoteid)) {
|
||||
return <span className={cn("term-prompt", "color-green")}> </span>;
|
||||
}
|
||||
let { remoteElem, isRoot } = this.getRemoteElem();
|
||||
let termClassNames = cn(
|
||||
"term-prompt",
|
||||
{ "term-prompt-color": this.props.color },
|
||||
{ "term-prompt-isroot": isRoot }
|
||||
);
|
||||
if (this.props.shellInitMsg != null) {
|
||||
return (
|
||||
<span className={termClassNames}>
|
||||
{remoteElem} <span className="term-prompt-shellmsg">{this.props.shellInitMsg}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const festate = this.props.festate ?? {};
|
||||
const remote = this.getRemote();
|
||||
const cwd = getCwdStr(remote, festate);
|
||||
const cwdElem = <span className="term-prompt-cwd">{cwd}</span>;
|
||||
let branchElem = null;
|
||||
let pythonElem = null;
|
||||
let condaElem = null;
|
||||
@ -131,17 +172,11 @@ class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<stri
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"term-prompt",
|
||||
{ "term-prompt-color": this.props.color },
|
||||
{ "term-prompt-isroot": isRoot }
|
||||
)}
|
||||
>
|
||||
<span className={termClassNames}>
|
||||
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Prompt, getRemoteStr };
|
||||
export { Prompt, getRemoteStr, getRemoteStrWithAlias };
|
||||
|
@ -24,6 +24,25 @@
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.cmdinput-conn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 0 0 4px 0;
|
||||
background-color: rgba(88, 193, 66, 0.3);
|
||||
padding: 2px 10px 4px 10px;
|
||||
font-size: calc(var(--termfontsize));
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(88, 193, 66, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.cmdinput-actions {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
@ -138,8 +157,10 @@
|
||||
color: var(--app-warning-color);
|
||||
align-items: center;
|
||||
|
||||
.wave-button {
|
||||
.wave-button,
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,6 +191,7 @@
|
||||
font-family: var(--termfontfamily);
|
||||
font-size: var(--termfontsize);
|
||||
line-height: var(--termlineheight);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.cmd-input-filter {
|
||||
|
@ -17,6 +17,7 @@ import { HistoryInfo } from "./historyinfo";
|
||||
import { Prompt } from "@/common/prompt/prompt";
|
||||
import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
|
||||
import { AIChat } from "./aichat";
|
||||
import * as util from "@/util/util";
|
||||
|
||||
import "./cmdinput.less";
|
||||
|
||||
@ -107,6 +108,24 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
GlobalCommandRunner.resetShellState();
|
||||
}
|
||||
|
||||
getRemoteDisplayName(rptr: RemotePtrType): string {
|
||||
if (rptr == null) {
|
||||
return "(unknown)";
|
||||
}
|
||||
const remote = GlobalModel.getRemote(rptr.remoteid);
|
||||
if (remote == null) {
|
||||
return "(invalid)";
|
||||
}
|
||||
let remoteNamePart = "";
|
||||
if (!util.isBlank(rptr.name)) {
|
||||
remoteNamePart = "#" + rptr.name;
|
||||
}
|
||||
if (remote.remotealias) {
|
||||
return remote.remotealias + remoteNamePart;
|
||||
}
|
||||
return remote.remotecanonicalname + remoteNamePart;
|
||||
}
|
||||
|
||||
render() {
|
||||
const model = GlobalModel;
|
||||
const inputModel = model.inputModel;
|
||||
@ -123,6 +142,9 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
remote = GlobalModel.getRemote(ri.remoteid);
|
||||
feState = ri.festate;
|
||||
}
|
||||
if (remote == null && rptr != null) {
|
||||
remote = GlobalModel.getRemote(rptr.remoteid);
|
||||
}
|
||||
feState = feState || {};
|
||||
const infoShow = inputModel.infoShow.get();
|
||||
const historyShow = !infoShow && inputModel.historyShow.get();
|
||||
@ -136,6 +158,19 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
if (win != null) {
|
||||
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
||||
}
|
||||
let shellInitMsg: string = null;
|
||||
let hidePrompt = false;
|
||||
if (ri == null) {
|
||||
let shellStr = "shell";
|
||||
if (!util.isBlank(remote?.defaultshelltype)) {
|
||||
shellStr = remote.defaultshelltype;
|
||||
}
|
||||
if (numRunningLines > 0) {
|
||||
shellInitMsg = `initializing ${shellStr}...`;
|
||||
} else {
|
||||
hidePrompt = true;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={this.cmdInputRef}
|
||||
@ -189,34 +224,44 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
is {remote.status}
|
||||
<If condition={remote.status != "connecting"}>
|
||||
<Button
|
||||
className="secondary small connect"
|
||||
className="primary outlined"
|
||||
onClick={() => this.clickConnectRemote(remote.remoteid)}
|
||||
>
|
||||
connect now
|
||||
Connect Now
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={feState["invalidshellstate"]}>
|
||||
<div className="remote-status-warning">
|
||||
WARNING: The shell state for this tab is invalid (
|
||||
The shell state for this tab is invalid (
|
||||
<a target="_blank" href="https://docs.waveterm.dev/reference/faq">
|
||||
see FAQ
|
||||
</a>
|
||||
). Must reset to continue.
|
||||
<div className="button is-wave-green is-outlined is-small" onClick={this.clickResetState}>
|
||||
reset shell state
|
||||
</div>
|
||||
<Button className="primary outlined" onClick={this.clickResetState}>
|
||||
Reset Now
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={ri == null && numRunningLines == 0}>
|
||||
<div className="remote-status-warning">
|
||||
Shell is not initialized, must reset to continue.
|
||||
<Button className="primary outlined" onClick={this.clickResetState}>
|
||||
Reset Now
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
<div key="base-cmdinput" className="base-cmdinput">
|
||||
<div key="prompt" className="cmd-input-context">
|
||||
<div className="has-text-white">
|
||||
<span ref={this.promptRef}>
|
||||
<Prompt rptr={rptr} festate={feState} color={true} />
|
||||
</span>
|
||||
<If condition={!hidePrompt}>
|
||||
<div key="prompt" className="cmd-input-context">
|
||||
<div className="has-text-white">
|
||||
<span ref={this.promptRef}>
|
||||
<Prompt rptr={rptr} festate={feState} color={true} shellInitMsg={shellInitMsg} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<div
|
||||
key="input"
|
||||
className={cn(
|
||||
|
@ -131,6 +131,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "cmdinput", "generic:confirm", (waveEvent) => {
|
||||
GlobalModel.closeTabSettings();
|
||||
if (GlobalModel.inputModel.isEmpty()) {
|
||||
let activeWindow = GlobalModel.getScreenLinesForActiveScreen();
|
||||
let activeScreen = GlobalModel.getActiveScreen();
|
||||
@ -144,6 +145,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "cmdinput", "generic:cancel", (waveEvent) => {
|
||||
GlobalModel.closeTabSettings();
|
||||
inputModel.toggleInfoMsg();
|
||||
if (inputModel.inputMode.get() != null) {
|
||||
inputModel.resetInputMode();
|
||||
@ -614,6 +616,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
||||
if (ri != null && ri.shelltype != null) {
|
||||
shellType = ri.shelltype;
|
||||
}
|
||||
if (shellType == "") {
|
||||
let rptr = screen.curRemote.get();
|
||||
if (rptr != null) {
|
||||
let remote = GlobalModel.getRemote(rptr.remoteid);
|
||||
if (remote != null) {
|
||||
shellType = remote.defaultshelltype;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let isMainInputFocused = this.mainInputFocused.get();
|
||||
let isHistoryFocused = this.historyFocused.get();
|
||||
|
230
src/app/workspace/screen/newtabsettings.tsx
Normal file
230
src/app/workspace/screen/newtabsettings.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalCommandRunner, GlobalModel, Screen } from "@/models";
|
||||
import { Button, TextField, Dropdown } from "@/elements";
|
||||
import { getRemoteStr, getRemoteStrWithAlias } from "@/common/prompt/prompt";
|
||||
import * as util from "@/util/util";
|
||||
import { TabIcon } from "@/elements/tabicon";
|
||||
import { ReactComponent as EllipseIcon } from "@/assets/icons/ellipse.svg";
|
||||
import { ReactComponent as Check12Icon } from "@/assets/icons/check12.svg";
|
||||
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
|
||||
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
|
||||
import * as appconst from "@/app/appconst";
|
||||
|
||||
import "./screenview.less";
|
||||
import "./tabs.less";
|
||||
|
||||
@mobxReact.observer
|
||||
class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
errorMessage: OV<string | null> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickNewConnection(): void {
|
||||
GlobalModel.remotesModel.openAddModal({ remoteedit: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let rptr = screen.curRemote.get();
|
||||
return (
|
||||
<div className="newtab-container">
|
||||
<div className="newtab-section name-section">
|
||||
<TabNameTextField screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section conn-section">
|
||||
<div className="unselectable">
|
||||
You're connected to <b>[{getRemoteStrWithAlias(rptr)}]</b>. Do you want to change it?
|
||||
</div>
|
||||
<div>
|
||||
<TabRemoteSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
<div className="text-caption cr-help-text">
|
||||
To change connection from the command line use `/connect [alias|user@host]`
|
||||
</div>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<TabColorSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TabNameTextField extends React.Component<{ screen: Screen; errorMessage?: OV<string> }, {}> {
|
||||
@boundMethod
|
||||
updateName(val: string): void {
|
||||
let { screen } = this.props;
|
||||
if (util.isStrEq(val, screen.name.get())) {
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { name: val }, false);
|
||||
util.commandRtnHandler(prtn, this.props.errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
return (
|
||||
<TextField label="Name" required={true} defaultValue={screen.name.get() ?? ""} onChange={this.updateName} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TabColorSelector extends React.Component<{ screen: Screen; errorMessage?: OV<string> }, {}> {
|
||||
@boundMethod
|
||||
selectTabColor(color: string): void {
|
||||
let { screen } = this.props;
|
||||
if (screen.getTabColor() == color) {
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabcolor: color }, false);
|
||||
util.commandRtnHandler(prtn, this.props.errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let curColor = screen.getTabColor();
|
||||
if (util.isBlank(curColor) || curColor == "default") {
|
||||
curColor = "green";
|
||||
}
|
||||
let color: string | null = null;
|
||||
return (
|
||||
<div className="tab-colors">
|
||||
<div className="tab-color-cur">
|
||||
<TabIcon icon={screen.getTabIcon()} color={screen.getTabColor()} />
|
||||
<div className="tab-color-name">{screen.getTabColor()}</div>
|
||||
</div>
|
||||
<div className="tab-color-sep">|</div>
|
||||
<For each="color" of={appconst.TabColors}>
|
||||
<div key={color} className="tab-color-select" onClick={() => this.selectTabColor(color)}>
|
||||
<TabIcon icon="square" color={color} />
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TabIconSelector extends React.Component<{ screen: Screen; errorMessage?: OV<string> }, {}> {
|
||||
@boundMethod
|
||||
selectTabIcon(icon: string): void {
|
||||
let { screen } = this.props;
|
||||
if (screen.getTabIcon() == icon) {
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabicon: icon }, false);
|
||||
util.commandRtnHandler(prtn, this.props.errorMessage);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let curIcon = screen.getTabIcon();
|
||||
if (util.isBlank(curIcon) || curIcon == "default") {
|
||||
curIcon = "square";
|
||||
}
|
||||
let icon: string | null = null;
|
||||
let curColor = screen.getTabColor();
|
||||
return (
|
||||
<div className="tab-icons">
|
||||
<div className="tab-icon-cur">
|
||||
<TabIcon icon={screen.getTabIcon()} color={curColor} />
|
||||
<div className="tab-icon-name">{screen.getTabIcon()}</div>
|
||||
</div>
|
||||
<div className="tab-icon-sep">|</div>
|
||||
<For each="icon" index="index" of={appconst.TabIcons}>
|
||||
<div key={icon} className="tab-icon-select" onClick={() => this.selectTabIcon(icon)}>
|
||||
<TabIcon icon={icon} color={curColor} />
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TabRemoteSelector extends React.Component<{ screen: Screen; errorMessage?: OV<string> }, {}> {
|
||||
selectedRemoteCN: OV<string> = mobx.observable.box(null, { name: "TabRemoteSelector-selectedRemoteCN" });
|
||||
|
||||
@boundMethod
|
||||
selectRemote(cname: string): void {
|
||||
mobx.action(() => {
|
||||
this.selectedRemoteCN.set(cname);
|
||||
})();
|
||||
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, true);
|
||||
util.commandRtnHandler(prtn, this.props.errorMessage);
|
||||
prtn.then((crtn) => {
|
||||
GlobalModel.inputModel.giveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getOptions(): DropdownItem[] {
|
||||
const remotes = GlobalModel.remotes;
|
||||
const options = remotes
|
||||
.filter((r) => !r.archived)
|
||||
.map((remote) => ({
|
||||
...remote,
|
||||
label: getRemoteStrWithAlias(remote),
|
||||
value: remote.remotecanonicalname,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
let connValA = util.getRemoteConnVal(a);
|
||||
let connValB = util.getRemoteConnVal(b);
|
||||
if (connValA !== connValB) {
|
||||
return connValA - connValB;
|
||||
}
|
||||
return a.remoteidx - b.remoteidx;
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { screen } = this.props;
|
||||
let selectedRemote = this.selectedRemoteCN.get();
|
||||
if (selectedRemote == null) {
|
||||
const curRI = screen.getCurRemoteInstance();
|
||||
if (curRI != null) {
|
||||
const curRemote = GlobalModel.getRemote(curRI.remoteid);
|
||||
selectedRemote = curRemote.remotecanonicalname;
|
||||
} else {
|
||||
const localRemote = GlobalModel.getLocalRemote();
|
||||
selectedRemote = localRemote.remotecanonicalname;
|
||||
}
|
||||
}
|
||||
let curRemote = GlobalModel.getRemoteByName(selectedRemote);
|
||||
return (
|
||||
<Dropdown
|
||||
className="conn-dropdown"
|
||||
options={this.getOptions()}
|
||||
defaultValue={curRemote.remotecanonicalname}
|
||||
onChange={this.selectRemote}
|
||||
decoration={{
|
||||
startDecoration: (
|
||||
<div className="lefticon">
|
||||
<GlobeIcon className="globe-icon" />
|
||||
<StatusCircleIcon className={cn("status-icon", "status-" + curRemote.status)} />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { NewTabSettings, TabColorSelector, TabIconSelector, TabNameTextField, TabRemoteSelector };
|
@ -171,37 +171,6 @@
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
|
||||
.conn-dropdown {
|
||||
width: 412px;
|
||||
|
||||
.lefticon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 16px;
|
||||
transform: translateY(-50%);
|
||||
|
||||
.globe-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
fill: var(--app-text-secondary-color);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 8px;
|
||||
circle {
|
||||
stroke: var(--app-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wave-dropdown-display {
|
||||
bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.conn-section {
|
||||
gap: 8px;
|
||||
}
|
||||
|
@ -12,18 +12,13 @@ import { debounce } from "throttle-debounce";
|
||||
import dayjs from "dayjs";
|
||||
import { GlobalCommandRunner, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "@/models";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { Button, TextField, Dropdown } from "@/elements";
|
||||
import { getRemoteStr } from "@/common/prompt/prompt";
|
||||
import { Button } from "@/elements";
|
||||
import { Line } from "@/app/line/linecomps";
|
||||
import { LinesView } from "@/app/line/linesview";
|
||||
import * as util from "@/util/util";
|
||||
import { TabIcon } from "@/elements/tabicon";
|
||||
import { ReactComponent as EllipseIcon } from "@/assets/icons/ellipse.svg";
|
||||
import { ReactComponent as Check12Icon } from "@/assets/icons/check12.svg";
|
||||
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
|
||||
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
|
||||
import * as appconst from "@/app/appconst";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
import { NewTabSettings } from "./newtabsettings";
|
||||
|
||||
import "./screenview.less";
|
||||
import "./tabs.less";
|
||||
@ -40,9 +35,16 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
|
||||
sidebarShowing: OV<boolean> = mobx.observable.box(false, { name: "screenview-sidebarShowing" });
|
||||
sidebarShowingTimeoutId: any = null;
|
||||
|
||||
constructor(props: any) {
|
||||
constructor(props: { session: Session; screen: Screen }) {
|
||||
super(props);
|
||||
this.handleResize_debounced = debounce(100, this.handleResize.bind(this));
|
||||
let screen = this.props.screen;
|
||||
let hasSidebar = false;
|
||||
if (screen != null) {
|
||||
let viewOpts = screen.viewOpts.get();
|
||||
hasSidebar = viewOpts?.sidebar?.open;
|
||||
}
|
||||
this.sidebarShowing = mobx.observable.box(hasSidebar, { name: "screenview-sidebarShowing" });
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@ -53,15 +55,13 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
|
||||
this.rszObs.observe(elem);
|
||||
this.handleResize();
|
||||
}
|
||||
let viewOpts = screen.viewOpts.get();
|
||||
let hasSidebar = viewOpts?.sidebar?.open;
|
||||
if (hasSidebar) {
|
||||
mobx.action(() => this.sidebarShowing.set(true))();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
let { screen } = this.props;
|
||||
if (screen == null) {
|
||||
return;
|
||||
}
|
||||
let viewOpts = screen.viewOpts.get();
|
||||
let hasSidebar = viewOpts?.sidebar?.open;
|
||||
if (hasSidebar && !this.sidebarShowing.get()) {
|
||||
@ -96,19 +96,62 @@ class ScreenView extends React.Component<{ session: Session; screen: Screen }, {
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
createWorkspace() {
|
||||
GlobalCommandRunner.createNewSession();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
createTab() {
|
||||
GlobalCommandRunner.createNewScreen();
|
||||
}
|
||||
|
||||
render() {
|
||||
let { session, screen } = this.props;
|
||||
if (screen == null) {
|
||||
return (
|
||||
<div className="screen-view" ref={this.screenViewRef}>
|
||||
(no screen found)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let screenWidth = this.width.get();
|
||||
if (screenWidth == null) {
|
||||
return <div className="screen-view" ref={this.screenViewRef}></div>;
|
||||
}
|
||||
if (session == null) {
|
||||
let sessionCount = GlobalModel.sessionList.length;
|
||||
return (
|
||||
<div className="screen-view" ref={this.screenViewRef}>
|
||||
<div className="window-view" style={{ width: "100%" }}>
|
||||
<div key="lines" className="lines"></div>
|
||||
<div key="window-empty" className={cn("window-empty")}>
|
||||
<div className="flex-centered-column">
|
||||
<code className="text-standard">[no workspace]</code>
|
||||
<If condition={sessionCount == 0}>
|
||||
<Button onClick={this.createWorkspace} style={{ marginTop: 10 }}>
|
||||
Create New Workspace
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (screen == null) {
|
||||
let screens = GlobalModel.getSessionScreens(session.sessionId);
|
||||
return (
|
||||
<div className="screen-view" ref={this.screenViewRef}>
|
||||
<div className="window-view" style={{ width: "100%" }}>
|
||||
<div key="lines" className="lines"></div>
|
||||
<div key="window-empty" className={cn("window-empty")}>
|
||||
<div className="flex-centered-column">
|
||||
<code className="text-standard">[no active tab]</code>
|
||||
<If condition={screens.length == 0}>
|
||||
<Button onClick={this.createTab} style={{ marginTop: 10 }}>
|
||||
Create New Tab
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let fontSize = GlobalModel.getTermFontSize();
|
||||
let dprStr = sprintf("%0.3f", GlobalModel.devicePixelRatio.get());
|
||||
let viewOpts = screen.viewOpts.get();
|
||||
@ -339,193 +382,6 @@ class ScreenSidebar extends React.Component<{ screen: Screen; width: string }, {
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
connDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "NewTabSettings-connDropdownActive" });
|
||||
errorMessage: OV<string | null> = mobx.observable.box(null, { name: "NewTabSettings-errorMessage" });
|
||||
remotes: RemoteType[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.remotes = GlobalModel.remotes;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectTabColor(color: string): void {
|
||||
let { screen } = this.props;
|
||||
if (screen.getTabColor() == color) {
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabcolor: color }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectTabIcon(icon: string): void {
|
||||
let { screen } = this.props;
|
||||
if (screen.getTabIcon() == icon) {
|
||||
return;
|
||||
}
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { tabicon: icon }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
updateName(val: string): void {
|
||||
let { screen } = this.props;
|
||||
let prtn = GlobalCommandRunner.screenSetSettings(screen.screenId, { name: val }, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleConnDropdown(): void {
|
||||
mobx.action(() => {
|
||||
this.connDropdownActive.set(!this.connDropdownActive.get());
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
selectRemote(cname: string): void {
|
||||
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||
util.commandRtnHandler(prtn, this.errorMessage);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickNewConnection(): void {
|
||||
GlobalModel.remotesModel.openAddModal({ remoteedit: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
getOptions(): { label: string; value: string }[] {
|
||||
return this.remotes
|
||||
.filter((r) => !r.archived)
|
||||
.map((remote) => ({
|
||||
...remote,
|
||||
label: !util.isBlank(remote.remotealias)
|
||||
? `${remote.remotealias} - ${remote.remotecanonicalname}`
|
||||
: remote.remotecanonicalname,
|
||||
value: remote.remotecanonicalname,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
let connValA = util.getRemoteConnVal(a);
|
||||
let connValB = util.getRemoteConnVal(b);
|
||||
if (connValA !== connValB) {
|
||||
return connValA - connValB;
|
||||
}
|
||||
return a.remoteidx - b.remoteidx;
|
||||
});
|
||||
}
|
||||
|
||||
renderTabIconSelector(): React.ReactNode {
|
||||
let { screen } = this.props;
|
||||
let curIcon = screen.getTabIcon();
|
||||
if (util.isBlank(curIcon) || curIcon == "default") {
|
||||
curIcon = "square";
|
||||
}
|
||||
let icon: string | null = null;
|
||||
let curColor = screen.getTabColor();
|
||||
return (
|
||||
<>
|
||||
<div className="bold unselectable">Tab Icon:</div>
|
||||
<div className="control-iconlist tabicon-list">
|
||||
<For each="icon" of={appconst.TabIcons}>
|
||||
<div
|
||||
className="icondiv tabicon"
|
||||
key={icon}
|
||||
title={icon || ""}
|
||||
onClick={() => this.selectTabIcon(icon || "")}
|
||||
>
|
||||
<TabIcon icon={icon} color={curColor} />
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderTabColorSelector(): React.ReactNode {
|
||||
let { screen } = this.props;
|
||||
let curColor = screen.getTabColor();
|
||||
if (util.isBlank(curColor) || curColor == "default") {
|
||||
curColor = "green";
|
||||
}
|
||||
let color: string | null = null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bold unselectable">Tab Color:</div>
|
||||
<div className="control-iconlist">
|
||||
<For each="color" of={appconst.TabColors}>
|
||||
<div
|
||||
className="icondiv"
|
||||
key={color}
|
||||
title={color || ""}
|
||||
onClick={() => this.selectTabColor(color || "")}
|
||||
>
|
||||
<EllipseIcon className={cn("icon", "color-" + color)} />
|
||||
<If condition={color == curColor}>
|
||||
<Check12Icon className="check-icon" />
|
||||
</If>
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let rptr = screen.curRemote.get();
|
||||
let curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
|
||||
|
||||
return (
|
||||
<div className="newtab-container">
|
||||
<div className="newtab-section name-section">
|
||||
<TextField
|
||||
label="Name"
|
||||
required={true}
|
||||
defaultValue={screen.name.get() ?? ""}
|
||||
onChange={this.updateName}
|
||||
/>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section conn-section">
|
||||
<div className="unselectable">
|
||||
You're connected to [{getRemoteStr(rptr)}]. Do you want to change it?
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown
|
||||
className="conn-dropdown"
|
||||
options={this.getOptions()}
|
||||
defaultValue={curRemote.remotecanonicalname}
|
||||
onChange={this.selectRemote}
|
||||
decoration={{
|
||||
startDecoration: (
|
||||
<div className="lefticon">
|
||||
<GlobeIcon className="globe-icon" />
|
||||
<StatusCircleIcon className={cn("status-icon", "status-" + curRemote.status)} />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-caption cr-help-text">
|
||||
To change connection from the command line use `cr [alias|user@host]`
|
||||
</div>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<div>{this.renderTabIconSelector()}</div>
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<div>{this.renderTabColorSelector()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// screen is not null
|
||||
@mobxReact.observer
|
||||
class ScreenWindowView extends React.Component<{ session: Session; screen: Screen; width: string }, {}> {
|
||||
@ -561,6 +417,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { screen } = this.props;
|
||||
let wvElem = this.windowViewRef.current;
|
||||
if (wvElem != null) {
|
||||
let width = wvElem.offsetWidth;
|
||||
@ -569,6 +426,12 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
|
||||
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
|
||||
this.rszObs.observe(wvElem);
|
||||
}
|
||||
if (screen.isNew) {
|
||||
screen.isNew = false;
|
||||
mobx.action(() => {
|
||||
GlobalModel.tabSettingsOpen.set(true);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -695,7 +558,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
|
||||
return (
|
||||
<div className="window-view" ref={this.windowViewRef} style={{ width: this.props.width }}>
|
||||
<If condition={lines.length == 0}>
|
||||
<If condition={screen.nextLineNum.get() == 1}>
|
||||
<If condition={false && screen.nextLineNum.get() == 1}>
|
||||
<NewTabSettings screen={screen} />
|
||||
</If>
|
||||
<If condition={screen.nextLineNum.get() != 1}>
|
||||
|
@ -53,9 +53,8 @@ class ScreenTab extends React.Component<
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mobx.action(() => {
|
||||
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
|
||||
GlobalModel.tabSettingsOpen.set(!GlobalModel.tabSettingsOpen.get());
|
||||
})();
|
||||
GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -112,6 +112,27 @@
|
||||
opacity: 1;
|
||||
font-weight: var(--screentabs-selected-font-weight);
|
||||
border-top: 2px solid var(--tab-color);
|
||||
|
||||
.screen-tab-inner {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-active) .status-indicator {
|
||||
.status-indicator-visible;
|
||||
}
|
||||
|
||||
&.is-active:not(:hover) .status-indicator {
|
||||
.status-indicator-visible;
|
||||
}
|
||||
|
||||
&.is-active:hover .actions {
|
||||
.positional-icon-visible;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--app-selected-mask-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-archived {
|
||||
@ -148,20 +169,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-line {
|
||||
border-left: 1px solid var(--app-border-color);
|
||||
margin: 10px 0 8px 0;
|
||||
}
|
||||
|
||||
&:not(:hover) .status-indicator {
|
||||
.status-indicator-visible;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.actions {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,4 +23,34 @@
|
||||
width: 100%;
|
||||
color: var(--app-text-secondary-color);
|
||||
}
|
||||
|
||||
.tab-settings-pulldown {
|
||||
position: absolute;
|
||||
top: var(--screentabs-height);
|
||||
width: 100%;
|
||||
height: 330px;
|
||||
transition: height 0.2s ease-in-out;
|
||||
overflow: hidden;
|
||||
z-index: 11;
|
||||
border-bottom: 3px solid var(--app-border-color);
|
||||
background-color: var(--app-panel-bg-color);
|
||||
border-radius: 0 0 5px 5px;
|
||||
|
||||
&.closed {
|
||||
height: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: var(--app-selected-mask-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ import { ScreenTabs } from "./screen/tabs";
|
||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
import "./workspace.less";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import type { Screen } from "@/models";
|
||||
import { getRemoteStr, getRemoteStrWithAlias } from "@/common/prompt/prompt";
|
||||
import { TabColorSelector, TabIconSelector, TabNameTextField, TabRemoteSelector } from "./screen/newtabsettings";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
@ -79,43 +83,115 @@ class SessionKeybindings extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class WorkspaceView extends React.Component<{}, {}> {
|
||||
class TabSettingsPulldownKeybindings extends React.Component<{}, {}> {
|
||||
componentDidMount() {
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
keybindManager.registerKeybinding("pane", "tabsettings", "generic:cancel", (waveEvent) => {
|
||||
GlobalModel.closeTabSettings();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
GlobalModel.keybindManager.unregisterDomain("tabsettings");
|
||||
}
|
||||
|
||||
render() {
|
||||
let model = GlobalModel;
|
||||
let session = model.getActiveSession();
|
||||
if (session == null) {
|
||||
return (
|
||||
<div className="session-view">
|
||||
<div className="center-message">
|
||||
<div>(no active workspace)</div>
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TabSettings extends React.Component<{ screen: Screen }, {}> {
|
||||
errorMessage: OV<string> = mobx.observable.box(null, { name: "TabSettings-errorMessage" });
|
||||
|
||||
render() {
|
||||
let { screen } = this.props;
|
||||
let rptr = screen.curRemote.get();
|
||||
return (
|
||||
<div className="newtab-container">
|
||||
<div className="newtab-section name-section">
|
||||
<TabNameTextField screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section conn-section">
|
||||
<div className="unselectable">
|
||||
You're connected to "{getRemoteStrWithAlias(rptr)}". Do you want to change it?
|
||||
</div>
|
||||
<div>
|
||||
<TabRemoteSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
<div className="text-caption cr-help-text">
|
||||
To change connection from the command line use `cr [alias|user@host]`
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
<div className="newtab-spacer" />
|
||||
<div className="newtab-section">
|
||||
<TabColorSelector screen={screen} errorMessage={this.errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class WorkspaceView extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
toggleTabSettings() {
|
||||
mobx.action(() => {
|
||||
GlobalModel.tabSettingsOpen.set(!GlobalModel.tabSettingsOpen.get());
|
||||
})();
|
||||
}
|
||||
|
||||
render() {
|
||||
const model = GlobalModel;
|
||||
const session = model.getActiveSession();
|
||||
let activeScreen: Screen = null;
|
||||
let sessionId: string = "none";
|
||||
if (session != null) {
|
||||
sessionId = session.sessionId;
|
||||
activeScreen = session.getActiveScreen();
|
||||
}
|
||||
let activeScreen = session.getActiveScreen();
|
||||
let cmdInputHeight = model.inputModel.cmdInputHeight.get();
|
||||
if (cmdInputHeight == 0) {
|
||||
cmdInputHeight = textmeasure.baseCmdInputHeight(GlobalModel.lineHeightEnv); // this is the base size of cmdInput (measured using devtools)
|
||||
}
|
||||
let isHidden = GlobalModel.activeMainView.get() != "session";
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
const isHidden = GlobalModel.activeMainView.get() != "session";
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
const showTabSettings = GlobalModel.tabSettingsOpen.get();
|
||||
return (
|
||||
<div
|
||||
className={cn("mainview", "session-view", { "is-hidden": isHidden })}
|
||||
data-sessionid={session.sessionId}
|
||||
data-sessionid={sessionId}
|
||||
style={{
|
||||
width: `${window.innerWidth - mainSidebarModel.getWidth()}px`,
|
||||
}}
|
||||
>
|
||||
<If condition={!isHidden}>
|
||||
<SessionKeybindings></SessionKeybindings>
|
||||
<SessionKeybindings key="keybindings"></SessionKeybindings>
|
||||
</If>
|
||||
<ScreenTabs key={"tabs-" + session.sessionId} session={session} />
|
||||
<ErrorBoundary>
|
||||
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />
|
||||
<ScreenTabs key={"tabs-" + sessionId} session={session} />
|
||||
<If condition={activeScreen != null}>
|
||||
<div key="pulldown" className={cn("tab-settings-pulldown", { closed: !showTabSettings })}>
|
||||
<div className="close-icon" onClick={this.toggleTabSettings}>
|
||||
<i className="fa-solid fa-sharp fa-xmark-large" />
|
||||
</div>
|
||||
<TabSettings key={activeScreen.screenId} screen={activeScreen} />
|
||||
<If condition={showTabSettings}>
|
||||
<TabSettingsPulldownKeybindings />
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
<ErrorBoundary key="eb">
|
||||
<ScreenView key={"screenview-" + sessionId} session={session} screen={activeScreen} />
|
||||
<div className="cmdinput-height-placeholder" style={{ height: cmdInputHeight }}></div>
|
||||
<CmdInput key={"cmdinput-" + session.sessionId} />
|
||||
<If condition={activeScreen != null}>
|
||||
<CmdInput key={"cmdinput-" + sessionId} />
|
||||
</If>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
@ -94,6 +94,10 @@ class CommandRunner {
|
||||
return GlobalModel.submitCommand("line", "set", [lineArg], kwargs, false);
|
||||
}
|
||||
|
||||
ensureWorkspace() {
|
||||
GlobalModel.submitCommand("session", "ensureone", null, { nohist: "1" }, true);
|
||||
}
|
||||
|
||||
createNewSession() {
|
||||
GlobalModel.submitCommand("session", "open", null, { nohist: "1" }, false);
|
||||
}
|
||||
|
@ -104,6 +104,9 @@ class Model {
|
||||
devicePixelRatio: OV<number> = mobx.observable.box(window.devicePixelRatio, {
|
||||
name: "devicePixelRatio",
|
||||
});
|
||||
tabSettingsOpen: OV<boolean> = mobx.observable.box(false, {
|
||||
name: "tabSettingsOpen",
|
||||
});
|
||||
remotesModel: RemotesModel;
|
||||
lineHeightEnv: LineHeightEnv;
|
||||
|
||||
@ -251,6 +254,14 @@ class Model {
|
||||
return (window as any).GlobalModel;
|
||||
}
|
||||
|
||||
closeTabSettings() {
|
||||
if (this.tabSettingsOpen.get()) {
|
||||
mobx.action(() => {
|
||||
this.tabSettingsOpen.set(false);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
toggleDevUI(): void {
|
||||
document.body.classList.toggle("is-dev");
|
||||
}
|
||||
@ -519,7 +530,7 @@ class Model {
|
||||
return;
|
||||
}
|
||||
const rtnp = this.showAlert({
|
||||
message: "Are you sure you want to delete this screen?",
|
||||
message: "Are you sure you want to delete this tab?",
|
||||
confirm: true,
|
||||
});
|
||||
rtnp.then((result) => {
|
||||
@ -586,7 +597,7 @@ class Model {
|
||||
|
||||
getLocalRemote(): RemoteType {
|
||||
for (const remote of this.remotes) {
|
||||
if (remote.local) {
|
||||
if (remote.local && !remote.issudo) {
|
||||
return remote;
|
||||
}
|
||||
}
|
||||
@ -811,6 +822,12 @@ class Model {
|
||||
}
|
||||
}
|
||||
|
||||
markScreensAsNotNew(): void {
|
||||
for (const screen of this.screenMap.values()) {
|
||||
screen.isNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateSessions(sessions: SessionDataType[]): void {
|
||||
genMergeData(
|
||||
this.sessionList,
|
||||
@ -864,6 +881,7 @@ class Model {
|
||||
if (update.connect.screens != null) {
|
||||
this.screenMap.clear();
|
||||
this.updateScreens(update.connect.screens);
|
||||
this.markScreensAsNotNew();
|
||||
}
|
||||
if (update.connect.sessions != null) {
|
||||
this.sessionList.clear();
|
||||
@ -955,6 +973,8 @@ class Model {
|
||||
} else if (update.userinputrequest != null) {
|
||||
const userInputRequest: UserInputRequest = update.userinputrequest;
|
||||
this.modalsModel.pushModal(appconst.USER_INPUT, userInputRequest);
|
||||
} else if (update.sessiontombstone != null || update.screentombstone != null) {
|
||||
// nothing (ignore)
|
||||
} else {
|
||||
// interactive-only updates follow below
|
||||
// we check interactive *inside* of the conditions because of isDev console.log message
|
||||
@ -995,9 +1015,13 @@ class Model {
|
||||
this.activeMainView.set("session");
|
||||
this.deactivateScreenLines();
|
||||
this.ws.watchScreen(newActiveSessionId, newActiveScreenId);
|
||||
setTimeout(() => {
|
||||
GlobalCommandRunner.syncShellState();
|
||||
}, 100);
|
||||
this.closeTabSettings();
|
||||
const activeScreen = this.getActiveScreen();
|
||||
if (activeScreen != null && activeScreen.getCurRemoteInstance() != null) {
|
||||
setTimeout(() => {
|
||||
GlobalCommandRunner.syncShellState();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("unknown update", genUpdate);
|
||||
|
@ -44,6 +44,7 @@ class Screen {
|
||||
filterRunning: OV<boolean>;
|
||||
statusIndicator: OV<appconst.StatusIndicatorLevel>;
|
||||
numRunningCmds: OV<number>;
|
||||
isNew: boolean; // used for showing screen settings on initial screen creation
|
||||
|
||||
constructor(sdata: ScreenDataType, globalModel: Model) {
|
||||
this.globalModel = globalModel;
|
||||
@ -91,6 +92,7 @@ class Screen {
|
||||
this.numRunningCmds = mobx.observable.box(0, {
|
||||
name: "screen-num-running-cmds",
|
||||
});
|
||||
this.isNew = true;
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
@ -469,7 +471,7 @@ class Screen {
|
||||
this.renderers[lineId] = renderer;
|
||||
}
|
||||
|
||||
setLineFocus(lineNum: number, lineid: string, focus: boolean): void {
|
||||
setLineFocus(lineNum: number, focus: boolean): void {
|
||||
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
|
||||
if (focus && this.selectedLine.get() != lineNum) {
|
||||
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
|
||||
@ -525,7 +527,7 @@ class Screen {
|
||||
termOpts: cmd.getTermOpts(),
|
||||
winSize: { height: 0, width: width },
|
||||
dataHandler: cmd.handleData.bind(cmd),
|
||||
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, line.lineid, focus),
|
||||
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, focus),
|
||||
isRunning: cmd.isRunning(),
|
||||
customKeyHandler: this.termCustomKeyHandler.bind(this),
|
||||
fontSize: this.globalModel.getTermFontSize(),
|
||||
|
@ -95,19 +95,6 @@ class Session {
|
||||
return rdata;
|
||||
}
|
||||
}
|
||||
let remote = this.globalModel.getRemote(rptr.remoteid);
|
||||
if (remote != null) {
|
||||
return {
|
||||
riid: "",
|
||||
sessionid: this.sessionId,
|
||||
screenid: screenId,
|
||||
remoteownerid: rptr.ownerid,
|
||||
remoteid: rptr.remoteid,
|
||||
name: rptr.name,
|
||||
festate: remote.defaultfestate,
|
||||
shelltype: remote.defaultshelltype,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1 @@
|
||||
# Code Editor for Wave Terminal
|
||||
|
||||
These instructions are for setting up the build on MacOS.
|
||||
If you're developing on Linux please use the [Linux Build Instructions](./build-linux.md).
|
||||
|
||||
## Running the Development Version of Wave
|
||||
|
||||
If you install the production version of Wave, you'll see a semi-transparent sidebar, and the data for Wave is stored in the directory ~/prompt. The development version has a red/brown sidebar and stores its data in ~/prompt-dev. This allows the production and development versions to be run simultaneously with no conflicts. If the dev database is corrupted by development bugs, or the schema changes in development it will not affect the production copy.
|
||||
# CodeEdit
|
||||
|
4
src/types/custom.d.ts
vendored
4
src/types/custom.d.ts
vendored
@ -112,7 +112,6 @@ declare global {
|
||||
errorstr: string;
|
||||
installstatus: string;
|
||||
installerrorstr: string;
|
||||
defaultfestate: Record<string, string>;
|
||||
connectmode: string;
|
||||
autoinstall: boolean;
|
||||
remoteidx: number;
|
||||
@ -126,6 +125,7 @@ declare global {
|
||||
waitingforpassword: boolean;
|
||||
remoteopts?: RemoteOptsType;
|
||||
local: boolean;
|
||||
issudo: boolean;
|
||||
remove?: boolean;
|
||||
shellpref: string;
|
||||
defaultshelltype: string;
|
||||
@ -366,6 +366,8 @@ declare global {
|
||||
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
|
||||
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
|
||||
userinputrequest?: UserInputRequest;
|
||||
screentombstone?: any;
|
||||
sessiontombstone?: any;
|
||||
};
|
||||
|
||||
type HistoryViewDataType = {
|
||||
|
@ -365,9 +365,11 @@ function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<strin
|
||||
}
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
errorMessage.set(crtn.error);
|
||||
})();
|
||||
if (errorMessage != null) {
|
||||
mobx.action(() => {
|
||||
errorMessage.set(crtn.error);
|
||||
})();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
EC_InvalidCwd = "ERRCWD"
|
||||
EC_InvalidCwd = "ERRCWD"
|
||||
EC_CmdNotRunning = "CMDNOTRUNNING"
|
||||
)
|
||||
|
||||
const PacketSenderQueueSize = 20
|
||||
|
@ -353,7 +353,9 @@ func (m *MServer) MakeShellStatePacket(reqId string, shellType string, stdinData
|
||||
return nil, err
|
||||
}
|
||||
rtnCh := make(chan shellapi.ShellStateOutput, 1)
|
||||
go sapi.GetShellState(rtnCh, stdinDataCh)
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
defer cancelFn()
|
||||
go sapi.GetShellState(ctx, rtnCh, stdinDataCh)
|
||||
for ssOutput := range rtnCh {
|
||||
if ssOutput.Error != "" {
|
||||
return nil, errors.New(ssOutput.Error)
|
||||
@ -746,11 +748,6 @@ func (m *MServer) runCommand(runPacket *packet.RunPacketType) {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require shell type"))
|
||||
return
|
||||
}
|
||||
_, curInitState := m.StateMap.GetCurrentState(runPacket.ShellType)
|
||||
if curInitState == nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("shell type %q is not initialized", runPacket.ShellType))
|
||||
return
|
||||
}
|
||||
if runPacket.State == nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require state"))
|
||||
return
|
||||
@ -760,10 +757,6 @@ func (m *MServer) runCommand(runPacket *packet.RunPacketType) {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("invalid shellstate version: %w", err))
|
||||
return
|
||||
}
|
||||
if !packet.StateVersionsCompatible(runPacket.State.Version, curInitState.Version) {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("shellstate version %q is not compatible with current shell version %q", runPacket.State.Version, curInitState.Version))
|
||||
return
|
||||
}
|
||||
ecmd, err := shexec.MakeMShellSingleCmd()
|
||||
if err != nil {
|
||||
m.Sender.SendErrorResponse(runPacket.ReqId, fmt.Errorf("server run packets require valid ck: %s", err))
|
||||
|
@ -80,8 +80,8 @@ func (b bashShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty
|
||||
return MakeBashShExecCommand(cmdStr, rcFileName, usePty)
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetShellState(outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
||||
GetBashShellState(outCh, stdinDataCh)
|
||||
func (b bashShellApi) GetShellState(ctx context.Context, outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
||||
GetBashShellState(ctx, outCh, stdinDataCh)
|
||||
}
|
||||
|
||||
func (b bashShellApi) GetBaseShellOpts() string {
|
||||
@ -146,7 +146,7 @@ printf "[%ENDBYTES%]";
|
||||
}
|
||||
|
||||
func execGetLocalBashShellVersion() string {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetVersionTimeout)
|
||||
defer cancelFn()
|
||||
ecmd := exec.CommandContext(ctx, "bash", "-c", BashShellVersionCmdStr)
|
||||
out, err := ecmd.Output()
|
||||
@ -169,9 +169,7 @@ func GetLocalBashMajorVersion() string {
|
||||
return localBashMajorVersion
|
||||
}
|
||||
|
||||
func GetBashShellState(outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
||||
defer cancelFn()
|
||||
func GetBashShellState(ctx context.Context, outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
||||
defer close(outCh)
|
||||
stateCmd, endBytes := GetBashShellStateCmd(StateOutputFdNum)
|
||||
cmdStr := BaseBashOpts + "; " + stateCmd
|
||||
|
@ -28,8 +28,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||
)
|
||||
|
||||
const GetStateTimeout = 10 * time.Second
|
||||
const ReInitTimeout = GetStateTimeout + 2*time.Second
|
||||
const GetVersionTimeout = 5 * time.Second
|
||||
const GetGitBranchCmdStr = `printf "GITBRANCH %s\x00" "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"`
|
||||
const GetK8sContextCmdStr = `printf "K8SCONTEXT %s\x00" "$(kubectl config current-context 2>/dev/null)"`
|
||||
const GetK8sNamespaceCmdStr = `printf "K8SNAMESPACE %s\x00" "$(kubectl config view --minify --output 'jsonpath={..namespace}' 2>/dev/null)"`
|
||||
@ -73,7 +72,7 @@ type ShellApi interface {
|
||||
GetRemoteShellPath() string
|
||||
MakeRunCommand(cmdStr string, opts RunCommandOpts) string
|
||||
MakeShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd
|
||||
GetShellState(outCh chan ShellStateOutput, stdinDataCh chan []byte)
|
||||
GetShellState(ctx context.Context, outCh chan ShellStateOutput, stdinDataCh chan []byte)
|
||||
GetBaseShellOpts() string
|
||||
ParseShellStateOutput(output []byte) (*packet.ShellState, *packet.ShellStateStats, error)
|
||||
MakeRcFileStr(pk *packet.RunPacketType) string
|
||||
|
@ -246,9 +246,7 @@ func (z zshShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty
|
||||
return exec.Command(GetLocalZshPath(), "-l", "-i", "-c", cmdStr)
|
||||
}
|
||||
|
||||
func (z zshShellApi) GetShellState(outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
||||
defer cancelFn()
|
||||
func (z zshShellApi) GetShellState(ctx context.Context, outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
||||
defer close(outCh)
|
||||
stateCmd, endBytes := GetZshShellStateCmd(StateOutputFdNum)
|
||||
cmdStr := BaseZshOpts + "; " + stateCmd
|
||||
@ -547,7 +545,7 @@ zshexit () {
|
||||
}
|
||||
|
||||
func execGetLocalZshShellVersion() string {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), GetVersionTimeout)
|
||||
defer cancelFn()
|
||||
ecmd := exec.CommandContext(ctx, "zsh", "-c", ZshShellVersionCmdStr)
|
||||
out, err := ecmd.Output()
|
||||
|
12
waveshell/pkg/utilfn/ansi.go
Normal file
12
waveshell/pkg/utilfn/ansi.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utilfn
|
||||
|
||||
func AnsiResetColor() string {
|
||||
return "\033[0m"
|
||||
}
|
||||
|
||||
func AnsiGreenColor() string {
|
||||
return "\033[32m"
|
||||
}
|
@ -978,11 +978,6 @@ func main() {
|
||||
log.Printf("[error] ensuring local remote: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = sstore.EnsureOneSession(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("[error] ensuring default session: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = remote.LoadRemotes(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("[error] loading remotes: %v\n", err)
|
||||
|
@ -30,7 +30,6 @@ import (
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/server"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellapi"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
|
||||
@ -96,6 +95,7 @@ const (
|
||||
KwArgTemplate = "template"
|
||||
KwArgLang = "lang"
|
||||
KwArgMinimap = "minimap"
|
||||
KwArgNoHist = "nohist"
|
||||
)
|
||||
|
||||
var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "orange", "green", "red", "white"}
|
||||
@ -190,6 +190,7 @@ func init() {
|
||||
registerCmdFn("session:showall", SessionShowAllCommand)
|
||||
registerCmdFn("session:show", SessionShowCommand)
|
||||
registerCmdFn("session:openshared", SessionOpenSharedCommand)
|
||||
registerCmdFn("session:ensureone", SessionEnsureOneCommand)
|
||||
|
||||
registerCmdFn("screen", ScreenCommand)
|
||||
registerCmdFn("screen:archive", ScreenArchiveCommand)
|
||||
@ -364,6 +365,20 @@ func resolveCommaSepListToMap(arg string) map[string]bool {
|
||||
return rtn
|
||||
}
|
||||
|
||||
func resolveShellType(shellArg string, defaultShell string) (string, error) {
|
||||
if shellArg == "" {
|
||||
if defaultShell == "" {
|
||||
shellArg = packet.ShellType_bash
|
||||
} else {
|
||||
shellArg = defaultShell
|
||||
}
|
||||
}
|
||||
if shellArg != packet.ShellType_bash && shellArg != packet.ShellType_zsh {
|
||||
return "", fmt.Errorf("invalid shell type %q", shellArg)
|
||||
}
|
||||
return shellArg, nil
|
||||
}
|
||||
|
||||
func resolveBool(arg string, def bool) bool {
|
||||
if arg == "" {
|
||||
return def
|
||||
@ -723,7 +738,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
|
||||
} else {
|
||||
return nil, fmt.Errorf("error in Eval Meta Command: %w", rtnErr)
|
||||
}
|
||||
if !resolveBool(pk.Kwargs["nohist"], false) {
|
||||
if !resolveBool(pk.Kwargs[KwArgNoHist], false) {
|
||||
// TODO should this be "pk" or "newPk" (2nd arg)
|
||||
err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil))
|
||||
if err != nil {
|
||||
@ -816,6 +831,14 @@ func ScreenDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
if screenId == "" {
|
||||
return nil, fmt.Errorf("/screen:delete no active screen or screen arg passed")
|
||||
}
|
||||
runningCmds, err := sstore.GetRunningScreenCmds(ctx, screenId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/screen:delete cannot get running cmds: %v", err)
|
||||
}
|
||||
for _, runningCmd := range runningCmds {
|
||||
// send SIGHUP to all running commands in this screen
|
||||
remote.SendSignalToCmd(ctx, runningCmd, "SIGHUP")
|
||||
}
|
||||
update, err := sstore.DeleteScreen(ctx, screenId, false, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -826,7 +849,7 @@ func ScreenDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
|
||||
func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
ids, err := resolveUiIds(ctx, pk, R_Session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/screen:open cannot open screen: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
activate := resolveBool(pk.Kwargs["activate"], true)
|
||||
newName := pk.Kwargs["name"]
|
||||
@ -836,13 +859,37 @@ func ScreenOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
update, err := sstore.InsertScreen(ctx, ids.SessionId, newName, sstore.ScreenCreateOpts{}, activate)
|
||||
sco := sstore.ScreenCreateOpts{RtnScreenId: new(string)}
|
||||
update, err := sstore.InsertScreen(ctx, ids.SessionId, newName, sco, activate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sco.RtnScreenId == nil {
|
||||
return nil, fmt.Errorf("error creating tab, no tab id returned")
|
||||
}
|
||||
uiContextCopy := *pk.UIContext
|
||||
uiContextCopy.ScreenId = *sco.RtnScreenId
|
||||
crUpdate, err := doNewTabConnectLocal(ctx, *sco.RtnScreenId, &uiContextCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
update.Merge(crUpdate)
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func doNewTabConnectLocal(ctx context.Context, screenId string, uiContext *scpacket.UIContextType) (scbus.UpdatePacket, error) {
|
||||
crPk := scpacket.MakeFeCommandPacket()
|
||||
crPk.MetaCmd = "connect"
|
||||
crPk.Args = []string{"local"}
|
||||
crPk.RawStr = "/connect local"
|
||||
crPk.UIContext = uiContext
|
||||
crUpdate, err := CrCommand(ctx, crPk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating tab, cannot connect to remote: %w", err)
|
||||
}
|
||||
return crUpdate, nil
|
||||
}
|
||||
|
||||
func ScreenReorderCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
// Resolve the UI IDs for the session and screen
|
||||
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
||||
@ -1666,7 +1713,7 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
|
||||
return nil, fmt.Errorf("cannot make termopts: %w", err)
|
||||
}
|
||||
pkTermOpts := convertTermOpts(termOpts)
|
||||
cmd, err := makeDynCmd(ctx, "copy file", ids, pk.GetRawStr(), *pkTermOpts)
|
||||
cmd, err := makeDynCmd(ctx, "copy file", ids, pk.GetRawStr(), *pkTermOpts, nil)
|
||||
writeStringToPty(ctx, cmd, outputStr, &outputPos)
|
||||
if err != nil {
|
||||
// TODO tricky error since the command was a success, but we can't show the output
|
||||
@ -2462,10 +2509,16 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get remote instances: %w", err)
|
||||
}
|
||||
rmap := remote.GetRemoteMap()
|
||||
if len(riArr) == 0 {
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(sstore.InfoMsgType{
|
||||
InfoMsg: "this tab has no shell states",
|
||||
})
|
||||
return update, nil
|
||||
}
|
||||
for _, ri := range riArr {
|
||||
rptr := sstore.RemotePtrType{RemoteId: ri.RemoteId, Name: ri.Name}
|
||||
msh := rmap[ri.RemoteId]
|
||||
msh := remote.GetRemoteById(ri.RemoteId)
|
||||
if msh == nil {
|
||||
continue
|
||||
}
|
||||
@ -2477,28 +2530,9 @@ func crShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType, ids re
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%-30s %-50s\n", displayName, cwdStr))
|
||||
}
|
||||
riBaseMap := make(map[string]bool)
|
||||
for _, ri := range riArr {
|
||||
if ri.Name == "" {
|
||||
riBaseMap[ri.RemoteId] = true
|
||||
}
|
||||
}
|
||||
for remoteId, msh := range rmap {
|
||||
if riBaseMap[remoteId] {
|
||||
continue
|
||||
}
|
||||
feState := msh.GetDefaultFeState(msh.GetShellPref())
|
||||
if feState == nil {
|
||||
continue
|
||||
}
|
||||
cwdStr := "-"
|
||||
if feState["cwd"] != "" {
|
||||
cwdStr = feState["cwd"]
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("%-30s %-50s (default)\n", msh.GetDisplayName(), cwdStr))
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(sstore.InfoMsgType{
|
||||
InfoTitle: "shell states for tab",
|
||||
InfoLines: splitLinesForInfo(buf.String()),
|
||||
})
|
||||
return update, nil
|
||||
@ -2854,7 +2888,7 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus
|
||||
return nil, fmt.Errorf("openai error, invalid 'pterm' value %q: %v", ptermVal, err)
|
||||
}
|
||||
termOpts := convertTermOpts(pkTermOpts)
|
||||
cmd, err := makeDynCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), *termOpts)
|
||||
cmd, err := makeDynCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), *termOpts, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openai error, cannot make dyn cmd")
|
||||
}
|
||||
@ -2936,50 +2970,104 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.Upd
|
||||
if rstate.Archived {
|
||||
return nil, fmt.Errorf("/%s error: remote %q cannot switch to archived remote", GetCmdStr(pk), newRemote)
|
||||
}
|
||||
newMsh := remote.GetRemoteById(rptr.RemoteId)
|
||||
if newMsh == nil {
|
||||
return nil, fmt.Errorf("/%s error: remote %q not found (msh)", GetCmdStr(pk), newRemote)
|
||||
}
|
||||
if !newMsh.IsConnected() {
|
||||
err := newMsh.TryAutoConnect()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%q is disconnected, auto-connect failed: %w", rstate.GetBaseDisplayName(), err)
|
||||
}
|
||||
if !newMsh.IsConnected() {
|
||||
if newMsh.GetRemoteCopy().ConnectMode == sstore.ConnectModeManual {
|
||||
return nil, fmt.Errorf("%q is disconnected (must manually connect)", rstate.GetBaseDisplayName())
|
||||
}
|
||||
return nil, fmt.Errorf("%q is disconnected", rstate.GetBaseDisplayName())
|
||||
}
|
||||
}
|
||||
err = sstore.UpdateCurRemote(ctx, ids.ScreenId, *rptr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/%s error: cannot update curremote: %w", GetCmdStr(pk), err)
|
||||
}
|
||||
noHist := resolveBool(pk.Kwargs["nohist"], false)
|
||||
if noHist {
|
||||
screen, err := sstore.GetScreenById(ctx, ids.ScreenId)
|
||||
ri, err := sstore.GetRemoteStatePtr(ctx, ids.SessionId, ids.ScreenId, *rptr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/%s error looking up connection state: %w", GetCmdStr(pk), err)
|
||||
}
|
||||
if ri == nil {
|
||||
// ok, if ri is nil we need to do a reinit
|
||||
verbose := resolveBool(pk.Kwargs["verbose"], false)
|
||||
shellType, err := resolveShellType(pk.Kwargs["shell"], rstate.DefaultShellType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/%s error: cannot resolve screen for update: %w", GetCmdStr(pk), err)
|
||||
return nil, err
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(*screen, sstore.InteractiveUpdate(pk.Interactive))
|
||||
termOpts, err := GetUITermOpts(pk.UIContext.WinSize, DefaultPTERM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot make termopts: %w", err)
|
||||
}
|
||||
pkTermOpts := convertTermOpts(termOpts)
|
||||
cmd, err := makeDynCmd(ctx, "connect", ids, pk.GetRawStr(), *pkTermOpts, &makeDynCmdOpts{OverrideRPtr: rptr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
update, err := addLineForCmd(ctx, "connect", true, ids, cmd, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := connectOptsType{
|
||||
Verbose: verbose,
|
||||
ShellType: shellType,
|
||||
SessionId: ids.SessionId,
|
||||
ScreenId: ids.ScreenId,
|
||||
RPtr: *rptr,
|
||||
}
|
||||
go doAsyncResetCommand(newMsh, opts, cmd)
|
||||
return update, nil
|
||||
} else {
|
||||
outputStr := fmt.Sprintf("reconnected to %s", GetFullRemoteDisplayName(rptr, rstate))
|
||||
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
||||
if err != nil {
|
||||
// TODO tricky error since the command was a success, but we can't show the output
|
||||
return nil, err
|
||||
}
|
||||
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "", nil)
|
||||
if err != nil {
|
||||
// TODO tricky error since the command was a success, but we can't show the output
|
||||
return nil, err
|
||||
}
|
||||
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
|
||||
return update, nil
|
||||
}
|
||||
outputStr := fmt.Sprintf("connected to %s", GetFullRemoteDisplayName(rptr, rstate))
|
||||
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
|
||||
if err != nil {
|
||||
// TODO tricky error since the command was a success, but we can't show the output
|
||||
return nil, err
|
||||
}
|
||||
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "", nil)
|
||||
if err != nil {
|
||||
// TODO tricky error since the command was a success, but we can't show the output
|
||||
return nil, err
|
||||
}
|
||||
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, termOpts sstore.TermOpts) (*sstore.CmdType, error) {
|
||||
type makeDynCmdOpts struct {
|
||||
OverrideRPtr *sstore.RemotePtrType
|
||||
}
|
||||
|
||||
func makeDynCmd(ctx context.Context, metaCmd string, ids resolvedIds, cmdStr string, termOpts sstore.TermOpts, opts *makeDynCmdOpts) (*sstore.CmdType, error) {
|
||||
var rptr scpacket.RemotePtrType
|
||||
if opts != nil && opts.OverrideRPtr != nil {
|
||||
rptr = *opts.OverrideRPtr
|
||||
} else if ids.Remote != nil {
|
||||
rptr = ids.Remote.RemotePtr
|
||||
} else {
|
||||
local := remote.GetLocalRemote()
|
||||
rptr = scpacket.RemotePtrType{RemoteId: local.RemoteId}
|
||||
}
|
||||
cmd := &sstore.CmdType{
|
||||
ScreenId: ids.ScreenId,
|
||||
LineId: scbase.GenWaveUUID(),
|
||||
CmdStr: cmdStr,
|
||||
RawCmdStr: cmdStr,
|
||||
Remote: ids.Remote.RemotePtr,
|
||||
Remote: rptr,
|
||||
TermOpts: termOpts,
|
||||
Status: sstore.CmdStatusRunning,
|
||||
RunOut: nil,
|
||||
}
|
||||
if ids.Remote.StatePtr != nil {
|
||||
if ids.Remote != nil && ids.Remote.StatePtr != nil {
|
||||
cmd.StatePtr = *ids.Remote.StatePtr
|
||||
}
|
||||
if ids.Remote.FeState != nil {
|
||||
if ids.Remote != nil && ids.Remote.FeState != nil {
|
||||
cmd.FeState = ids.Remote.FeState
|
||||
}
|
||||
err := sstore.CreateCmdPtyFile(ctx, cmd.ScreenId, cmd.LineId, cmd.TermOpts.MaxPtySize)
|
||||
@ -3369,13 +3457,32 @@ func SessionOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
update, err := sstore.InsertSessionWithName(ctx, newName, activate)
|
||||
update, newSessionId, newScreenId, err := sstore.InsertSessionWithName(ctx, newName, activate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uiContextCopy := *pk.UIContext
|
||||
uiContextCopy.SessionId = newSessionId
|
||||
uiContextCopy.ScreenId = newScreenId
|
||||
crUpdate, err := doNewTabConnectLocal(ctx, newScreenId, &uiContextCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
update.Merge(crUpdate)
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func SessionEnsureOneCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
|
||||
numSessions, err := sstore.GetSessionCount(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get number of sessions: %v", err)
|
||||
}
|
||||
if numSessions > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return SessionOpenCommand(ctx, pk)
|
||||
}
|
||||
|
||||
func makeExternLink(urlStr string) string {
|
||||
return fmt.Sprintf(`https://extern?%s`, url.QueryEscape(urlStr))
|
||||
}
|
||||
@ -3471,7 +3578,7 @@ func ScreenShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
||||
if screen == nil {
|
||||
return nil, fmt.Errorf("screen not found")
|
||||
}
|
||||
statePtr, err := remote.ResolveCurrentScreenStatePtr(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr)
|
||||
statePtr, err := sstore.GetRemoteStatePtr(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot resolve current screen stateptr: %v", err)
|
||||
}
|
||||
@ -3483,8 +3590,10 @@ func ScreenShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "tabicon", screen.ScreenOpts.TabIcon))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %d\n", "selectedline", screen.SelectedLine))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "curremote", GetFullRemoteDisplayName(&screen.CurRemote, &ids.Remote.RState)))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "stateptr-base", statePtr.BaseHash))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %v\n", "stateptr-diff", statePtr.DiffHashArr))
|
||||
if statePtr != nil {
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %s\n", "stateptr-base", statePtr.BaseHash))
|
||||
buf.WriteString(fmt.Sprintf(" %-15s %v\n", "stateptr-diff", statePtr.DiffHashArr))
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(sstore.InfoMsgType{
|
||||
InfoTitle: "screen info",
|
||||
@ -3682,21 +3791,17 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
|
||||
if !ids.Remote.MShell.IsConnected() {
|
||||
return nil, fmt.Errorf("cannot reinit, remote is not connected")
|
||||
}
|
||||
shellType := ids.Remote.ShellType
|
||||
if pk.Kwargs["shell"] != "" {
|
||||
shellArg := pk.Kwargs["shell"]
|
||||
if shellArg != packet.ShellType_bash && shellArg != packet.ShellType_zsh {
|
||||
return nil, fmt.Errorf("/reset invalid shell type %q", shellArg)
|
||||
}
|
||||
shellType = shellArg
|
||||
}
|
||||
verbose := resolveBool(pk.Kwargs["verbose"], false)
|
||||
shellType, err := resolveShellType(pk.Kwargs["shell"], ids.Remote.ShellType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
termOpts, err := GetUITermOpts(pk.UIContext.WinSize, DefaultPTERM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot make termopts: %w", err)
|
||||
}
|
||||
pkTermOpts := convertTermOpts(termOpts)
|
||||
cmd, err := makeDynCmd(ctx, "reset", ids, pk.GetRawStr(), *pkTermOpts)
|
||||
cmd, err := makeDynCmd(ctx, "reset", ids, pk.GetRawStr(), *pkTermOpts, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -3704,12 +3809,28 @@ func RemoteResetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go doResetCommand(ids, shellType, cmd, verbose)
|
||||
opts := connectOptsType{
|
||||
Verbose: verbose,
|
||||
ShellType: shellType,
|
||||
SessionId: ids.SessionId,
|
||||
ScreenId: ids.ScreenId,
|
||||
RPtr: ids.Remote.RemotePtr,
|
||||
}
|
||||
go doAsyncResetCommand(ids.Remote.MShell, opts, cmd)
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func doResetCommand(ids resolvedIds, shellType string, cmd *sstore.CmdType, verbose bool) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), shellapi.ReInitTimeout)
|
||||
type connectOptsType struct {
|
||||
ShellType string // shell type to connect with
|
||||
Verbose bool // extra output (show state changes, sizes, etc.)
|
||||
SessionId string
|
||||
ScreenId string
|
||||
RPtr sstore.RemotePtrType
|
||||
}
|
||||
|
||||
// this does the asynchroneous part of the connection reset
|
||||
func doAsyncResetCommand(msh *remote.MShellProc, opts connectOptsType, cmd *sstore.CmdType) {
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
defer cancelFn()
|
||||
startTime := time.Now()
|
||||
var outputPos int64
|
||||
@ -3725,28 +3846,30 @@ func doResetCommand(ids resolvedIds, shellType string, cmd *sstore.CmdType, verb
|
||||
dataFn := func(data []byte) {
|
||||
writeStringToPty(ctx, cmd, string(data), &outputPos)
|
||||
}
|
||||
origStatePtr := ids.Remote.MShell.GetDefaultStatePtr(shellType)
|
||||
ssPk, err := ids.Remote.MShell.ReInit(ctx, base.MakeCommandKey(cmd.ScreenId, cmd.LineId), shellType, dataFn, verbose)
|
||||
origStatePtr, _ := sstore.GetRemoteStatePtr(ctx, opts.SessionId, opts.ScreenId, opts.RPtr)
|
||||
ssPk, err := msh.ReInit(ctx, base.MakeCommandKey(cmd.ScreenId, cmd.LineId), opts.ShellType, dataFn, opts.Verbose)
|
||||
if err != nil {
|
||||
rtnErr = err
|
||||
return
|
||||
}
|
||||
if ssPk == nil || ssPk.State == nil {
|
||||
rtnErr = fmt.Errorf("invalid initpk received from remote (no remote state)")
|
||||
rtnErr = fmt.Errorf("no state received from connection (nil)")
|
||||
return
|
||||
}
|
||||
feState := sstore.FeStateFromShellState(ssPk.State)
|
||||
remoteInst, err := sstore.UpdateRemoteState(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr, feState, ssPk.State, nil)
|
||||
remoteInst, err := sstore.UpdateRemoteState(ctx, opts.SessionId, opts.ScreenId, opts.RPtr, feState, ssPk.State, nil)
|
||||
if err != nil {
|
||||
rtnErr = err
|
||||
return
|
||||
}
|
||||
newStatePtr := ids.Remote.MShell.GetDefaultStatePtr(shellType)
|
||||
if verbose && origStatePtr != nil && newStatePtr != nil {
|
||||
newStatePtr := sstore.ShellStatePtr{
|
||||
BaseHash: ssPk.State.GetHashVal(false),
|
||||
}
|
||||
if opts.Verbose && origStatePtr != nil {
|
||||
statePtrDiff := fmt.Sprintf("oldstate: %v, newstate: %v\r\n", origStatePtr.BaseHash, newStatePtr.BaseHash)
|
||||
writeStringToPty(ctx, cmd, statePtrDiff, &outputPos)
|
||||
origFullState, _ := sstore.GetFullState(ctx, *origStatePtr)
|
||||
newFullState, _ := sstore.GetFullState(ctx, *newStatePtr)
|
||||
newFullState, _ := sstore.GetFullState(ctx, newStatePtr)
|
||||
if origFullState != nil && newFullState != nil {
|
||||
var diffBuf bytes.Buffer
|
||||
rtnstate.DisplayStateUpdateDiff(&diffBuf, *origFullState, *newFullState)
|
||||
@ -3756,7 +3879,7 @@ func doResetCommand(ids resolvedIds, shellType string, cmd *sstore.CmdType, verb
|
||||
}
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(sstore.MakeSessionUpdateForRemote(ids.SessionId, remoteInst))
|
||||
update.AddUpdate(sstore.MakeSessionUpdateForRemote(opts.SessionId, remoteInst))
|
||||
scbus.MainUpdateBus.DoUpdate(update)
|
||||
}
|
||||
|
||||
@ -3765,10 +3888,13 @@ func ResetCwdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statePtr, err := remote.ResolveCurrentScreenStatePtr(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr)
|
||||
statePtr, err := sstore.GetRemoteStatePtr(ctx, ids.SessionId, ids.ScreenId, ids.Remote.RemotePtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if statePtr == nil {
|
||||
return nil, fmt.Errorf("no shell state found, cannot reset cwd (run /reset)")
|
||||
}
|
||||
stateDiff, err := sstore.GetCurStateDiffFromPtr(ctx, statePtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -41,7 +41,7 @@ type ResolvedRemote struct {
|
||||
MShell *remote.MShellProc
|
||||
RState remote.RemoteRuntimeState
|
||||
RemoteCopy *sstore.RemoteType
|
||||
ShellType string
|
||||
ShellType string // default remote shell preference
|
||||
StatePtr *sstore.ShellStatePtr
|
||||
FeState map[string]string
|
||||
}
|
||||
@ -488,8 +488,8 @@ func ResolveRemoteFromPtr(ctx context.Context, rptr *sstore.RemotePtrType, sessi
|
||||
} else {
|
||||
if ri == nil {
|
||||
rtn.ShellType = msh.GetShellPref()
|
||||
rtn.StatePtr = msh.GetDefaultStatePtr(rtn.ShellType)
|
||||
rtn.FeState = msh.GetDefaultFeState(rtn.ShellType)
|
||||
rtn.StatePtr = nil
|
||||
rtn.FeState = nil
|
||||
} else {
|
||||
rtn.StatePtr = &sstore.ShellStatePtr{BaseHash: ri.StateBaseHash, DiffHashArr: ri.StateDiffHashArr}
|
||||
rtn.FeState = ri.FeState
|
||||
|
@ -314,7 +314,7 @@ func IsReturnStateCommand(cmdStr string) bool {
|
||||
func EvalBracketArgs(origCmdStr string) (map[string]string, string, error) {
|
||||
rtn := make(map[string]string)
|
||||
if strings.HasPrefix(origCmdStr, " ") {
|
||||
rtn["nohist"] = "1"
|
||||
rtn[KwArgNoHist] = "1"
|
||||
}
|
||||
cmdStr := strings.TrimSpace(origCmdStr)
|
||||
if !strings.HasPrefix(cmdStr, "[") {
|
||||
|
@ -214,38 +214,6 @@ func (msh *MShellProc) GetStatus() string {
|
||||
return msh.Status
|
||||
}
|
||||
|
||||
func (msh *MShellProc) GetDefaultState(shellType string) *packet.ShellState {
|
||||
_, state := msh.StateMap.GetCurrentState(shellType)
|
||||
return state
|
||||
}
|
||||
|
||||
func (msh *MShellProc) EnsureShellType(ctx context.Context, shellType string) error {
|
||||
if msh.StateMap.HasShell(shellType) {
|
||||
return nil
|
||||
}
|
||||
// try to reinit the shell
|
||||
_, err := msh.ReInit(ctx, base.CommandKey(""), shellType, nil, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error trying to initialize shell %q: %v", shellType, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (msh *MShellProc) GetDefaultStatePtr(shellType string) *sstore.ShellStatePtr {
|
||||
msh.Lock.Lock()
|
||||
defer msh.Lock.Unlock()
|
||||
hash, _ := msh.StateMap.GetCurrentState(shellType)
|
||||
if hash == "" {
|
||||
return nil
|
||||
}
|
||||
return &sstore.ShellStatePtr{BaseHash: hash}
|
||||
}
|
||||
|
||||
func (msh *MShellProc) GetDefaultFeState(shellType string) map[string]string {
|
||||
state := msh.GetDefaultState(shellType)
|
||||
return sstore.FeStateFromShellState(state)
|
||||
}
|
||||
|
||||
func (msh *MShellProc) GetRemoteId() string {
|
||||
msh.Lock.Lock()
|
||||
defer msh.Lock.Unlock()
|
||||
@ -489,6 +457,26 @@ func ResolveRemoteRef(remoteRef string) *RemoteRuntimeState {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendSignalToCmd(ctx context.Context, cmd *sstore.CmdType, sig string) error {
|
||||
msh := GetRemoteById(cmd.Remote.RemoteId)
|
||||
if msh == nil {
|
||||
return fmt.Errorf("no connection found")
|
||||
}
|
||||
if !msh.IsConnected() {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
cmdCk := base.MakeCommandKey(cmd.ScreenId, cmd.LineId)
|
||||
if !msh.IsCmdRunning(cmdCk) {
|
||||
// this could also return nil (depends on use case)
|
||||
// settled on coded error so we can check for this error
|
||||
return base.CodedErrorf(packet.EC_CmdNotRunning, "cmd not running")
|
||||
}
|
||||
sigPk := packet.MakeSpecialInputPacket()
|
||||
sigPk.CK = cmdCk
|
||||
sigPk.SigName = sig
|
||||
return msh.ServerProc.Input.SendPacket(sigPk)
|
||||
}
|
||||
|
||||
func unquoteDQBashString(str string) (string, bool) {
|
||||
if len(str) < 2 {
|
||||
return str, false
|
||||
@ -588,6 +576,7 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState {
|
||||
InstallStatus: msh.InstallStatus,
|
||||
NeedsMShellUpgrade: msh.NeedsMShellUpgrade,
|
||||
Local: msh.Remote.Local,
|
||||
IsSudo: msh.Remote.IsSudo(),
|
||||
NoInitPk: msh.ErrNoInitPk,
|
||||
AuthType: sstore.RemoteAuthTypeNone,
|
||||
ShellPref: msh.Remote.ShellPref,
|
||||
@ -661,11 +650,6 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState {
|
||||
vars["besthost"] = vars["remotehost"]
|
||||
vars["bestshorthost"] = vars["remoteshorthost"]
|
||||
}
|
||||
_, curState := msh.StateMap.GetCurrentState(shellPref)
|
||||
if curState != nil {
|
||||
state.DefaultFeState = sstore.FeStateFromShellState(curState)
|
||||
vars["cwd"] = curState.Cwd
|
||||
}
|
||||
if msh.Remote.Local && msh.Remote.IsSudo() {
|
||||
vars["bestuser"] = "sudo"
|
||||
} else if msh.Remote.IsSudo() {
|
||||
@ -687,7 +671,6 @@ func (msh *MShellProc) GetRemoteRuntimeState() RemoteRuntimeState {
|
||||
varsCopy[key] = value
|
||||
}
|
||||
state.RemoteVars = varsCopy
|
||||
state.ActiveShells = msh.StateMap.GetShells()
|
||||
return state
|
||||
}
|
||||
|
||||
@ -932,9 +915,9 @@ func (msh *MShellProc) writeToPtyBuffer_nolock(strFmt string, args ...interface{
|
||||
realStr = realStr + "\r\n"
|
||||
}
|
||||
if strings.HasPrefix(realStr, "*") {
|
||||
realStr = "\033[0m\033[31mprompt>\033[0m " + realStr[1:]
|
||||
realStr = "\033[0m\033[31mwave>\033[0m " + realStr[1:]
|
||||
} else {
|
||||
realStr = "\033[0m\033[32mprompt>\033[0m " + realStr
|
||||
realStr = "\033[0m\033[32mwave>\033[0m " + realStr
|
||||
}
|
||||
barr := msh.PtyBuffer.Bytes()
|
||||
if len(barr) > 0 && barr[len(barr)-1] != '\n' {
|
||||
@ -1509,18 +1492,18 @@ func (msh *MShellProc) ReInit(ctx context.Context, ck base.CommandKey, shellType
|
||||
}
|
||||
|
||||
func makeShellInitOutputMsg(verbose bool, state *packet.ShellState, stats *packet.ShellStateStats, dur time.Duration, ptyMsg bool) string {
|
||||
waveStr := fmt.Sprintf("%swave>%s", utilfn.AnsiGreenColor(), utilfn.AnsiResetColor())
|
||||
if !verbose || ptyMsg {
|
||||
if ptyMsg {
|
||||
return fmt.Sprintf("initialized state shell:%s statehash:%s %dms\n", state.GetShellType(), state.GetHashVal(false), dur.Milliseconds())
|
||||
} else {
|
||||
return fmt.Sprintf("initialized connection state (shell:%s)\r\n", state.GetShellType())
|
||||
return fmt.Sprintf("%s initialized connection state (shell:%s)\r\n", waveStr, state.GetShellType())
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("-----\r\n")
|
||||
buf.WriteString(fmt.Sprintf("initialized connection shell:%s statehash:%s %dms\r\n", state.GetShellType(), state.GetHashVal(false), dur.Milliseconds()))
|
||||
buf.WriteString(fmt.Sprintf("%s initialized connection shell:%s statehash:%s %dms\r\n", waveStr, state.GetShellType(), state.GetHashVal(false), dur.Milliseconds()))
|
||||
if stats != nil {
|
||||
buf.WriteString(fmt.Sprintf(" outsize:%s size:%s env:%d, vars:%d, aliases:%d, funcs:%d\r\n", scbase.NumFormatDec(stats.OutputSize), scbase.NumFormatDec(stats.StateSize), stats.EnvCount, stats.VarCount, stats.AliasCount, stats.FuncCount))
|
||||
buf.WriteString(fmt.Sprintf("%s outsize:%s size:%s env:%d, vars:%d, aliases:%d, funcs:%d\r\n", waveStr, scbase.NumFormatDec(stats.OutputSize), scbase.NumFormatDec(stats.StateSize), stats.EnvCount, stats.VarCount, stats.AliasCount, stats.FuncCount))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
@ -1762,7 +1745,7 @@ func (msh *MShellProc) Launch(interactive bool) {
|
||||
msh.WriteToPtyBuffer("*disconnected exitcode=%d\n", exitCode)
|
||||
}()
|
||||
go msh.ProcessPackets()
|
||||
msh.initActiveShells()
|
||||
// msh.initActiveShells()
|
||||
go msh.NotifyRemoteUpdate()
|
||||
}
|
||||
|
||||
@ -1780,7 +1763,7 @@ func (msh *MShellProc) initActiveShells() {
|
||||
wg.Add(1)
|
||||
go func(shellType string) {
|
||||
defer wg.Done()
|
||||
reinitCtx, cancelFn := context.WithTimeout(context.Background(), shellapi.ReInitTimeout)
|
||||
reinitCtx, cancelFn := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancelFn()
|
||||
_, err = msh.ReInit(reinitCtx, base.CommandKey(""), shellType, nil, false)
|
||||
if err != nil {
|
||||
@ -1894,25 +1877,6 @@ func (msh *MShellProc) removePendingStateCmd(screenId string, rptr sstore.Remote
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveCurrentScreenStatePtr(ctx context.Context, sessionId string, screenId string, remotePtr sstore.RemotePtrType) (*sstore.ShellStatePtr, error) {
|
||||
statePtr, err := sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get current connection stateptr: %w", err)
|
||||
}
|
||||
if statePtr == nil {
|
||||
msh := GetRemoteById(remotePtr.RemoteId)
|
||||
err := msh.EnsureShellType(ctx, msh.GetShellPref()) // make sure shellType is initialized
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statePtr = msh.GetDefaultStatePtr(msh.GetShellPref())
|
||||
if statePtr == nil {
|
||||
return nil, fmt.Errorf("no valid default connection stateptr")
|
||||
}
|
||||
}
|
||||
return statePtr, nil
|
||||
}
|
||||
|
||||
type RunCommandOpts struct {
|
||||
SessionId string
|
||||
ScreenId string
|
||||
@ -1990,10 +1954,13 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
||||
statePtr = rcOpts.StatePtr
|
||||
} else {
|
||||
var err error
|
||||
statePtr, err = ResolveCurrentScreenStatePtr(ctx, sessionId, screenId, remotePtr)
|
||||
statePtr, err = sstore.GetRemoteStatePtr(ctx, sessionId, screenId, remotePtr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command: %w", err)
|
||||
}
|
||||
if statePtr == nil {
|
||||
return nil, nil, fmt.Errorf("cannot run command: no valid shell state found")
|
||||
}
|
||||
}
|
||||
currentState, err := sstore.GetFullState(ctx, *statePtr)
|
||||
if err != nil || currentState == nil {
|
||||
@ -2002,10 +1969,6 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
||||
runPacket.State = addScVarsToState(currentState)
|
||||
runPacket.StateComplete = true
|
||||
runPacket.ShellType = currentState.GetShellType()
|
||||
err = msh.EnsureShellType(ctx, runPacket.ShellType) // make sure shellType is initialized
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// start cmdwait. must be started before sending the run packet
|
||||
// this ensures that we don't process output, or cmddone packets until we set up the line, cmd, and ptyout file
|
||||
@ -2144,7 +2107,7 @@ func (msh *MShellProc) HandleFeInput(inputPk *scpacket.FeInputPacketType) error
|
||||
msh.Lock.Unlock()
|
||||
if sink == nil {
|
||||
// no sink and no running command
|
||||
return fmt.Errorf("cannot send input, cmd is not running")
|
||||
return fmt.Errorf("cannot send input, cmd is not running (%s)", inputPk.CK)
|
||||
}
|
||||
return sink.HandleInput(inputPk)
|
||||
}
|
||||
@ -2461,7 +2424,7 @@ func (msh *MShellProc) processSinglePacket(pk packet.PacketType) {
|
||||
msh.WriteToPtyBuffer("stderr> [remote %s] %s\n", msh.GetRemoteName(), rawPk.Data)
|
||||
return
|
||||
}
|
||||
msh.WriteToPtyBuffer("MSH> [remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk))
|
||||
msh.WriteToPtyBuffer("*[remote %s] unhandled packet %s\n", msh.GetRemoteName(), packet.AsString(pk))
|
||||
}
|
||||
|
||||
func (msh *MShellProc) ProcessPackets() {
|
||||
|
@ -5,6 +5,7 @@ package scbus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
@ -96,6 +97,16 @@ func (upk *ModelUpdatePacketType) AddUpdate(items ...ModelUpdateItem) {
|
||||
*(upk.Data) = append(*(upk.Data), items...)
|
||||
}
|
||||
|
||||
// adds the items from p2 to the update (p2 must be ModelUpdatePacketType)
|
||||
func (upk *ModelUpdatePacketType) Merge(p2Arg UpdatePacket) error {
|
||||
p2, ok := p2Arg.(*ModelUpdatePacketType)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot merge ModelUpdatePacketType with %T", p2Arg)
|
||||
}
|
||||
*(upk.Data) = append(*(upk.Data), *(p2.Data)...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new model update packet
|
||||
func MakeUpdatePacket() *ModelUpdatePacketType {
|
||||
return &ModelUpdatePacketType{
|
||||
|
@ -106,6 +106,9 @@ func (pk *FeCommandPacketType) GetRawStr() string {
|
||||
}
|
||||
var args []string
|
||||
for k, v := range pk.Kwargs {
|
||||
if k == "nohist" {
|
||||
continue
|
||||
}
|
||||
argStr := fmt.Sprintf("%s=%s", shellescape.Quote(k), shellescape.Quote(v))
|
||||
args = append(args, argStr)
|
||||
}
|
||||
|
@ -386,9 +386,9 @@ func GetSessionByName(ctx context.Context, name string) (*SessionType, error) {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// returns sessionId
|
||||
// returns (update, newSessionId, newScreenId, error)
|
||||
// if sessionName == "", it will be generated
|
||||
func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (*scbus.ModelUpdatePacketType, error) {
|
||||
func InsertSessionWithName(ctx context.Context, sessionName string, activate bool) (*scbus.ModelUpdatePacketType, string, string, error) {
|
||||
var newScreen *ScreenType
|
||||
newSessionId := scbase.GenWaveUUID()
|
||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||
@ -414,11 +414,11 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return nil, txErr
|
||||
return nil, "", "", txErr
|
||||
}
|
||||
session, err := GetSessionById(ctx, newSessionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", "", err
|
||||
}
|
||||
update := scbus.MakeUpdatePacket()
|
||||
update.AddUpdate(*session)
|
||||
@ -426,7 +426,7 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo
|
||||
if activate {
|
||||
update.AddUpdate(ActiveSessionIdUpdate(newSessionId))
|
||||
}
|
||||
return update, nil
|
||||
return update, newSessionId, newScreen.ScreenId, nil
|
||||
}
|
||||
|
||||
func SetActiveSessionId(ctx context.Context, sessionId string) error {
|
||||
@ -569,6 +569,9 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string,
|
||||
query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?`
|
||||
tx.Exec(query, newScreenId, sessionId)
|
||||
}
|
||||
if opts.RtnScreenId != nil {
|
||||
*opts.RtnScreenId = newScreenId
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
@ -1044,11 +1047,6 @@ func DeleteScreen(ctx context.Context, screenId string, sessionDel bool, update
|
||||
if sessionId == "" {
|
||||
return fmt.Errorf("cannot delete screen (no sessionid)")
|
||||
}
|
||||
query = `SELECT count(*) FROM screen WHERE sessionid = ? AND NOT archived`
|
||||
numScreens := tx.GetInt(query, sessionId)
|
||||
if numScreens <= 1 {
|
||||
return fmt.Errorf("cannot delete the last screen in a session")
|
||||
}
|
||||
isActive = tx.Exists(`SELECT sessionid FROM session WHERE sessionid = ? AND activescreenid = ?`, sessionId, screenId)
|
||||
if isActive {
|
||||
screenIds := tx.SelectStrings(`SELECT screenid FROM screen WHERE sessionid = ? AND NOT archived ORDER BY screenidx`, sessionId)
|
||||
|
@ -375,6 +375,7 @@ type ScreenCreateOpts struct {
|
||||
CopyRemote bool
|
||||
CopyCwd bool
|
||||
CopyEnv bool
|
||||
RtnScreenId *string
|
||||
}
|
||||
|
||||
func (sco ScreenCreateOpts) HasCopy() bool {
|
||||
@ -760,7 +761,6 @@ type RemoteRuntimeState struct {
|
||||
RemoteAlias string `json:"remotealias,omitempty"`
|
||||
RemoteCanonicalName string `json:"remotecanonicalname"`
|
||||
RemoteVars map[string]string `json:"remotevars"`
|
||||
DefaultFeState map[string]string `json:"defaultfestate"`
|
||||
Status string `json:"status"`
|
||||
ConnectTimeout int `json:"connecttimeout,omitempty"`
|
||||
CountdownActive bool `json:"countdownactive"`
|
||||
@ -779,9 +779,9 @@ type RemoteRuntimeState struct {
|
||||
MShellVersion string `json:"mshellversion"`
|
||||
WaitingForPassword bool `json:"waitingforpassword,omitempty"`
|
||||
Local bool `json:"local,omitempty"`
|
||||
IsSudo bool `json:"issudo,omitempty"`
|
||||
RemoteOpts *RemoteOptsType `json:"remoteopts,omitempty"`
|
||||
CanComplete bool `json:"cancomplete,omitempty"`
|
||||
ActiveShells []string `json:"activeshells,omitempty"`
|
||||
ShellPref string `json:"shellpref,omitempty"`
|
||||
DefaultShellType string `json:"defaultshelltype,omitempty"`
|
||||
}
|
||||
@ -1127,21 +1127,6 @@ func EnsureLocalRemote(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureOneSession(ctx context.Context) error {
|
||||
numSessions, err := GetSessionCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if numSessions > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err = InsertSessionWithName(ctx, DefaultSessionName, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createClientData(tx *TxWrap) error {
|
||||
curve := elliptic.P384()
|
||||
pkey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
|
Loading…
Reference in New Issue
Block a user