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:
Mike Sawka 2024-03-27 00:22:57 -07:00 committed by GitHub
parent 6065ee931f
commit 3c3eec73aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1070 additions and 770 deletions

View File

@ -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;

View File

@ -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}

View File

@ -107,7 +107,8 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
this.model.setRecentConnAdded(true);
this.model.closeModal();
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
if (GlobalModel.activeMainView.get() == "session") {
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, true);
crRtn.then((crcrtn) => {
if (crcrtn.success) {
return;
@ -116,6 +117,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
this.errorStr.set(crcrtn.error);
})();
});
}
return;
}
mobx.action(() => {

View File

@ -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">

View File

@ -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

View File

@ -86,7 +86,7 @@
justify-content: center;
button {
font-size: 12.5px !important;
font-size: 15px;
margin-top: 16px;
}

View File

@ -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" />
<div className="item-inner">
<div className="item-title">Telemetry</div>
<div className="item-text">
We only collect minimal <i>anonymous</i>
<a
target="_blank"
href={util.makeExternLink("https://docs.waveterm.dev/reference/telemetry")}
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
rel={"noopener"}
>
&nbsp;telemetry data&nbsp;
<img src={github} alt="Github" />
</a>
<div className="item-inner">
<div className="item-title">Support us on GitHub</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{" "}
<a
target="_blank"
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
rel={"noopener"}
>
Github&nbsp;(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&nbsp;(wavetermdev/waveterm)
&nbsp;telemetry data&nbsp;
</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>

View File

@ -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);
}

View File

@ -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")}>&nbsp;</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")}>&nbsp;</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 };

View File

@ -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 {

View File

@ -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<{}, {}> {
&nbsp;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:&nbsp; 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
<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">
<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} />
<Prompt rptr={rptr} festate={feState} color={true} shellInitMsg={shellInitMsg} />
</span>
</div>
</div>
</If>
<div
key="input"
className={cn(

View File

@ -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();

View 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 };

View File

@ -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;
}

View File

@ -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}>

View File

@ -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() {

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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 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="session-view">
<div className="center-message">
<div>(no active workspace)</div>
<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>
);
}
let activeScreen = session.getActiveScreen();
}
@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 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>
);

View File

@ -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);
}

View File

@ -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,10 +1015,14 @@ class Model {
this.activeMainView.set("session");
this.deactivateScreenLines();
this.ws.watchScreen(newActiveSessionId, newActiveScreenId);
this.closeTabSettings();
const activeScreen = this.getActiveScreen();
if (activeScreen != null && activeScreen.getCurRemoteInstance() != null) {
setTimeout(() => {
GlobalCommandRunner.syncShellState();
}, 100);
}
}
} else {
console.warn("unknown update", genUpdate);
}

View File

@ -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(),

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 = {

View File

@ -365,9 +365,11 @@ function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<strin
}
return;
}
if (errorMessage != null) {
mobx.action(() => {
errorMessage.set(crtn.error);
})();
}
});
}

View File

@ -77,6 +77,7 @@ const (
const (
EC_InvalidCwd = "ERRCWD"
EC_CmdNotRunning = "CMDNOTRUNNING"
)
const PacketSenderQueueSize = 20

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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()

View 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"
}

View File

@ -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)

View File

@ -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,21 +2970,61 @@ 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: cannot resolve screen for update: %w", GetCmdStr(pk), err)
return nil, fmt.Errorf("/%s error looking up connection state: %w", GetCmdStr(pk), err)
}
update := scbus.MakeUpdatePacket()
update.AddUpdate(*screen, sstore.InteractiveUpdate(pk.Interactive))
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, 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, "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
}
outputStr := fmt.Sprintf("connected to %s", GetFullRemoteDisplayName(rptr, rstate))
} 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
@ -2963,23 +3037,37 @@ func CrCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.Upd
}
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)))
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

View File

@ -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

View File

@ -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, "[") {

View File

@ -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() {

View File

@ -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{

View File

@ -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)
}

View File

@ -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)

View File

@ -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)