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.
This commit is contained in:
Mike Sawka
2023-12-17 23:46:53 -08:00
committed by GitHub
parent 781ebe8154
commit 21ab82e2e2
18 changed files with 941 additions and 110 deletions
+4
View File
@@ -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";
+68 -29
View File
@@ -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<boolean> = mobx.observable.box(false, {
name: "line-overflow",
});
isMinimised: OV<boolean> = mobx.observable.box(false, {
isMinimized: OV<boolean> = mobx.observable.box(false, {
name: "line-minimised",
});
isCmdExpanded: OV<boolean> = 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 (
<div
className={mainDivCn}
@@ -639,38 +659,57 @@ class LineCmd extends React.Component<
{this.renderMeta1(cmd)}
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div>
<div
key="pin"
title="Pin"
className={cn("line-icon", { active: line.pinned })}
onClick={this.clickPin}
style={{ display: "none" }}
>
<PinIcon className="icon" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark", "hoverEffect")}
onClick={this.clickBookmark}
>
<FavoritesIcon className="icon" />
</div>
<div
key="minimise"
title={`${this.isMinimised.get() ? "Maximise" : "Minimise"}`}
className={cn(
"line-icon",
"line-minimise",
"hoverEffect",
this.isMinimised.get() ? "line-icon-show" : ""
)}
onClick={this.clickMinimise}
>
{this.isMinimised.get() ? <PlusIcon className="icon plus" /> : <MinusIcon className="icon" />}
<i className="fa-sharp fa-regular fa-bookmark" />
</div>
<If condition={containerType == appconst.LineContainer_Main}>
<div
key="minimize"
title={`${this.isMinimized.get() ? "Maximise" : "Minimize"}`}
className={cn(
"line-icon",
"line-minimize",
"hoverEffect",
this.isMinimized.get() ? "line-icon-show" : ""
)}
onClick={this.clickMinimize}
>
<If condition={this.isMinimized.get()}>
<i className="fa-sharp fa-regular fa-circle-plus" />
</If>
<If condition={!this.isMinimized.get()}>
<i className="fa-sharp fa-regular fa-circle-minus" />
</If>
</div>
<div
className="line-icon line-sidebar"
onClick={this.clickMoveToSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-right-to-line" />
</div>
</If>
<If condition={containerType == appconst.LineContainer_Sidebar}>
<div
className="line-icon line-sidebar"
onClick={this.clickRemoveFromSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-left-to-line" />
</div>
</If>
</div>
<If condition={!this.isMinimised.get()}>
<If condition={isInSidebar}>
<div className="sidebar-message" style={{ fontSize: termFontSize }}>
&nbsp;&nbsp;showing in sidebar =&gt;
</div>
</If>
<If condition={!this.isMinimized.get() && !isInSidebar}>
<ErrorBoundary plugin={rendererPlugin?.name} lineContext={lineutil.getRendererContext(line)}>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer
+6 -11
View File
@@ -35,11 +35,16 @@
}
}
.sidebar-message {
color: @term-yellow;
}
.line-header {
display: flex;
flex-direction: row;
padding-bottom: 0.7rem;
width: 100%;
line-height: 1.2;
&.is-expanded {
height: auto;
@@ -54,9 +59,8 @@
visibility: hidden;
cursor: pointer;
padding: 3px;
width: 2rem;
height: 2rem;
border-radius: 50%;
font-size: 14px;
}
.line-icon-show {
@@ -307,15 +311,6 @@
right: -4px;
}
&.num-4 {
}
&.num-5 {
}
&.num-6 {
}
&.status-done {
background-color: #555;
}
+1 -1
View File
@@ -351,7 +351,7 @@ class LinesView extends React.Component<
}
}
handleResize(entries: any) {
handleResize(entries: ResizeObserverEntry[]) {
let linesElem = this.linesRef.current;
if (linesElem == null) {
return;
+4
View File
@@ -23,6 +23,10 @@ let MagicLayout = {
TermWidthBuffer: 15,
TabWidth: 175,
ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28,
};
let m = MagicLayout;
+1
View File
@@ -9,6 +9,7 @@
position: relative;
font-size: 12.5px;
line-height: 20px;
backdrop-filter: blur(4px);
.title-bar-drag {
-webkit-app-region: drag;
+58 -1
View File
@@ -6,11 +6,68 @@
position: relative;
}
.screen-sidebar {
position: absolute;
top: 0;
right: 0;
display: flex;
flex-direction: column;
height: calc(100% - 0.5rem);
overflow: hidden;
margin-left: 5px;
padding-left: 5px;
overflow-y: auto;
.sidebar-header {
/* sidebar-header height linked to MagicLayout.ScreenSidebarHeaderHeight */
display: flex;
flex-direction: row;
padding: 3px 5px;
border-radius: 3px;
margin: 3px 5px 0 5px;
i {
padding: 3px;
}
}
.screen-sidebar-close {
margin-top: 10px;
}
.close-button-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20px;
margin-bottom: 10px;
}
.screen-sidebar-section {
&:last-child {
padding-bottom: 0;
}
}
.empty-sidebar {
align-self: center;
margin-top: 20%;
.sidebar-help-text {
margin-top: 20px;
padding: 5px 10px;
background-color: #333;
border-radius: 5px;
font-family: @fixed-font;
}
}
}
.window-view {
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: calc(100% - 0.5rem);
overflow-x: hidden;
+267 -8
View File
@@ -10,7 +10,7 @@ import { If, For } from "tsx-control-statements/components";
import cn from "classnames";
import { debounce } from "throttle-debounce";
import dayjs from "dayjs";
import { GlobalCommandRunner, TabColors, TabIcons } from "../../../model/model";
import { GlobalCommandRunner, TabColors, TabIcons, ForwardLineContainer } from "../../../model/model";
import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
import * as T from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
@@ -26,9 +26,12 @@ import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import { ReactComponent as GlobeIcon } from "../../assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "../../assets/icons/statuscircle.svg";
import { termWidthFromCols, termHeightFromRows } from "../../../util/textmeasure";
import * as appconst from "../../appconst";
import "./screenview.less";
import "./tabs.less";
import { MagicLayout } from "../../magiclayout";
dayjs.extend(localizedFormat);
@@ -36,15 +39,266 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class ScreenView extends React.Component<{ session: Session; screen: Screen }, {}> {
rszObs: ResizeObserver;
screenViewRef: React.RefObject<any> = React.createRef();
width: OV<number> = 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 <div className="screen-view">(no screen found)</div>;
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>;
}
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 (
<div className="screen-view" data-screenid={screen.screenId}>
<ScreenWindowView key={screen.screenId + ":" + fontSize} session={session} screen={screen} />
<div className="screen-view" data-screenid={screen.screenId} ref={this.screenViewRef}>
<ScreenWindowView
key={screen.screenId + ":" + fontSize}
session={session}
screen={screen}
width={winWidth}
/>
<If condition={hasSidebar}>
<ScreenSidebar screen={screen} width={sidebarWidth} />
</If>
</div>
);
}
}
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<SidebarLineContainerPropsType, {}> {
container: ForwardLineContainer;
overrideCollapsed: OV<boolean> = mobx.observable.box(false, { name: "overrideCollapsed" });
visible: OV<boolean> = mobx.observable.box(true, { name: "visible" });
ready: OV<boolean> = 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 (
<Line
screen={this.container}
line={line}
width={winSize.width}
staticRender={false}
visible={this.visible}
onHeightChange={this.handleHeightChange}
overrideCollapsed={this.overrideCollapsed}
topBorder={false}
renderMode="normal"
noSelect={true}
/>
);
}
}
@mobxReact.observer
class ScreenSidebar extends React.Component<{ screen: Screen; width: string }, {}> {
rszObs: ResizeObserver;
sidebarSize: OV<T.WindowSize> = mobx.observable.box({ height: 0, width: 0 }, { name: "sidebarSize" });
sidebarRef: React.RefObject<any> = 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 (
<div className="screen-sidebar" style={{ width: width }} ref={this.sidebarRef}>
<div className="sidebar-header">
<div className="flex-spacer" />
<div onClick={this.sidebarOpenHalf} title="Set Sidebar Width to 50%">
<i className="fa-sharp fa-solid fa-table-columns" />
</div>
<div onClick={this.sidebarOpenPartial} title="Set Sidebar Width to 500px">
<i className="fa-sharp fa-solid fa-sidebar-flip" />
</div>
<div onClick={this.sidebarClose} style={{ marginLeft: 5 }}>
<i className="fa-sharp fa-solid fa-xmark" />
</div>
</div>
<If condition={!sidebarOk}>
<div className="empty-sidebar">
<div className="sidebar-main-text">No Sidebar Line Selected</div>
<div className="sidebar-help-text">
/sidebar:open [width=[50%|500px]]
<br />
/sidebar:close
<br />
/sidebar:add line=[linenum]
<br />
</div>
<div onClick={this.sidebarClose} className="close-button-container">
<Button theme="secondary" onClick={this.sidebarClose}>
Close Sidebar
</Button>
</div>
</div>
</If>
<If condition={sidebarOk}>
<SidebarLineContainer key={lineId} screen={screen} winSize={sidebarSize} lineId={lineId} />
</If>
</div>
);
}
@@ -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<any>;
width: mobx.IObservableValue<number> = 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 (
<div className="window-view" ref={this.windowViewRef} data-screenid={screen.screenId}>
<div
className="window-view"
ref={this.windowViewRef}
data-screenid={screen.screenId}
style={{ width: this.props.width }}
>
<div key="lines" className="lines"></div>
<div key="window-empty" className={cn("window-empty", { "should-fade": fade })}>
<div className="text-standard">{message}</div>
@@ -404,7 +663,7 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
let lines = this.determineVisibleLines(win);
let renderMode = this.renderMode.get();
return (
<div className="window-view" ref={this.windowViewRef}>
<div className="window-view" ref={this.windowViewRef} style={{ width: this.props.width }}>
<div
key="rendermode-tag"
className={cn("rendermode-tag", { "is-active": isActive })}
+264 -34
View File
@@ -78,7 +78,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
import { MagicLayout } from "../app/magiclayout";
import { modalsRegistry } from "../app/common/modals/modalsRegistry";
import * as constants from "../app/appconst";
import * as appconst from "../app/appconst";
dayjs.extend(customParseFormat);
dayjs.extend(localizedFormat);
@@ -130,6 +130,9 @@ type LineContainerModel = {
setContentHeight: (context: RendererContext, height: number) => 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<number>;
opts: OV<ScreenOptsType>;
viewOpts: OV<T.ScreenViewOptsType>;
name: OV<string>;
archived: OV<boolean>;
curRemote: OV<RemotePtrType>;
@@ -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<boolean> {
mobx.action(() => {
this.alertMessage.set(alertMessage);
GlobalModel.modalsModel.pushModal(constants.ALERT);
GlobalModel.modalsModel.pushModal(appconst.ALERT);
})();
let prtn = new Promise<boolean>((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<string, string> = {
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<CommandRtnType> {
@@ -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<string, string> = { 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 };
+16 -1
View File
@@ -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<V> = mobx.IObservableValue<V>;
@@ -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,
};
@@ -0,0 +1 @@
ALTER TABLE screen DROP COLUMN screenviewopts;
@@ -0,0 +1 @@
ALTER TABLE screen ADD COLUMN screenviewopts json DEFAULT '{}';
+179 -1
View File
@@ -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)
}
+1
View File
@@ -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
}
+13 -5
View File
@@ -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 {
+3 -3
View File
@@ -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 {
+30 -15
View File
@@ -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")
+24 -1
View File
@@ -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},
},