From 21ab82e2e282dbe9590039499a167d3135ccae3b Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Sun, 17 Dec 2023 23:46:53 -0800 Subject: [PATCH] Implement a Sidebar for Tabs (#157) * work on basic sidebar layout * fix more golang warnings * sidebar open/close * add ability to set width of split * sidebar add and remove, set width, etc. * almost working sidebar implementation -- still needs height/width, input control, and bug with initial add, but getting there * add isSidebarOpen() method * fix resize jump -- must set width in error handler as well (before window is loaded) * sidebar UI touchups and help * more sidebar progress, render more like regular lines, just in the right column * merge * move migration to 26 * simplify sidebar types * checkpoint * proxy things through parent screen object for sidebar * checkpoint, add/remove from sidebar * work on add/remove icons for sidebar * fix height calculation, remove close button * bring back close button when no line is selected * add sidebar flag to run command to run new command output in sidebar * implement 'sidebar' kwarg in eval. this lets sidebar work for slashcommands as well that produce lines (codeedit, mdview, etc.) * prettier * minor fixes * working on resizing. must exclude sidebar entries and send separate resize events based on size of sidebar (implement exclude / include for resize) * fix sidebar terminal command resizing * add sidebar header (toggles for half/partial width and close). add hotkey to open/close sidebar (Cmd-Ctrl-S). more robust calculation for sidebar width. add width validation. minimum sidebar width is 200px. other fixes, etc. --- src/app/appconst.ts | 4 + src/app/line/linecomps.tsx | 97 ++++-- src/app/line/lines.less | 17 +- src/app/line/linesview.tsx | 2 +- src/app/magiclayout.ts | 4 + src/app/sidebar/sidebar.less | 1 + src/app/workspace/screen/screenview.less | 59 +++- src/app/workspace/screen/screenview.tsx | 275 +++++++++++++++- src/model/model.ts | 298 ++++++++++++++++-- src/types/types.ts | 17 +- .../migrations/000026_screenviewopts.down.sql | 1 + .../migrations/000026_screenviewopts.up.sql | 1 + wavesrv/pkg/cmdrunner/cmdrunner.go | 180 ++++++++++- wavesrv/pkg/cmdrunner/shparse.go | 1 + wavesrv/pkg/sstore/dbops.go | 18 +- wavesrv/pkg/sstore/migrate.go | 6 +- wavesrv/pkg/sstore/sstore.go | 45 ++- wavesrv/pkg/sstore/updatebus.go | 25 +- 18 files changed, 941 insertions(+), 110 deletions(-) create mode 100644 wavesrv/db/migrations/000026_screenviewopts.down.sql create mode 100644 wavesrv/db/migrations/000026_screenviewopts.up.sql diff --git a/src/app/appconst.ts b/src/app/appconst.ts index df63d49d..aeee08b2 100644 --- a/src/app/appconst.ts +++ b/src/app/appconst.ts @@ -7,3 +7,7 @@ export const SCREEN_SETTINGS = "screenSettings"; export const SESSION_SETTINGS = "sessionSettings"; export const LINE_SETTINGS = "lineSettings"; export const CLIENT_SETTINGS = "clientSettings"; + +export const LineContainer_Main = "main"; +export const LineContainer_History = "history"; +export const LineContainer_Sidebar = "sidebar"; diff --git a/src/app/line/linecomps.tsx b/src/app/line/linecomps.tsx index 6e915fc6..e0732f17 100644 --- a/src/app/line/linecomps.tsx +++ b/src/app/line/linecomps.tsx @@ -39,7 +39,7 @@ import { PluginModel } from "../../plugins/plugins"; import { Prompt } from "../common/prompt/prompt"; import * as lineutil from "./lineutil"; import { ErrorBoundary } from "../../app/common/error/errorboundary"; -import * as constants from "../appconst"; +import * as appconst from "../appconst"; import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg"; import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.svg"; @@ -130,7 +130,7 @@ class LineCmd extends React.Component< isOverflow: OV = mobx.observable.box(false, { name: "line-overflow", }); - isMinimised: OV = mobx.observable.box(false, { + isMinimized: OV = mobx.observable.box(false, { name: "line-minimised", }); isCmdExpanded: OV = mobx.observable.box(false, { @@ -355,12 +355,23 @@ class LineCmd extends React.Component< } @boundMethod - clickMinimise() { + clickMinimize() { mobx.action(() => { - this.isMinimised.set(!this.isMinimised.get()); + this.isMinimized.set(!this.isMinimized.get()); })(); } + @boundMethod + clickMoveToSidebar() { + let { line } = this.props; + GlobalCommandRunner.screenSidebarAddLine(line.lineid); + } + + @boundMethod + clickRemoveFromSidebar() { + GlobalCommandRunner.screenSidebarRemove(); + } + @boundMethod handleResizeButton() { console.log("resize button"); @@ -443,7 +454,7 @@ class LineCmd extends React.Component< mobx.action(() => { GlobalModel.lineSettingsModal.set(line.linenum); })(); - GlobalModel.modalsModel.pushModal(constants.LINE_SETTINGS); + GlobalModel.modalsModel.pushModal(appconst.LINE_SETTINGS); } } @@ -598,6 +609,14 @@ class LineCmd extends React.Component< { name: "computed-shouldCmdFocus" } ) .get(); + let isInSidebar = mobx + .computed( + () => { + return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid); + }, + { name: "computed-isInSidebar" } + ) + .get(); let isStatic = staticRender; let isRunning = cmd.isRunning(); let isExpanded = this.isCmdExpanded.get(); @@ -622,6 +641,7 @@ class LineCmd extends React.Component< if (rtnStateDiffSize < 10) { rtnStateDiffSize = Math.max(termFontSize, 10); } + let containerType = screen.getContainerType(); return (
{this.renderCmdText(cmd)}
-
- -
- -
-
- {this.isMinimised.get() ? : } +
+ +
+ + + + + + +
+
+ +
+
+ +
+ +
+
- + +
+   showing in sidebar => +
+
+ = mobx.IObservableValue; @mobxReact.observer class ScreenView extends React.Component<{ session: Session; screen: Screen }, {}> { + rszObs: ResizeObserver; + screenViewRef: React.RefObject = React.createRef(); + width: OV = mobx.observable.box(null, { name: "screenview-width" }); + handleResize_debounced: () => void; + + constructor(props: any) { + super(props); + this.handleResize_debounced = debounce(100, this.handleResize.bind(this)); + } + + componentDidMount(): void { + let elem = this.screenViewRef.current; + if (elem != null) { + this.rszObs = new ResizeObserver(this.handleResize_debounced); + this.rszObs.observe(elem); + this.handleResize(); + } + } + + componentWillUnmount(): void { + if (this.rszObs != null) { + this.rszObs.disconnect(); + } + } + + handleResize() { + let elem = this.screenViewRef.current; + if (elem == null) { + return; + } + mobx.action(() => { + this.width.set(elem.offsetWidth); + })(); + } + render() { let { session, screen } = this.props; if (screen == null) { - return
(no screen found)
; + return ( +
+ (no screen found) +
+ ); + } + let screenWidth = this.width.get(); + if (screenWidth == null) { + return
; } let fontSize = GlobalModel.termFontSize.get(); + let viewOpts = screen.viewOpts.get(); + let hasSidebar = viewOpts?.sidebar?.open; + let winWidth = "100%"; + let sidebarWidth = "0px"; + if (hasSidebar) { + let targetWidth = viewOpts?.sidebar?.width; + let realWidth = 0; + if (util.isBlank(targetWidth) || screenWidth < (MagicLayout.ScreenSidebarMinWidth * 2)) { + realWidth = Math.floor(screenWidth / 2) - MagicLayout.ScreenSidebarWidthPadding; + } else if (targetWidth.indexOf("%") != -1) { + let targetPercent = parseInt(targetWidth); + if (targetPercent > 100) { + targetPercent = 100; + } + let targetMul = targetPercent / 100; + realWidth = Math.floor((screenWidth * targetPercent) / 100); + realWidth = util.boundInt(realWidth, MagicLayout.ScreenSidebarMinWidth, screenWidth - MagicLayout.ScreenSidebarMinWidth); + } else { + // screen is at least 400px wide + let targetWidthNum = parseInt(targetWidth); + realWidth = util.boundInt(targetWidthNum, MagicLayout.ScreenSidebarMinWidth, screenWidth - MagicLayout.ScreenSidebarMinWidth); + } + winWidth = screenWidth - realWidth + "px"; + sidebarWidth = realWidth - MagicLayout.ScreenSidebarWidthPadding + "px"; + } return ( -
- +
+ + + + +
+ ); + } +} + +type SidebarLineContainerPropsType = { + screen: Screen; + winSize: T.WindowSize; + lineId: string; +}; + +// note a new SidebarLineContainer will be made for every lineId (so lineId prop should never change) +// implemented using a 'key' in parent +@mobxReact.observer +class SidebarLineContainer extends React.Component { + container: ForwardLineContainer; + overrideCollapsed: OV = mobx.observable.box(false, { name: "overrideCollapsed" }); + visible: OV = mobx.observable.box(true, { name: "visible" }); + ready: OV = mobx.observable.box(false, { name: "ready" }); + + componentDidMount(): void { + let { screen, winSize, lineId } = this.props; + // TODO this is a hack for now to make the timing work out. + setTimeout(() => { + mobx.action(() => { + this.container = new ForwardLineContainer(screen, winSize, appconst.LineContainer_Sidebar, lineId); + this.ready.set(true); + })(); + }, 100); + } + + @boundMethod + handleHeightChange() {} + + componentDidUpdate(prevProps: SidebarLineContainerPropsType): void { + let prevWinSize = prevProps.winSize; + let winSize = this.props.winSize; + if (prevWinSize.width != winSize.width || prevWinSize.height != winSize.height) { + if (this.container != null) { + this.container.screenSizeCallback(mobx.toJS(winSize)); + } + } + } + + render() { + if (!this.ready.get() || this.container == null) { + return null; + } + let { screen, winSize, lineId } = this.props; + let line = screen.getLineById(lineId); + if (line == null) { + return null; + } + return ( + + ); + } +} + +@mobxReact.observer +class ScreenSidebar extends React.Component<{ screen: Screen; width: string }, {}> { + rszObs: ResizeObserver; + sidebarSize: OV = mobx.observable.box({ height: 0, width: 0 }, { name: "sidebarSize" }); + sidebarRef: React.RefObject = React.createRef(); + handleResize_debounced: (entries: ResizeObserverEntry[]) => void; + + constructor(props: any) { + super(props); + this.handleResize_debounced = debounce(100, this.handleResize.bind(this)); + } + + componentDidMount(): void { + let { screen } = this.props; + let sidebarElem = this.sidebarRef.current; + if (sidebarElem != null) { + this.rszObs = new ResizeObserver(this.handleResize_debounced); + this.rszObs.observe(sidebarElem); + this.handleResize([]); + } + let size = this.sidebarSize.get(); + } + + componentWillUnmount(): void { + if (this.rszObs != null) { + this.rszObs.disconnect(); + } + } + + @boundMethod + handleResize(entries: ResizeObserverEntry[]): void { + // dont use entries (just use the ref) -- we call it with an empty array in componentDidMount to initialize it + let sidebarElem = this.sidebarRef.current; + if (sidebarElem == null) { + return; + } + let size = { + width: sidebarElem.offsetWidth - MagicLayout.ScreenMaxContentWidthBuffer, + height: sidebarElem.offsetHeight - MagicLayout.ScreenMaxContentHeightBuffer - MagicLayout.ScreenSidebarHeaderHeight, + }; + mobx.action(() => this.sidebarSize.set(size))(); + } + + @boundMethod + sidebarClose(): void { + GlobalCommandRunner.screenSidebarClose(); + } + + @boundMethod + sidebarOpenHalf(): void { + GlobalCommandRunner.screenSidebarOpen("50%"); + } + + @boundMethod + sidebarOpenPartial(): void { + GlobalCommandRunner.screenSidebarOpen("500px"); + } + + getSidebarConfig(): T.ScreenSidebarOptsType { + let { screen } = this.props; + let viewOpts = screen.viewOpts.get(); + return viewOpts?.sidebar; + } + + render() { + let { screen, width } = this.props; + let sidebarSize = this.sidebarSize.get(); + let sidebar = this.getSidebarConfig(); + let lineId = sidebar?.sidebarlineid; + let sidebarOk = sidebarSize != null && sidebarSize.width > 0 && !util.isBlank(sidebar?.sidebarlineid); + return ( +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
No Sidebar Line Selected
+
+ /sidebar:open [width=[50%|500px]] +
+ /sidebar:close +
+ /sidebar:add line=[linenum] +
+
+
+ +
+
+
+ + +
); } @@ -244,8 +498,8 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> { // screen is not null @mobxReact.observer -class ScreenWindowView extends React.Component<{ session: Session; screen: Screen }, {}> { - rszObs: any; +class ScreenWindowView extends React.Component<{ session: Session; screen: Screen; width: string }, {}> { + rszObs: ResizeObserver; windowViewRef: React.RefObject; width: mobx.IObservableValue = mobx.observable.box(0, { name: "sw-view-width" }); @@ -325,7 +579,12 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree renderError(message: string, fade: boolean) { let { screen } = this.props; return ( -
+
{message}
@@ -404,7 +663,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree let lines = this.determineVisibleLines(win); let renderMode = this.renderMode.get(); return ( -
+
void; getMaxContentSize(): WindowSize; getIdealContentSize(): WindowSize; + isSidebarOpen(): boolean; + isLineIdInSidebar(lineId: string): boolean; + getContainerType(): T.LineContainerStrs; }; type SWLinePtr = { @@ -341,6 +344,7 @@ class Screen { screenId: string; screenIdx: OV; opts: OV; + viewOpts: OV; name: OV; archived: OV; curRemote: OV; @@ -368,6 +372,7 @@ class Screen { name: "screen-screenidx", }); this.opts = mobx.observable.box(sdata.screenopts, { name: "screen-opts" }); + this.viewOpts = mobx.observable.box(sdata.screenviewopts, { name: "viewOpts" }); this.archived = mobx.observable.box(!!sdata.archived, { name: "screen-archived", }); @@ -405,6 +410,29 @@ class Screen { return this.shareMode.get() == "web" && this.webShareOpts.get() != null; } + isSidebarOpen(): boolean { + let viewOpts = this.viewOpts.get(); + if (viewOpts == null) { + return false; + } + return viewOpts.sidebar?.open; + } + + isLineIdInSidebar(lineId: string): boolean { + let viewOpts = this.viewOpts.get(); + if (viewOpts == null) { + return false; + } + if (!viewOpts.sidebar?.open) { + return false; + } + return viewOpts?.sidebar?.sidebarlineid == lineId; + } + + getContainerType(): T.LineContainerStrs { + return appconst.LineContainer_Main; + } + getShareName(): string { if (!this.isWebShared()) { return null; @@ -441,6 +469,7 @@ class Screen { mobx.action(() => { this.screenIdx.set(data.screenidx); this.opts.set(data.screenopts); + this.viewOpts.set(data.screenviewopts); this.name.set(data.name); this.nextLineNum.set(data.nextlinenum); this.archived.set(!!data.archived); @@ -468,6 +497,10 @@ class Screen { return GlobalModel.getCmd(line); } + getCmdById(lineId: string): Cmd { + return GlobalModel.getCmdByScreenLine(this.screenId, lineId); + } + getAnchorStr(): string { let anchor = this.anchor.get(); if (anchor.anchorLine == null || anchor.anchorLine == 0) { @@ -590,6 +623,26 @@ class Screen { return null; } + getLineById(lineId: string): LineType { + if (lineId == null) { + return null; + } + let win = this.getScreenLines(); + if (win == null) { + return null; + } + let lines = win.lines; + if (lines == null || lines.length == 0) { + return null; + } + for (let i = 0; i < lines.length; i++) { + if (lines[i].lineid == lineId) { + return lines[i]; + } + } + return null; + } + getPresentLineNum(lineNum: number): number { let win = this.getScreenLines(); if (win == null || !win.loaded.get()) { @@ -704,10 +757,16 @@ class Screen { } this.lastRows = rows; this.lastCols = cols; + let exclude = []; for (let lineid in this.terminals) { - this.terminals[lineid].resizeCols(cols); + let inSidebar = this.isLineIdInSidebar(lineid); + if (!inSidebar) { + this.terminals[lineid].resizeCols(cols); + } else { + exclude.push(lineid); + } } - GlobalCommandRunner.resizeScreen(this.screenId, rows, cols); + GlobalCommandRunner.resizeScreen(this.screenId, rows, cols, { exclude }); } getTermWrap(lineId: string): TermWrap { @@ -1779,23 +1838,142 @@ type LineFocusType = { screenid?: string; }; -class SpecialHistoryViewLineContainer { - historyItem: HistoryItem; +type CmdFinder = { + getCmdById(cmdId: string): Cmd; +}; + +class ForwardLineContainer { + winSize: T.WindowSize; + screen: Screen; + containerType: T.LineContainerStrs; + lineId: string; + + constructor(screen: Screen, winSize: T.WindowSize, containerType: T.LineContainerStrs, lineId: string) { + this.screen = screen; + this.winSize = winSize; + this.containerType = containerType; + this.lineId = lineId; + } + + screenSizeCallback(winSize: WindowSize): void { + this.winSize = winSize; + let termWrap = this.getTermWrap(this.lineId); + if (termWrap != null) { + let fontSize = GlobalModel.termFontSize.get(); + let cols = windowWidthToCols(winSize.width, fontSize); + let rows = windowHeightToRows(winSize.height, fontSize); + termWrap.resizeCols(cols); + GlobalCommandRunner.resizeScreen(this.screen.screenId, rows, cols, { include: [this.lineId] }); + } + } + + getContainerType(): T.LineContainerStrs { + return this.containerType; + } + + getCmd(line: LineType): Cmd { + return this.screen.getCmd(line); + } + + isSidebarOpen(): boolean { + return false; + } + + isLineIdInSidebar(lineId: string): boolean { + return false; + } + + setLineFocus(lineNum: number, focus: boolean): void { + this.screen.setLineFocus(lineNum, focus); + } + + setContentHeight(context: RendererContext, height: number): void { + return; + } + + getMaxContentSize(): WindowSize { + return this.winSize; + } + + getIdealContentSize(): WindowSize { + return this.winSize; + } + + loadTerminalRenderer(elem: Element, line: LineType, cmd: Cmd, width: number): void { + this.screen.loadTerminalRenderer(elem, line, cmd, width); + } + + registerRenderer(lineId: string, renderer: RendererModel): void { + this.screen.registerRenderer(lineId, renderer); + } + + unloadRenderer(lineId: string): void { + this.screen.unloadRenderer(lineId); + } + + getContentHeight(context: RendererContext): number { + return this.screen.getContentHeight(context); + } + + getUsedRows(context: RendererContext, line: LineType, cmd: Cmd, width: number): number { + return this.screen.getUsedRows(context, line, cmd, width); + } + + getIsFocused(lineNum: number): boolean { + return this.screen.getIsFocused(lineNum); + } + + getRenderer(lineId: string): RendererModel { + return this.screen.getRenderer(lineId); + } + + getTermWrap(lineId: string): TermWrap { + return this.screen.getTermWrap(lineId); + } + + getFocusType(): FocusTypeStrs { + return this.screen.getFocusType(); + } + + getSelectedLine(): number { + return this.screen.getSelectedLine(); + } +} + +class SpecialLineContainer { + wsize: T.WindowSize; + allowInput: boolean; terminal: TermWrap; renderer: RendererModel; cmd: Cmd; + cmdFinder: CmdFinder; + containerType: T.LineContainerStrs; - constructor(hitem: HistoryItem) { - this.historyItem = hitem; + constructor(cmdFinder: CmdFinder, wsize: T.WindowSize, allowInput: boolean, containerType: T.LineContainerStrs) { + this.cmdFinder = cmdFinder; + this.wsize = wsize; + this.allowInput = allowInput; } getCmd(line: LineType): Cmd { if (this.cmd == null) { - this.cmd = GlobalModel.historyViewModel.getCmdById(line.lineid); + this.cmd = this.cmdFinder.getCmdById(line.lineid); } return this.cmd; } + getContainerType(): T.LineContainerStrs { + return this.containerType; + } + + isSidebarOpen(): boolean { + return false; + } + + isLineIdInSidebar(lineId: string): boolean { + return false; + } + setLineFocus(lineNum: number, focus: boolean): void { return; } @@ -1805,15 +1983,11 @@ class SpecialHistoryViewLineContainer { } getMaxContentSize(): WindowSize { - let width = termWidthFromCols(80, GlobalModel.termFontSize.get()); - let height = termHeightFromRows(25, GlobalModel.termFontSize.get()); - return { width, height }; + return this.wsize; } getIdealContentSize(): WindowSize { - let width = termWidthFromCols(80, GlobalModel.termFontSize.get()); - let height = termHeightFromRows(25, GlobalModel.termFontSize.get()); - return { width, height }; + return this.wsize; } loadTerminalRenderer(elem: Element, line: LineType, cmd: Cmd, width: number): void { @@ -1956,7 +2130,7 @@ class HistoryViewModel { historyItemLines: LineType[] = []; historyItemCmds: CmdDataType[] = []; - specialLineContainer: SpecialHistoryViewLineContainer; + specialLineContainer: SpecialLineContainer; constructor() {} @@ -2015,7 +2189,14 @@ class HistoryViewModel { this.specialLineContainer = null; } else { this.activeItem.set(hitem.historyid); - this.specialLineContainer = new SpecialHistoryViewLineContainer(hitem); + let width = termWidthFromCols(80, GlobalModel.termFontSize.get()); + let height = termHeightFromRows(25, GlobalModel.termFontSize.get()); + this.specialLineContainer = new SpecialLineContainer( + this, + { width, height }, + false, + appconst.LineContainer_History + ); } })(); } @@ -2751,26 +2932,26 @@ class RemotesModel { mobx.action(() => { this.selectedRemoteId.set(remoteId); this.remoteEdit.set(null); - GlobalModel.modalsModel.pushModal(constants.VIEW_REMOTE); + GlobalModel.modalsModel.pushModal(appconst.VIEW_REMOTE); })(); } openAddModal(redit: RemoteEditType): void { mobx.action(() => { this.remoteEdit.set(redit); - GlobalModel.modalsModel.pushModal(constants.CREATE_REMOTE); + GlobalModel.modalsModel.pushModal(appconst.CREATE_REMOTE); })(); } openEditModal(redit?: RemoteEditType): void { if (redit == null) { this.startEditAuth(); - GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); + GlobalModel.modalsModel.pushModal(appconst.EDIT_REMOTE); } else { mobx.action(() => { this.selectedRemoteId.set(redit?.remoteid); this.remoteEdit.set(redit); - GlobalModel.modalsModel.pushModal(constants.EDIT_REMOTE); + GlobalModel.modalsModel.pushModal(appconst.EDIT_REMOTE); })(); } } @@ -2996,9 +3177,10 @@ class Model { this.clientId = getApi().getId(); this.isDev = getApi().getIsDev(); this.authKey = getApi().getAuthKey(); - this.ws = new WSControl(this.getBaseWsHostPort(), this.clientId, this.authKey, (message: any) => - this.runUpdate(message, false) - ); + this.ws = new WSControl(this.getBaseWsHostPort(), this.clientId, this.authKey, (message: any) => { + let interactive = message?.interactive ?? false; + this.runUpdate(message, interactive); + }); this.ws.reconnect(); this.inputModel = new InputModel(); this.pluginsModel = new PluginsModel(); @@ -3096,7 +3278,7 @@ class Model { showAlert(alertMessage: AlertMessageType): Promise { mobx.action(() => { this.alertMessage.set(alertMessage); - GlobalModel.modalsModel.pushModal(constants.ALERT); + GlobalModel.modalsModel.pushModal(appconst.ALERT); })(); let prtn = new Promise((resolve, reject) => { this.alertPromiseResolver = resolve; @@ -3219,6 +3401,23 @@ class Model { e.preventDefault(); GlobalCommandRunner.bookmarksView(); } + if ( + this.activeMainView.get() == "session" && + e.code == "KeyS" && + e.getModifierState("Meta") && + e.getModifierState("Control") + ) { + e.preventDefault(); + let activeScreen = this.getActiveScreen(); + if (activeScreen != null) { + let isSidebarOpen = activeScreen.isSidebarOpen(); + if (isSidebarOpen) { + GlobalCommandRunner.screenSidebarClose(); + } else { + GlobalCommandRunner.screenSidebarOpen(); + } + } + } } clearModals(): boolean { @@ -3375,7 +3574,7 @@ class Model { onMenuItemAbout(): void { mobx.action(() => { - this.modalsModel.pushModal(constants.ABOUT); + this.modalsModel.pushModal(appconst.ABOUT); })(); } @@ -4098,14 +4297,23 @@ class CommandRunner { GlobalModel.submitCommand("screen", "close", [screen], { nohist: "1" }, false); } - resizeScreen(screenId: string, rows: number, cols: number) { - GlobalModel.submitCommand( - "screen", - "resize", - null, - { nohist: "1", screen: screenId, cols: String(cols), rows: String(rows) }, - false - ); + // include is lineIds to include, exclude is lineIds to exclude + // if include is given then it *only* does those ids. if exclude is given (or not), + // it does all running commands in the screen except for excluded. + resizeScreen(screenId: string, rows: number, cols: number, opts?: { include?: string[]; exclude?: string[] }) { + let kwargs: Record = { + nohist: "1", + screen: screenId, + cols: String(cols), + rows: String(rows), + }; + if (opts?.include != null && opts?.include.length > 0) { + kwargs.include = opts.include.join(","); + } + if (opts?.exclude != null && opts?.exclude.length > 0) { + kwargs.exclude = opts.exclude.join(","); + } + GlobalModel.submitCommand("screen", "resize", null, kwargs, false); } screenArchive(screenId: string, shouldArchive: boolean): Promise { @@ -4380,6 +4588,26 @@ class CommandRunner { interactive ); } + + screenSidebarAddLine(lineId: string) { + GlobalModel.submitCommand("sidebar", "add", null, { nohist: "1", line: lineId }, false); + } + + screenSidebarRemove() { + GlobalModel.submitCommand("sidebar", "remove", null, { nohist: "1" }, false); + } + + screenSidebarClose(): void { + GlobalModel.submitCommand("sidebar", "close", null, { nohist: "1" }, false); + } + + screenSidebarOpen(width?: string): void { + let kwargs: Record = { nohist: "1" }; + if (width != null) { + kwargs.width = width; + } + GlobalModel.submitCommand("sidebar", "open", null, kwargs, false); + } } function cmdPacketString(pk: FeCmdPacketType): string { @@ -4467,6 +4695,8 @@ export { RemotesModel, MinFontSize, MaxFontSize, - VERSION + SpecialLineContainer, + ForwardLineContainer, + VERSION, }; export type { LineContainerModel }; diff --git a/src/types/types.ts b/src/types/types.ts index ded3df71..fa693ee3 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -8,6 +8,7 @@ type ShareModeType = "local" | "web"; type FocusTypeStrs = "input" | "cmd"; type HistoryTypeStrs = "global" | "session" | "screen"; type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error"; +type LineContainerStrs = "main" | "sidebar" | "history"; type OV = mobx.IObservableValue; @@ -59,6 +60,16 @@ type WebShareOpts = { viewkey: string; }; +type ScreenViewOptsType = { + sidebar: ScreenSidebarOptsType; +}; + +type ScreenSidebarOptsType = { + open: boolean; + width: string; + sidebarlineid: string; +}; + type ScreenDataType = { sessionid: string; screenid: string; @@ -68,6 +79,7 @@ type ScreenDataType = { webshareopts?: WebShareOpts; archived?: boolean; screenopts: ScreenOptsType; + screenviewopts: ScreenViewOptsType; curremote: RemotePtrType; nextlinenum: number; selectedline: number; @@ -458,7 +470,7 @@ type ClientOptsType = { acceptedtos: number; }; -type ReleaseInfoType = { +type ReleaseInfoType = { latestversion: string; }; @@ -660,6 +672,8 @@ export type { FeCmdPacketType, TermOptsType, CmdDataType, + ScreenViewOptsType, + ScreenSidebarOptsType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, @@ -719,4 +733,5 @@ export type { FileInfoType, ExtBlob, ExtFile, + LineContainerStrs, }; diff --git a/wavesrv/db/migrations/000026_screenviewopts.down.sql b/wavesrv/db/migrations/000026_screenviewopts.down.sql new file mode 100644 index 00000000..79633b7a --- /dev/null +++ b/wavesrv/db/migrations/000026_screenviewopts.down.sql @@ -0,0 +1 @@ +ALTER TABLE screen DROP COLUMN screenviewopts; diff --git a/wavesrv/db/migrations/000026_screenviewopts.up.sql b/wavesrv/db/migrations/000026_screenviewopts.up.sql new file mode 100644 index 00000000..c7fd33ff --- /dev/null +++ b/wavesrv/db/migrations/000026_screenviewopts.up.sql @@ -0,0 +1 @@ +ALTER TABLE screen ADD COLUMN screenviewopts json DEFAULT '{}'; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index d1bd79c4..8830cc8e 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -63,6 +63,7 @@ const MaxSignalNum = 64 const MaxEvalDepth = 5 const MaxOpenAIAPITokenLen = 100 const MaxOpenAIModelLen = 100 +const MaxSidebarSections = 5 const TermFontSizeMin = 8 const TermFontSizeMax = 24 @@ -209,6 +210,11 @@ func init() { registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand) registerCmdFn("client:accepttos", ClientAcceptTosCommand) + registerCmdFn("sidebar:open", SidebarOpenCommand) + registerCmdFn("sidebar:close", SidebarCloseCommand) + registerCmdFn("sidebar:add", SidebarAddCommand) + registerCmdFn("sidebar:remove", SidebarRemoveCommand) + registerCmdFn("telemetry", TelemetryCommand) registerCmdFn("telemetry:on", TelemetryOnCommand) registerCmdFn("telemetry:off", TelemetryOffCommand) @@ -308,6 +314,20 @@ func argN(pk *scpacket.FeCommandPacketType, n int) string { return pk.Args[n] } +// will trim strings for whitespace +func resolveCommaSepListToMap(arg string) map[string]bool { + if arg == "" { + return nil + } + rtn := make(map[string]bool) + fields := strings.Split(arg, ",") + for _, field := range fields { + field = strings.TrimSpace(field) + rtn[field] = true + } + return rtn +} + func resolveBool(arg string, def bool) bool { if arg == "" { return def @@ -578,10 +598,26 @@ func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.U return nil, err } update.Interactive = pk.Interactive + // this update is sent asynchronously for timing issues. the cmd update comes async as well + // so if we return this directly it sometimes gets evaluated first. by pushing it on the MainBus + // it ensures it happens after the command creation event. sstore.MainBus.SendScreenUpdate(ids.ScreenId, update) return nil, nil } +func implementRunInSidebar(ctx context.Context, screenId string, lineId string) (*sstore.ScreenType, error) { + screen, err := sidebarSetOpen(ctx, "run", screenId, true, "") + if err != nil { + return nil, err + } + screen.ScreenViewOpts.Sidebar.SidebarLineId = lineId + err = sstore.ScreenUpdateViewOpts(ctx, screenId, screen.ScreenViewOpts) + if err != nil { + return nil, fmt.Errorf("/run error updating screenviewopts: %v", err) + } + return screen, nil +} + func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, historyContext historyContextType, isMetaCmd bool, hadError bool) error { cmdStr := firstArg(pk) ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) @@ -641,12 +677,36 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore. update, rtnErr = HandleCommand(ctxWithHistory, newPk) } if !resolveBool(pk.Kwargs["nohist"], false) { + // TODO should this be "pk" or "newPk" (2nd arg) err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil)) if err != nil { log.Printf("[error] adding to history: %v\n", err) // fall through (non-fatal error) } } + var hasModelUpdate bool + var modelUpdate *sstore.ModelUpdate + if update == nil { + hasModelUpdate = true + modelUpdate = &sstore.ModelUpdate{} + update = modelUpdate + } else if mu, ok := update.(*sstore.ModelUpdate); ok { + hasModelUpdate = true + modelUpdate = mu + } + if resolveBool(newPk.Kwargs["sidebar"], false) && historyContext.LineId != "" && hasModelUpdate { + ids, resolveErr := resolveUiIds(ctx, newPk, R_Session|R_Screen) + // we are ignoring resolveErr (if not nil). obviously can't add to sidebar and + // either another error already happened, or this command was never about the sidebar + if resolveErr == nil { + screen, sidebarErr := implementRunInSidebar(ctx, ids.ScreenId, historyContext.LineId) + if sidebarErr == nil { + modelUpdate.UpdateScreen(screen) + } else { + modelUpdate.AddInfoError(fmt.Sprintf("cannot move command to sidebar: %v", sidebarErr)) + } + } + } return update, rtnErr } @@ -776,6 +836,8 @@ func ScreenReorderCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return update, nil } +var screenAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$") + func ScreenSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen) if err != nil { @@ -908,7 +970,115 @@ func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor return update, nil } -var screenAnchorRe = regexp.MustCompile("^(\\d+)(?::(-?\\d+))?$") +var sidebarWidthRe = regexp.MustCompile("^\\d+(px|%)$") + +func sidebarSetOpen(ctx context.Context, cmdStr string, screenId string, open bool, width string) (*sstore.ScreenType, error) { + if width != "" && !sidebarWidthRe.MatchString(width) { + return nil, fmt.Errorf("/%s invalid width specified, must be either a px value or a percent (e.g. '300px' or '50%%')", cmdStr) + } + if strings.HasSuffix(width, "%") { + percentNum, _ := strconv.Atoi(width[:len(width)-1]) + if percentNum < 10 || percentNum > 90 { + return nil, fmt.Errorf("/%s invalid width specified, percentage must be between 10%% and 90%%", cmdStr) + } + } + if strings.HasSuffix(width, "px") { + pxNum, _ := strconv.Atoi(width[:len(width)-2]) + if pxNum < 200 { + return nil, fmt.Errorf("/%s invalid width specified, minimum sizebar width is 200px", cmdStr) + } + } + screen, err := sstore.GetScreenById(ctx, screenId) + if err != nil { + return nil, fmt.Errorf("/%s cannot get screen: %v", cmdStr, err) + } + if screen.ScreenViewOpts.Sidebar == nil { + screen.ScreenViewOpts.Sidebar = &sstore.ScreenSidebarOptsType{} + } + screen.ScreenViewOpts.Sidebar.Open = open + if width != "" { + screen.ScreenViewOpts.Sidebar.Width = width + } + err = sstore.ScreenUpdateViewOpts(ctx, screenId, screen.ScreenViewOpts) + if err != nil { + return nil, fmt.Errorf("/%s error updating screenviewopts: %v", cmdStr, err) + } + return screen, nil +} + +func SidebarOpenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Screen) + if err != nil { + return nil, err + } + screen, err := sidebarSetOpen(ctx, GetCmdStr(pk), ids.ScreenId, true, pk.Kwargs["width"]) + if err != nil { + return nil, err + } + return &sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}, nil +} + +func SidebarCloseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Screen) + if err != nil { + return nil, err + } + screen, err := sidebarSetOpen(ctx, GetCmdStr(pk), ids.ScreenId, false, "") + if err != nil { + return nil, err + } + return &sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}, nil +} + +func SidebarAddCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Screen) + if err != nil { + return nil, err + } + var addLineId string + if lineArg, ok := pk.Kwargs["line"]; ok { + lineId, err := sstore.FindLineIdByArg(ctx, ids.ScreenId, lineArg) + if err != nil { + return nil, fmt.Errorf("error looking up lineid: %v", err) + } + addLineId = lineId + } + if addLineId == "" { + return nil, fmt.Errorf("/%s must specify line=[lineid] to add to the sidebar", GetCmdStr(pk)) + } + screen, err := sidebarSetOpen(ctx, GetCmdStr(pk), ids.ScreenId, true, pk.Kwargs["width"]) + if err != nil { + return nil, err + } + screen.ScreenViewOpts.Sidebar.SidebarLineId = addLineId + err = sstore.ScreenUpdateViewOpts(ctx, ids.ScreenId, screen.ScreenViewOpts) + if err != nil { + return nil, fmt.Errorf("/%s error updating screenviewopts: %v", GetCmdStr(pk), err) + } + return &sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}, nil +} + +func SidebarRemoveCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Screen) + if err != nil { + return nil, err + } + screen, err := sstore.GetScreenById(ctx, ids.ScreenId) + if err != nil { + return nil, fmt.Errorf("/%s cannot get screeen: %v", GetCmdStr(pk), err) + } + sidebar := screen.ScreenViewOpts.Sidebar + if sidebar == nil { + return nil, nil + } + sidebar.SidebarLineId = "" + sidebar.Open = false + err = sstore.ScreenUpdateViewOpts(ctx, ids.ScreenId, screen.ScreenViewOpts) + if err != nil { + return nil, fmt.Errorf("/%s error updating screenviewopts: %v", GetCmdStr(pk), err) + } + return &sstore.ModelUpdate{Screens: []*sstore.ScreenType{screen}}, nil +} func RemoteInstallCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_Remote) @@ -2629,7 +2799,15 @@ func ScreenResizeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) if len(runningCmds) == 0 { return nil, nil } + includeMap := resolveCommaSepListToMap(pk.Kwargs["include"]) + excludeMap := resolveCommaSepListToMap(pk.Kwargs["exclude"]) for _, cmd := range runningCmds { + if excludeMap[cmd.LineId] { + continue + } + if len(includeMap) > 0 && !includeMap[cmd.LineId] { + continue + } if int(cmd.TermOpts.Cols) != cols { resizeRunningCommand(ctx, cmd, cols) } diff --git a/wavesrv/pkg/cmdrunner/shparse.go b/wavesrv/pkg/cmdrunner/shparse.go index 67e21182..d4945865 100644 --- a/wavesrv/pkg/cmdrunner/shparse.go +++ b/wavesrv/pkg/cmdrunner/shparse.go @@ -299,6 +299,7 @@ func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) rtnPk.Kwargs = make(map[string]string) rtnPk.UIContext = origPk.UIContext rtnPk.RawStr = origPk.RawStr + rtnPk.Interactive = origPk.Interactive for key, val := range origPk.Kwargs { rtnPk.Kwargs[key] = val } diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index 75b778ca..20f4f21a 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -567,14 +567,14 @@ func InsertSessionWithName(ctx context.Context, sessionName string, activate boo if err != nil { return nil, err } - update := ModelUpdate{ + update := &ModelUpdate{ Sessions: []*SessionType{session}, Screens: []*ScreenType{newScreen}, } if activate { update.ActiveSessionId = newSessionId } - return &update, nil + return update, nil } func SetActiveSessionId(ctx context.Context, sessionId string) error { @@ -710,8 +710,8 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string, Archived: false, ArchivedTs: 0, } - query = `INSERT INTO screen ( sessionid, screenid, name, screenidx, screenopts, ownerid, sharemode, webshareopts, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) - VALUES (:sessionid,:screenid,:name,:screenidx,:screenopts,:ownerid,:sharemode,:webshareopts,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:selectedline,:anchor,:focustype,:archived,:archivedts)` + query = `INSERT INTO screen ( sessionid, screenid, name, screenidx, screenopts, screenviewopts, ownerid, sharemode, webshareopts, curremoteownerid, curremoteid, curremotename, nextlinenum, selectedline, anchor, focustype, archived, archivedts) + VALUES (:sessionid,:screenid,:name,:screenidx,:screenopts,:screenviewopts,:ownerid,:sharemode,:webshareopts,:curremoteownerid,:curremoteid,:curremotename,:nextlinenum,:selectedline,:anchor,:focustype,:archived,:archivedts)` tx.NamedExec(query, screen.ToMap()) if activate { query = `UPDATE session SET activescreenid = ? WHERE sessionid = ?` @@ -1150,7 +1150,7 @@ func PurgeScreen(ctx context.Context, screenId string, sessionDel bool) (UpdateP return nil, nil } update := &ModelUpdate{} - update.Screens = []*ScreenType{&ScreenType{SessionId: sessionId, ScreenId: screenId, Remove: true}} + update.Screens = []*ScreenType{{SessionId: sessionId, ScreenId: screenId, Remove: true}} if isActive { bareSession, err := GetBareSessionById(ctx, sessionId) if err != nil { @@ -1764,6 +1764,14 @@ func UpdateScreen(ctx context.Context, screenId string, editMap map[string]inter return GetScreenById(ctx, screenId) } +func ScreenUpdateViewOpts(ctx context.Context, screenId string, viewOpts ScreenViewOptsType) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE screen SET screenviewopts = ? WHERE screenid = ?` + tx.Exec(query, quickJson(viewOpts), screenId) + return nil + }) +} + func GetLineResolveItems(ctx context.Context, screenId string) ([]ResolveItem, error) { var rtn []ResolveItem txErr := WithTx(ctx, func(tx *TxWrap) error { diff --git a/wavesrv/pkg/sstore/migrate.go b/wavesrv/pkg/sstore/migrate.go index 4d4c9b24..cd095db2 100644 --- a/wavesrv/pkg/sstore/migrate.go +++ b/wavesrv/pkg/sstore/migrate.go @@ -22,7 +22,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 25 +const MaxMigration = 26 const MigratePrimaryScreenVersion = 9 const CmdScreenSpecialMigration = 13 const CmdLineSpecialMigration = 20 @@ -51,12 +51,12 @@ func copyFile(srcFile string, dstFile string, notFoundOk bool) error { return nil } if err != nil { - return fmt.Errorf("cannot open %s: %v", err) + return fmt.Errorf("cannot open %s: %v", srcFile, err) } defer srcFd.Close() dstFd, err := os.OpenFile(dstFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { - return fmt.Errorf("cannot open destination file %s: %v", err) + return fmt.Errorf("cannot open destination file %s: %v", dstFile, err) } _, err = io.Copy(dstFd, srcFd) if err != nil { diff --git a/wavesrv/pkg/sstore/sstore.go b/wavesrv/pkg/sstore/sstore.go index 527e06fc..ba9471bc 100644 --- a/wavesrv/pkg/sstore/sstore.go +++ b/wavesrv/pkg/sstore/sstore.go @@ -467,22 +467,35 @@ func (sco ScreenCreateOpts) HasCopy() bool { return sco.CopyRemote || sco.CopyCwd || sco.CopyEnv } +type ScreenSidebarOptsType struct { + Open bool `json:"open,omitempty"` + Width string `json:"width,omitempty"` + + // this used to be more complicated (sections with types). simplified for this release + SidebarLineId string `json:"sidebarlineid,omitempty"` +} + +type ScreenViewOptsType struct { + Sidebar *ScreenSidebarOptsType `json:"sidebar,omitempty"` +} + type ScreenType struct { - SessionId string `json:"sessionid"` - ScreenId string `json:"screenid"` - Name string `json:"name"` - ScreenIdx int64 `json:"screenidx"` - ScreenOpts ScreenOptsType `json:"screenopts"` - OwnerId string `json:"ownerid"` - ShareMode string `json:"sharemode"` - WebShareOpts *ScreenWebShareOpts `json:"webshareopts,omitempty"` - CurRemote RemotePtrType `json:"curremote"` - NextLineNum int64 `json:"nextlinenum"` - SelectedLine int64 `json:"selectedline"` - Anchor ScreenAnchorType `json:"anchor"` - FocusType string `json:"focustype"` - Archived bool `json:"archived,omitempty"` - ArchivedTs int64 `json:"archivedts,omitempty"` + SessionId string `json:"sessionid"` + ScreenId string `json:"screenid"` + Name string `json:"name"` + ScreenIdx int64 `json:"screenidx"` + ScreenOpts ScreenOptsType `json:"screenopts"` + ScreenViewOpts ScreenViewOptsType `json:"screenviewopts"` + OwnerId string `json:"ownerid"` + ShareMode string `json:"sharemode"` + WebShareOpts *ScreenWebShareOpts `json:"webshareopts,omitempty"` + CurRemote RemotePtrType `json:"curremote"` + NextLineNum int64 `json:"nextlinenum"` + SelectedLine int64 `json:"selectedline"` + Anchor ScreenAnchorType `json:"anchor"` + FocusType string `json:"focustype"` + Archived bool `json:"archived,omitempty"` + ArchivedTs int64 `json:"archivedts,omitempty"` // only for updates Full bool `json:"full,omitempty"` @@ -496,6 +509,7 @@ func (s *ScreenType) ToMap() map[string]interface{} { rtn["name"] = s.Name rtn["screenidx"] = s.ScreenIdx rtn["screenopts"] = quickJson(s.ScreenOpts) + rtn["screenviewopts"] = quickJson(s.ScreenViewOpts) rtn["ownerid"] = s.OwnerId rtn["sharemode"] = s.ShareMode rtn["webshareopts"] = quickNullableJson(s.WebShareOpts) @@ -517,6 +531,7 @@ func (s *ScreenType) FromMap(m map[string]interface{}) bool { quickSetStr(&s.Name, m, "name") quickSetInt64(&s.ScreenIdx, m, "screenidx") quickSetJson(&s.ScreenOpts, m, "screenopts") + quickSetJson(&s.ScreenViewOpts, m, "screenviewopts") quickSetStr(&s.OwnerId, m, "ownerid") quickSetStr(&s.ShareMode, m, "sharemode") quickSetNullableJson(&s.WebShareOpts, m, "webshareopts") diff --git a/wavesrv/pkg/sstore/updatebus.go b/wavesrv/pkg/sstore/updatebus.go index 242c1c80..b97510ce 100644 --- a/wavesrv/pkg/sstore/updatebus.go +++ b/wavesrv/pkg/sstore/updatebus.go @@ -69,6 +69,29 @@ func (update *ModelUpdate) Clean() { update.ClientData = update.ClientData.Clean() } +func (update *ModelUpdate) UpdateScreen(newScreen *ScreenType) { + if newScreen == nil { + return + } + for idx, screen := range update.Screens { + if screen.ScreenId == newScreen.ScreenId { + update.Screens[idx] = newScreen + return + } + } + update.Screens = append(update.Screens, newScreen) +} + +// only sets InfoError if InfoError is not already set +func (update *ModelUpdate) AddInfoError(errStr string) { + if update.Info == nil { + update.Info = &InfoMsgType{} + } + if update.Info.InfoError == "" { + update.Info.InfoError = errStr + } +} + type RemoteViewType struct { RemoteShowAll bool `json:"remoteshowall,omitempty"` PtyRemoteId string `json:"ptyremoteid,omitempty"` @@ -219,7 +242,7 @@ func (bus *UpdateBus) SendScreenUpdate(screenId string, update UpdatePacket) { func MakeSessionsUpdateForRemote(sessionId string, ri *RemoteInstance) []*SessionType { return []*SessionType{ - &SessionType{ + { SessionId: sessionId, Remotes: []*RemoteInstance{ri}, },