mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
Screen MemStore (#197)
* working on an in-memory store for screen information * nostrpos sentinel * textareainput now tracks selection (to update backend) * make websocket connections much safer. add a defer/panic handler for each ws message handled on backend. don't allow client to reconnect to backend ws handler more than once per second (handles issue with lots of fast fails) * use onSelect to have frontend textarea sync state to backend ScreenMem store * restore cmdline when switching screens * prettier
This commit is contained in:
parent
9980a6b204
commit
6a1b2c8bd4
@ -11,3 +11,5 @@ export const CLIENT_SETTINGS = "clientSettings";
|
|||||||
export const LineContainer_Main = "main";
|
export const LineContainer_Main = "main";
|
||||||
export const LineContainer_History = "history";
|
export const LineContainer_History = "history";
|
||||||
export const LineContainer_Sidebar = "sidebar";
|
export const LineContainer_Sidebar = "sidebar";
|
||||||
|
|
||||||
|
export const NoStrPos = -1;
|
||||||
|
@ -127,10 +127,7 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={this.cmdInputRef} className={cn("cmd-input", { "has-info": infoShow }, { active: focusVal })}>
|
||||||
ref={this.cmdInputRef}
|
|
||||||
className={cn("cmd-input", { "has-info": infoShow }, { active: focusVal })}
|
|
||||||
>
|
|
||||||
<If condition={historyShow}>
|
<If condition={historyShow}>
|
||||||
<div className="cmd-input-grow-spacer"></div>
|
<div className="cmd-input-grow-spacer"></div>
|
||||||
<HistoryInfo />
|
<HistoryInfo />
|
||||||
@ -153,10 +150,12 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
</If>
|
</If>
|
||||||
<div key="prompt" className="cmd-input-context">
|
<div key="prompt" className="cmd-input-context">
|
||||||
<div className="has-text-white">
|
<div className="has-text-white">
|
||||||
<span ref={this.promptRef}><Prompt rptr={rptr} festate={feState} /></span>
|
<span ref={this.promptRef}>
|
||||||
|
<Prompt rptr={rptr} festate={feState} />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<If condition={numRunningLines > 0}>
|
<If condition={numRunningLines > 0}>
|
||||||
<div onClick={() => this.toggleFilter(screen)}className="cmd-input-filter">
|
<div onClick={() => this.toggleFilter(screen)} className="cmd-input-filter">
|
||||||
{numRunningLines}
|
{numRunningLines}
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<RotateIcon className="warning spin" />
|
<RotateIcon className="warning spin" />
|
||||||
@ -176,7 +175,11 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
<div className="button is-static">{inputMode}</div>
|
<div className="button is-static">{inputMode}</div>
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
<TextAreaInput key={textAreaInputKey} onHeightChange={this.handleInnerHeightUpdate} />
|
<TextAreaInput
|
||||||
|
key={textAreaInputKey}
|
||||||
|
screen={screen}
|
||||||
|
onHeightChange={this.handleInnerHeightUpdate}
|
||||||
|
/>
|
||||||
<div className="control cmd-exec">
|
<div className="control cmd-exec">
|
||||||
{/**<div onClick={inputModel.toggleExpandInput} className="hint-item color-white">
|
{/**<div onClick={inputModel.toggleExpandInput} className="hint-item color-white">
|
||||||
{inputModel.inputExpanded.get() ? "shrink" : "expand"} input ({renderCmdText("E")})
|
{inputModel.inputExpanded.get() ? "shrink" : "expand"} input ({renderCmdText("E")})
|
||||||
|
@ -4,11 +4,15 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as mobxReact from "mobx-react";
|
import * as mobxReact from "mobx-react";
|
||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
|
import type * as T from "../../../types/types";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||||
import { getMonoFontSize } from "../../../util/textmeasure";
|
import { getMonoFontSize } from "../../../util/textmeasure";
|
||||||
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
|
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
|
||||||
|
import * as appconst from "../../appconst";
|
||||||
|
|
||||||
|
type OV<T> = mobx.IObservableValue<T>;
|
||||||
|
|
||||||
function pageSize(div: any): number {
|
function pageSize(div: any): number {
|
||||||
if (div == null) {
|
if (div == null) {
|
||||||
@ -35,21 +39,44 @@ function scrollDiv(div: any, amt: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}> {
|
class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () => void }, {}> {
|
||||||
lastTab: boolean = false;
|
lastTab: boolean = false;
|
||||||
lastHistoryUpDown: boolean = false;
|
lastHistoryUpDown: boolean = false;
|
||||||
lastTabCurLine: mobx.IObservableValue<string> = mobx.observable.box(null);
|
lastTabCurLine: OV<string> = mobx.observable.box(null);
|
||||||
lastFocusType: string = null;
|
lastFocusType: string = null;
|
||||||
mainInputRef: React.RefObject<any>;
|
mainInputRef: React.RefObject<HTMLTextAreaElement> = React.createRef();
|
||||||
historyInputRef: React.RefObject<any>;
|
historyInputRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||||
controlRef: React.RefObject<any>;
|
controlRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
lastHeight: number = 0;
|
lastHeight: number = 0;
|
||||||
|
lastSP: T.StrWithPos = { str: "", pos: appconst.NoStrPos };
|
||||||
|
version: OV<number> = mobx.observable.box(0); // forces render updates
|
||||||
|
|
||||||
constructor(props) {
|
incVersion(): void {
|
||||||
super(props);
|
let v = this.version.get();
|
||||||
this.mainInputRef = React.createRef();
|
mobx.action(() => this.version.set(v + 1))();
|
||||||
this.historyInputRef = React.createRef();
|
}
|
||||||
this.controlRef = React.createRef();
|
|
||||||
|
getCurSP(): T.StrWithPos {
|
||||||
|
let textarea = this.mainInputRef.current;
|
||||||
|
if (textarea == null) {
|
||||||
|
return this.lastSP;
|
||||||
|
}
|
||||||
|
let str = textarea.value;
|
||||||
|
let pos = textarea.selectionStart;
|
||||||
|
let endPos = textarea.selectionEnd;
|
||||||
|
if (pos != endPos) {
|
||||||
|
return { str, pos: appconst.NoStrPos };
|
||||||
|
}
|
||||||
|
return { str, pos };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSP(): void {
|
||||||
|
let curSP = this.getCurSP();
|
||||||
|
if (curSP.str == this.lastSP.str && curSP.pos == this.lastSP.pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastSP = curSP;
|
||||||
|
GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFocus(): void {
|
setFocus(): void {
|
||||||
@ -100,6 +127,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
this.lastFocusType = focusType;
|
this.lastFocusType = focusType;
|
||||||
}
|
}
|
||||||
this.checkHeight(false);
|
this.checkHeight(false);
|
||||||
|
this.updateSP();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
@ -112,10 +140,11 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
this.lastFocusType = focusType;
|
this.lastFocusType = focusType;
|
||||||
}
|
}
|
||||||
let inputModel = GlobalModel.inputModel;
|
let inputModel = GlobalModel.inputModel;
|
||||||
if (inputModel.forceCursorPos.get() != null) {
|
let fcpos = inputModel.forceCursorPos.get();
|
||||||
|
if (fcpos != null && fcpos != appconst.NoStrPos) {
|
||||||
if (this.mainInputRef.current != null) {
|
if (this.mainInputRef.current != null) {
|
||||||
this.mainInputRef.current.selectionStart = inputModel.forceCursorPos.get();
|
this.mainInputRef.current.selectionStart = fcpos;
|
||||||
this.mainInputRef.current.selectionEnd = inputModel.forceCursorPos.get();
|
this.mainInputRef.current.selectionEnd = fcpos;
|
||||||
}
|
}
|
||||||
mobx.action(() => inputModel.forceCursorPos.set(null))();
|
mobx.action(() => inputModel.forceCursorPos.set(null))();
|
||||||
}
|
}
|
||||||
@ -124,6 +153,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
this.setFocus();
|
this.setFocus();
|
||||||
}
|
}
|
||||||
this.checkHeight(true);
|
this.checkHeight(true);
|
||||||
|
this.updateSP();
|
||||||
}
|
}
|
||||||
|
|
||||||
getLinePos(elem: any): { numLines: number; linePos: number } {
|
getLinePos(elem: any): { numLines: number; linePos: number } {
|
||||||
@ -294,6 +324,11 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
onSelect(e: any) {
|
||||||
|
this.incVersion();
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
onHistoryKeyDown(e: any) {
|
onHistoryKeyDown(e: any) {
|
||||||
let inputModel = GlobalModel.inputModel;
|
let inputModel = GlobalModel.inputModel;
|
||||||
@ -386,7 +421,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
}
|
}
|
||||||
let cutValue = value.substr(0, selStart);
|
let cutValue = value.substr(0, selStart);
|
||||||
let restValue = value.substr(selStart);
|
let restValue = value.substr(selStart);
|
||||||
let cmdLineUpdate = { cmdline: restValue, cursorpos: 0 };
|
let cmdLineUpdate = { str: restValue, pos: 0 };
|
||||||
navigator.clipboard.writeText(cutValue);
|
navigator.clipboard.writeText(cutValue);
|
||||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||||
}
|
}
|
||||||
@ -439,7 +474,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
let cutValue = value.slice(cutSpot, selStart);
|
let cutValue = value.slice(cutSpot, selStart);
|
||||||
let prevValue = value.slice(0, cutSpot);
|
let prevValue = value.slice(0, cutSpot);
|
||||||
let restValue = value.slice(selStart);
|
let restValue = value.slice(selStart);
|
||||||
let cmdLineUpdate = { cmdline: prevValue + restValue, cursorpos: prevValue.length };
|
let cmdLineUpdate = { str: prevValue + restValue, pos: prevValue.length };
|
||||||
navigator.clipboard.writeText(cutValue);
|
navigator.clipboard.writeText(cutValue);
|
||||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||||
}
|
}
|
||||||
@ -459,7 +494,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let newValue = value.substr(0, selStart) + clipText + value.substr(selEnd);
|
let newValue = value.substr(0, selStart) + clipText + value.substr(selEnd);
|
||||||
let cmdLineUpdate = { cmdline: newValue, cursorpos: selStart + clipText.length };
|
let cmdLineUpdate = { str: newValue, pos: selStart + clipText.length };
|
||||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -525,6 +560,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
let numLines = curLine.split("\n").length;
|
let numLines = curLine.split("\n").length;
|
||||||
let maxCols = this.getTextAreaMaxCols();
|
let maxCols = this.getTextAreaMaxCols();
|
||||||
let longLine = false;
|
let longLine = false;
|
||||||
|
let version = this.version.get(); // to force reactions
|
||||||
if (maxCols != 0 && curLine.length >= maxCols - 4) {
|
if (maxCols != 0 && curLine.length >= maxCols - 4) {
|
||||||
longLine = true;
|
longLine = true;
|
||||||
}
|
}
|
||||||
@ -559,6 +595,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
|||||||
value={curLine}
|
value={curLine}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
onSelect={this.onSelect}
|
||||||
className={cn("textarea", { "display-disabled": disabled })}
|
className={cn("textarea", { "display-disabled": disabled })}
|
||||||
></textarea>
|
></textarea>
|
||||||
<input
|
<input
|
||||||
|
@ -1444,10 +1444,12 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmdLine(cmdLine: CmdLineUpdateType): void {
|
updateCmdLine(cmdLine: T.StrWithPos): void {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.setCurLine(cmdLine.cmdline);
|
this.setCurLine(cmdLine.str);
|
||||||
this.forceCursorPos.set(cmdLine.cursorpos);
|
if (cmdLine.pos != appconst.NoStrPos) {
|
||||||
|
this.forceCursorPos.set(cmdLine.pos);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3164,6 +3166,7 @@ class Model {
|
|||||||
showLinks: OV<boolean> = mobx.observable.box(true, {
|
showLinks: OV<boolean> = mobx.observable.box(true, {
|
||||||
name: "model-showLinks",
|
name: "model-showLinks",
|
||||||
});
|
});
|
||||||
|
packetSeqNum: number = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.clientId = getApi().getId();
|
this.clientId = getApi().getId();
|
||||||
@ -3217,6 +3220,11 @@ class Model {
|
|||||||
setTimeout(() => this.getClientDataLoop(1), 10);
|
setTimeout(() => this.getClientDataLoop(1), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNextPacketSeqNum(): number {
|
||||||
|
this.packetSeqNum++;
|
||||||
|
return this.packetSeqNum;
|
||||||
|
}
|
||||||
|
|
||||||
getPlatform(): string {
|
getPlatform(): string {
|
||||||
if (this.platform != null) {
|
if (this.platform != null) {
|
||||||
return this.platform;
|
return this.platform;
|
||||||
@ -3618,6 +3626,12 @@ class Model {
|
|||||||
let newContext = this.getUIContext();
|
let newContext = this.getUIContext();
|
||||||
if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) {
|
if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) {
|
||||||
this.inputModel.resetInput();
|
this.inputModel.resetInput();
|
||||||
|
if ("cmdline" in genUpdate) {
|
||||||
|
// TODO a bit of a hack since this update gets applied in runUpdate_internal.
|
||||||
|
// we then undo that update with the resetInput, and then redo it with the line below
|
||||||
|
// not sure how else to handle this for now though
|
||||||
|
this.inputModel.updateCmdLine(genUpdate.cmdline);
|
||||||
|
}
|
||||||
} else if (remotePtrToString(oldContext.remote) != remotePtrToString(newContext.remote)) {
|
} else if (remotePtrToString(oldContext.remote) != remotePtrToString(newContext.remote)) {
|
||||||
this.inputModel.resetHistory();
|
this.inputModel.resetHistory();
|
||||||
}
|
}
|
||||||
@ -3996,10 +4010,7 @@ class Model {
|
|||||||
getActiveIds(): [string, string] {
|
getActiveIds(): [string, string] {
|
||||||
let activeSession = this.getActiveSession();
|
let activeSession = this.getActiveSession();
|
||||||
let activeScreen = this.getActiveScreen();
|
let activeScreen = this.getActiveScreen();
|
||||||
return [
|
return [activeSession?.sessionId, activeScreen?.screenId];
|
||||||
activeSession == null ? null : activeSession.sessionId,
|
|
||||||
activeScreen == null ? null : activeScreen.screenId,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadScreenLinesAsync(newWin: ScreenLines) {
|
_loadScreenLinesAsync(newWin: ScreenLines) {
|
||||||
@ -4118,6 +4129,16 @@ class Model {
|
|||||||
this.ws.pushMessage(inputPacket);
|
this.ws.pushMessage(inputPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendCmdInputText(screenId: string, sp: T.StrWithPos) {
|
||||||
|
let pk: T.CmdInputTextPacketType = {
|
||||||
|
type: "cmdinputtext",
|
||||||
|
seqnum: this.getNextPacketSeqNum(),
|
||||||
|
screenid: screenId,
|
||||||
|
text: sp,
|
||||||
|
};
|
||||||
|
this.ws.pushMessage(pk);
|
||||||
|
}
|
||||||
|
|
||||||
resolveUserIdToName(userid: string): string {
|
resolveUserIdToName(userid: string): string {
|
||||||
return "@[unknown]";
|
return "@[unknown]";
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ class WSControl {
|
|||||||
wsLog: mobx.IObservableArray<string> = mobx.observable.array([], { name: "wsLog" });
|
wsLog: mobx.IObservableArray<string> = mobx.observable.array([], { name: "wsLog" });
|
||||||
authKey: string;
|
authKey: string;
|
||||||
baseHostPort: string;
|
baseHostPort: string;
|
||||||
|
lastReconnectTime: number = 0;
|
||||||
|
|
||||||
constructor(baseHostPort: string, clientId: string, authKey: string, messageCallback: (any) => void) {
|
constructor(baseHostPort: string, clientId: string, authKey: string, messageCallback: (any) => void) {
|
||||||
this.baseHostPort = baseHostPort;
|
this.baseHostPort = baseHostPort;
|
||||||
@ -51,6 +52,7 @@ class WSControl {
|
|||||||
if (this.open.get()) {
|
if (this.open.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.lastReconnectTime = Date.now();
|
||||||
this.log(sprintf("try reconnect (%s)", desc));
|
this.log(sprintf("try reconnect (%s)", desc));
|
||||||
this.opening = true;
|
this.opening = true;
|
||||||
this.wsConn = new WebSocket(this.baseHostPort + "/ws?clientid=" + this.clientId);
|
this.wsConn = new WebSocket(this.baseHostPort + "/ws?clientid=" + this.clientId);
|
||||||
@ -78,6 +80,9 @@ class WSControl {
|
|||||||
if (this.reconnectTimes < timeoutArr.length) {
|
if (this.reconnectTimes < timeoutArr.length) {
|
||||||
timeout = timeoutArr[this.reconnectTimes];
|
timeout = timeoutArr[this.reconnectTimes];
|
||||||
}
|
}
|
||||||
|
if (Date.now() - this.lastReconnectTime < 500) {
|
||||||
|
timeout = 1;
|
||||||
|
}
|
||||||
if (timeout > 0) {
|
if (timeout > 0) {
|
||||||
this.log(sprintf("sleeping %ds", timeout));
|
this.log(sprintf("sleeping %ds", timeout));
|
||||||
}
|
}
|
||||||
|
@ -210,6 +210,13 @@ type WatchScreenPacketType = {
|
|||||||
authkey: string;
|
authkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CmdInputTextPacketType = {
|
||||||
|
type: string;
|
||||||
|
seqnum: number;
|
||||||
|
screenid: string;
|
||||||
|
text: StrWithPos;
|
||||||
|
};
|
||||||
|
|
||||||
type TermWinSize = {
|
type TermWinSize = {
|
||||||
rows: number;
|
rows: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
@ -267,7 +274,7 @@ type ModelUpdateType = {
|
|||||||
lines?: LineType[];
|
lines?: LineType[];
|
||||||
cmd?: CmdDataType;
|
cmd?: CmdDataType;
|
||||||
info?: InfoType;
|
info?: InfoType;
|
||||||
cmdline?: CmdLineUpdateType;
|
cmdline?: StrWithPos;
|
||||||
remotes?: RemoteType[];
|
remotes?: RemoteType[];
|
||||||
history?: HistoryInfoType;
|
history?: HistoryInfoType;
|
||||||
connect?: boolean;
|
connect?: boolean;
|
||||||
@ -666,6 +673,11 @@ type ModalStoreEntry = {
|
|||||||
uniqueKey: string;
|
uniqueKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StrWithPos = {
|
||||||
|
str: string;
|
||||||
|
pos: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
SessionDataType,
|
SessionDataType,
|
||||||
LineStateType,
|
LineStateType,
|
||||||
@ -741,4 +753,6 @@ export type {
|
|||||||
ExtFile,
|
ExtFile,
|
||||||
LineContainerStrs,
|
LineContainerStrs,
|
||||||
ModalStoreEntry,
|
ModalStoreEntry,
|
||||||
|
StrWithPos,
|
||||||
|
CmdInputTextPacketType,
|
||||||
};
|
};
|
||||||
|
@ -2142,7 +2142,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
update := &sstore.ModelUpdate{
|
update := &sstore.ModelUpdate{
|
||||||
CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos},
|
CmdLine: &utilfn.StrWithPos{Str: newSP.Str, Pos: newSP.Pos},
|
||||||
}
|
}
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,14 @@ import (
|
|||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FeCommandPacketStr = "fecmd"
|
const FeCommandPacketStr = "fecmd"
|
||||||
const WatchScreenPacketStr = "watchscreen"
|
const WatchScreenPacketStr = "watchscreen"
|
||||||
const FeInputPacketStr = "feinput"
|
const FeInputPacketStr = "feinput"
|
||||||
const RemoteInputPacketStr = "remoteinput"
|
const RemoteInputPacketStr = "remoteinput"
|
||||||
|
const CmdInputTextPacketStr = "cmdinputtext"
|
||||||
|
|
||||||
type FeCommandPacketType struct {
|
type FeCommandPacketType struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -83,11 +85,27 @@ type WatchScreenPacketType struct {
|
|||||||
AuthKey string `json:"authkey"`
|
AuthKey string `json:"authkey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CmdInputTextPacketType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
SeqNum int `json:"seqnum"`
|
||||||
|
ScreenId string `json:"screenid"`
|
||||||
|
Text utilfn.StrWithPos `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{}))
|
packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{}))
|
||||||
packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{}))
|
packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{}))
|
||||||
packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{}))
|
packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{}))
|
||||||
packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{}))
|
packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{}))
|
||||||
|
packet.RegisterPacketType(CmdInputTextPacketStr, reflect.TypeOf(CmdInputTextPacketType{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CmdInputTextPacketType) GetType() string {
|
||||||
|
return CmdInputTextPacketStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeCmdInputTextPacket(screenId string) *CmdInputTextPacketType {
|
||||||
|
return &CmdInputTextPacketType{Type: CmdInputTextPacketStr, ScreenId: screenId}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*FeCommandPacketType) GetType() string {
|
func (*FeCommandPacketType) GetType() string {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -214,6 +215,76 @@ func (ws *WSState) handleWatchScreen(wsPk *scpacket.WatchScreenPacketType) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ws *WSState) processMessage(msgBytes []byte) error {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[scws] panic in processMessage: %v\n", r)
|
||||||
|
debug.PrintStack()
|
||||||
|
}()
|
||||||
|
|
||||||
|
pk, err := packet.ParseJsonPacket(msgBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error unmarshalling ws message: %w", err)
|
||||||
|
}
|
||||||
|
if pk.GetType() == scpacket.WatchScreenPacketStr {
|
||||||
|
wsPk := pk.(*scpacket.WatchScreenPacketType)
|
||||||
|
err := ws.handleWatchScreen(wsPk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client:%s error %w", ws.ClientId, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
isAuth := ws.IsAuthenticated()
|
||||||
|
if !isAuth {
|
||||||
|
return fmt.Errorf("cannot process ws-packet[%s], not authenticated", pk.GetType())
|
||||||
|
}
|
||||||
|
if pk.GetType() == scpacket.FeInputPacketStr {
|
||||||
|
feInputPk := pk.(*scpacket.FeInputPacketType)
|
||||||
|
if feInputPk.Remote.OwnerId != "" {
|
||||||
|
return fmt.Errorf("error cannot send input to remote with ownerid")
|
||||||
|
}
|
||||||
|
if feInputPk.Remote.RemoteId == "" {
|
||||||
|
return fmt.Errorf("error invalid input packet, remoteid is not set")
|
||||||
|
}
|
||||||
|
err := RemoteInputMapQueue.Enqueue(feInputPk.Remote.RemoteId, func() {
|
||||||
|
sendErr := sendCmdInput(feInputPk)
|
||||||
|
if sendErr != nil {
|
||||||
|
log.Printf("[scws] sending command input: %v\n", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("[error] could not queue sendCmdInput: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if pk.GetType() == scpacket.RemoteInputPacketStr {
|
||||||
|
inputPk := pk.(*scpacket.RemoteInputPacketType)
|
||||||
|
if inputPk.RemoteId == "" {
|
||||||
|
return fmt.Errorf("error invalid remoteinput packet, remoteid is not set")
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
sendErr := remote.SendRemoteInput(inputPk)
|
||||||
|
if sendErr != nil {
|
||||||
|
log.Printf("[scws] error processing remote input: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if pk.GetType() == scpacket.CmdInputTextPacketStr {
|
||||||
|
cmdInputPk := pk.(*scpacket.CmdInputTextPacketType)
|
||||||
|
if cmdInputPk.ScreenId == "" {
|
||||||
|
return fmt.Errorf("error invalid cmdinput packet, screenid is not set")
|
||||||
|
}
|
||||||
|
// no need for goroutine for memory ops
|
||||||
|
sstore.ScreenMemSetCmdInputText(cmdInputPk.ScreenId, cmdInputPk.Text, cmdInputPk.SeqNum)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("got ws bad message: %v", pk.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
func (ws *WSState) RunWSRead() {
|
func (ws *WSState) RunWSRead() {
|
||||||
shell := ws.GetShell()
|
shell := ws.GetShell()
|
||||||
if shell == nil {
|
if shell == nil {
|
||||||
@ -221,62 +292,11 @@ func (ws *WSState) RunWSRead() {
|
|||||||
}
|
}
|
||||||
shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error
|
shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error
|
||||||
for msgBytes := range shell.ReadChan {
|
for msgBytes := range shell.ReadChan {
|
||||||
pk, err := packet.ParseJsonPacket(msgBytes)
|
err := ws.processMessage(msgBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error unmarshalling ws message: %v\n", err)
|
// TODO send errors back to client? likely unrecoverable
|
||||||
continue
|
log.Printf("[scws] %v\n", err)
|
||||||
}
|
}
|
||||||
if pk.GetType() == scpacket.WatchScreenPacketStr {
|
|
||||||
wsPk := pk.(*scpacket.WatchScreenPacketType)
|
|
||||||
err := ws.handleWatchScreen(wsPk)
|
|
||||||
if err != nil {
|
|
||||||
// TODO send errors back to client, likely unrecoverable
|
|
||||||
log.Printf("[ws %s] error %v\n", ws.ClientId, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
isAuth := ws.IsAuthenticated()
|
|
||||||
if !isAuth {
|
|
||||||
log.Printf("[error] cannot process ws-packet[%s], not authenticated\n", pk.GetType())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == scpacket.FeInputPacketStr {
|
|
||||||
feInputPk := pk.(*scpacket.FeInputPacketType)
|
|
||||||
if feInputPk.Remote.OwnerId != "" {
|
|
||||||
log.Printf("[error] cannot send input to remote with ownerid\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if feInputPk.Remote.RemoteId == "" {
|
|
||||||
log.Printf("[error] invalid input packet, remoteid is not set\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := RemoteInputMapQueue.Enqueue(feInputPk.Remote.RemoteId, func() {
|
|
||||||
err = sendCmdInput(feInputPk)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[error] sending command input: %v\n", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[error] could not queue sendCmdInput: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pk.GetType() == scpacket.RemoteInputPacketStr {
|
|
||||||
inputPk := pk.(*scpacket.RemoteInputPacketType)
|
|
||||||
if inputPk.RemoteId == "" {
|
|
||||||
log.Printf("[error] invalid remoteinput packet, remoteid is not set\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
err = remote.SendRemoteInput(inputPk)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[error] processing remote input: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("got ws bad message: %v\n", pk.GetType())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1015,7 +1015,12 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ModelUpdate{ActiveSessionId: sessionId, Sessions: []*SessionType{bareSession}}, nil
|
update := &ModelUpdate{ActiveSessionId: sessionId, Sessions: []*SessionType{bareSession}}
|
||||||
|
memState := GetScreenMemState(screenId)
|
||||||
|
if memState != nil {
|
||||||
|
update.CmdLine = &memState.CmdInputText
|
||||||
|
}
|
||||||
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// screen may not exist at this point (so don't query screen table)
|
// screen may not exist at this point (so don't query screen table)
|
||||||
|
96
wavesrv/pkg/sstore/memops.go
Normal file
96
wavesrv/pkg/sstore/memops.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// in-memory storage for waveterm server
|
||||||
|
package sstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// global lock for all memory operations
|
||||||
|
// memory ops are very fast, so this is not a bottleneck
|
||||||
|
var MemLock *sync.Mutex = &sync.Mutex{}
|
||||||
|
var ScreenMemStore map[string]*ScreenMemState = make(map[string]*ScreenMemState) // map of screenid -> ScreenMemState
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScreenIndicator_None = ""
|
||||||
|
ScreenIndicator_Error = "error"
|
||||||
|
ScreenIndicator_Success = "success"
|
||||||
|
ScreenIndicator_Output = "output"
|
||||||
|
)
|
||||||
|
|
||||||
|
var screenIndicatorLevels map[string]int = map[string]int{
|
||||||
|
ScreenIndicator_None: 0,
|
||||||
|
ScreenIndicator_Output: 1,
|
||||||
|
ScreenIndicator_Success: 2,
|
||||||
|
ScreenIndicator_Error: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpScreenMemStore() {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
for k, v := range ScreenMemStore {
|
||||||
|
log.Printf(" ScreenMemStore[%s] = %+v\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if i1 > i2
|
||||||
|
func isIndicatorGreater(i1 string, i2 string) bool {
|
||||||
|
return screenIndicatorLevels[i1] > screenIndicatorLevels[i2]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenMemState struct {
|
||||||
|
NumRunningCommands int `json:"numrunningcommands,omitempty"`
|
||||||
|
IndicatorType string `json:"indicatortype,omitempty"`
|
||||||
|
CmdInputText utilfn.StrWithPos `json:"cmdinputtext,omitempty"`
|
||||||
|
CmdInputSeqNum int `json:"cmdinputseqnum,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemSetCmdInputText(screenId string, sp utilfn.StrWithPos, seqNum int) {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
if seqNum <= ScreenMemStore[screenId].CmdInputSeqNum {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ScreenMemStore[screenId].CmdInputText = sp
|
||||||
|
ScreenMemStore[screenId].CmdInputSeqNum = seqNum
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemSetNumRunningCommands(screenId string, num int) {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
ScreenMemStore[screenId].NumRunningCommands = num
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemCombineIndicator(screenId string, indicator string) {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
if isIndicatorGreater(indicator, ScreenMemStore[screenId].IndicatorType) {
|
||||||
|
ScreenMemStore[screenId].IndicatorType = indicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// safe because we return a copy
|
||||||
|
func GetScreenMemState(screenId string) *ScreenMemState {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
ptr := ScreenMemStore[screenId]
|
||||||
|
if ptr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rtn := *ptr
|
||||||
|
return &rtn
|
||||||
|
}
|
@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MainBus *UpdateBus = MakeUpdateBus()
|
var MainBus *UpdateBus = MakeUpdateBus()
|
||||||
@ -36,26 +38,26 @@ func (*PtyDataUpdate) UpdateType() string {
|
|||||||
func (pdu *PtyDataUpdate) Clean() {}
|
func (pdu *PtyDataUpdate) Clean() {}
|
||||||
|
|
||||||
type ModelUpdate struct {
|
type ModelUpdate struct {
|
||||||
Sessions []*SessionType `json:"sessions,omitempty"`
|
Sessions []*SessionType `json:"sessions,omitempty"`
|
||||||
ActiveSessionId string `json:"activesessionid,omitempty"`
|
ActiveSessionId string `json:"activesessionid,omitempty"`
|
||||||
Screens []*ScreenType `json:"screens,omitempty"`
|
Screens []*ScreenType `json:"screens,omitempty"`
|
||||||
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
|
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
|
||||||
Line *LineType `json:"line,omitempty"`
|
Line *LineType `json:"line,omitempty"`
|
||||||
Lines []*LineType `json:"lines,omitempty"`
|
Lines []*LineType `json:"lines,omitempty"`
|
||||||
Cmd *CmdType `json:"cmd,omitempty"`
|
Cmd *CmdType `json:"cmd,omitempty"`
|
||||||
CmdLine *CmdLineType `json:"cmdline,omitempty"`
|
CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"`
|
||||||
Info *InfoMsgType `json:"info,omitempty"`
|
Info *InfoMsgType `json:"info,omitempty"`
|
||||||
ClearInfo bool `json:"clearinfo,omitempty"`
|
ClearInfo bool `json:"clearinfo,omitempty"`
|
||||||
Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState
|
Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState
|
||||||
History *HistoryInfoType `json:"history,omitempty"`
|
History *HistoryInfoType `json:"history,omitempty"`
|
||||||
Interactive bool `json:"interactive"`
|
Interactive bool `json:"interactive"`
|
||||||
Connect bool `json:"connect,omitempty"`
|
Connect bool `json:"connect,omitempty"`
|
||||||
MainView string `json:"mainview,omitempty"`
|
MainView string `json:"mainview,omitempty"`
|
||||||
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
|
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
|
||||||
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
||||||
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
|
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
|
||||||
ClientData *ClientData `json:"clientdata,omitempty"`
|
ClientData *ClientData `json:"clientdata,omitempty"`
|
||||||
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
|
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ModelUpdate) UpdateType() string {
|
func (*ModelUpdate) UpdateType() string {
|
||||||
@ -144,11 +146,6 @@ type HistoryInfoType struct {
|
|||||||
Show bool `json:"show"`
|
Show bool `json:"show"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CmdLineType struct {
|
|
||||||
CmdLine string `json:"cmdline"`
|
|
||||||
CursorPos int `json:"cursorpos"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateChannel struct {
|
type UpdateChannel struct {
|
||||||
ScreenId string
|
ScreenId string
|
||||||
ClientId string
|
ClientId string
|
||||||
|
@ -146,9 +146,12 @@ func IsPrefix(strs []string, test string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sentinel value for StrWithPos.Pos to indicate no position
|
||||||
|
const NoStrPos = -1
|
||||||
|
|
||||||
type StrWithPos struct {
|
type StrWithPos struct {
|
||||||
Str string
|
Str string `json:"str"`
|
||||||
Pos int // this is a 'rune' position (not a byte position)
|
Pos int `json:"pos"` // this is a 'rune' position (not a byte position)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sp StrWithPos) String() string {
|
func (sp StrWithPos) String() string {
|
||||||
@ -158,22 +161,26 @@ func (sp StrWithPos) String() string {
|
|||||||
func ParseToSP(s string) StrWithPos {
|
func ParseToSP(s string) StrWithPos {
|
||||||
idx := strings.Index(s, "[*]")
|
idx := strings.Index(s, "[*]")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return StrWithPos{Str: s}
|
return StrWithPos{Str: s, Pos: NoStrPos}
|
||||||
}
|
}
|
||||||
return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: utf8.RuneCountInString(s[0:idx])}
|
return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: utf8.RuneCountInString(s[0:idx])}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strWithCursor(str string, pos int) string {
|
func strWithCursor(str string, pos int) string {
|
||||||
|
if pos == NoStrPos {
|
||||||
|
return str
|
||||||
|
}
|
||||||
if pos < 0 {
|
if pos < 0 {
|
||||||
|
// invalid position
|
||||||
return "[*]_" + str
|
return "[*]_" + str
|
||||||
}
|
}
|
||||||
if pos >= len(str) {
|
if pos > len(str) {
|
||||||
if pos > len(str) {
|
// invalid position
|
||||||
return str + "_[*]"
|
return str + "_[*]"
|
||||||
}
|
}
|
||||||
|
if pos == len(str) {
|
||||||
return str + "[*]"
|
return str + "[*]"
|
||||||
}
|
}
|
||||||
|
|
||||||
var rtn []rune
|
var rtn []rune
|
||||||
for _, ch := range str {
|
for _, ch := range str {
|
||||||
if len(rtn) == pos {
|
if len(rtn) == pos {
|
||||||
|
Loading…
Reference in New Issue
Block a user