allow resizing of left sidebar (#244)

* wip

* integrate original sidebar content

* ResizableSidebar component

* trigger toggleCollapse

* remove debugging code

* minor refactor. disable text select on mousemove

* replace icons with fontawesome icons. fix alignment issues

* fix session view width when tabs overflow

* prevent index and icon from shifting when resizing

* snap effect

* minor refactor

* apply collapsed mode to sidebar contents

* change default width to 240px

* backend implementation

* fix wrong subcmd

* save collapsed state

* retore sidebar state on reload/launch

* use collapse data form db on first load. use previously saved width on expand.

* persist width as well collapse state

* various fixes and improvements

* bind methods

* refactor

* more refactor

* fix minor bug

* fix merge issues

* various fixes

* refactor

* fixes

* fix issues

* fix all issues

* resolve undefind tempWidth

* fix toggleCollapsed

* use Promise in stopResizing method

* use tempCollapsed to for real time toggling between logos

* minor method name change

* refactor

* remove debugging code

* fix conflict

* fix setting collapsed state via CLI

* minor refactor

* remove debugging code

* create setTempWidthAndTempCollapsed method

* handle invalid width set via cli

* refactor: setbycli not actually needed

* remove unused code
This commit is contained in:
Red J Adaya 2024-01-31 12:17:49 +08:00 committed by GitHub
parent 40757fa7f4
commit 37ab1bca90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 471 additions and 129 deletions

View File

@ -30,6 +30,7 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer @mobxReact.observer
class App extends React.Component<{}, {}> { class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" }); dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> {
let hasClientStop = GlobalModel.getHasClientStop(); let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get(); let dcWait = this.dcWait.get();
let platform = GlobalModel.getPlatform(); let platform = GlobalModel.getPlatform();
let clientData = GlobalModel.clientData.get();
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
if (clientData == null) {
return null;
}
if (disconnected || hasClientStop) { if (disconnected || hasClientStop) {
if (!dcWait) { if (!dcWait) {
@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> {
} }
return ( return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}> <div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content"> <div ref={this.mainContentRef} className="main-content">
<MainSideBar /> <MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="session-view" /> <div className="session-view" />
</div> </div>
<If condition={dcWait}> <If condition={dcWait}>
@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> {
} }
return ( return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}> <div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content"> <div ref={this.mainContentRef} className="main-content">
<MainSideBar /> <MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary> <ErrorBoundary>
<PluginsView /> <PluginsView />
<WorkspaceView /> <WorkspaceView />

View File

@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import cn from "classnames"; import cn from "classnames";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import { RemoteType, StatusIndicatorLevel } from "../../types/types"; import { RemoteType } from "../../types/types";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { GlobalModel } from "../../model/model"; import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import * as appconst from "../appconst"; import * as appconst from "../appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil"; import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
import { MagicLayout } from "../magiclayout";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg"; import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg"; import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
@ -1265,6 +1266,169 @@ In order to use Wave's advanced features like unified history and persistent ses
}); });
} }
interface ResizableSidebarProps {
parentRef: React.RefObject<HTMLElement>;
position: "left" | "right";
enableSnap?: boolean;
className?: string;
children?: (toggleCollapsed: () => void) => React.ReactNode;
toggleCollapse?: () => void;
}
@mobxReact.observer
class ResizableSidebar extends React.Component<ResizableSidebarProps> {
resizeStartWidth: number = 0;
startX: number = 0;
prevDelta: number = 0;
prevDragDirection: string = null;
disposeReaction: any;
@boundMethod
startResizing(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
let { parentRef, position } = this.props;
let parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
if (position === "right") {
this.startX = parentRect.right - event.clientX;
} else {
this.startX = event.clientX - parentRect.left;
}
this.resizeStartWidth = GlobalModel.mainSidebarModel.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
GlobalModel.mainSidebarModel.isDragging.set(true);
})();
}
@boundMethod
onMouseMove(event: MouseEvent) {
event.preventDefault();
let { parentRef, enableSnap, position } = this.props;
let parentRect = parentRef.current?.getBoundingClientRect();
let mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
let delta, newWidth;
if (position === "right") {
delta = parentRect.right - event.clientX - this.startX;
} else {
delta = event.clientX - parentRect.left - this.startX;
}
newWidth = this.resizeStartWidth + delta;
if (enableSnap) {
let minWidth = MagicLayout.MainSidebarMinWidth;
let snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
let dragResistance = MagicLayout.MainSidebarDragResistance;
let dragDirection;
if (delta - this.prevDelta > 0) {
dragDirection = "+";
} else if (delta - this.prevDelta == 0) {
if (this.prevDragDirection == "+") {
dragDirection = "+";
} else {
dragDirection = "-";
}
} else {
dragDirection = "-";
}
this.prevDelta = delta;
this.prevDragDirection = dragDirection;
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
newWidth = snapPoint;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
newWidth = minWidth;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else if (newWidth > snapPoint) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
} else {
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
}
}
@boundMethod
stopResizing() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
GlobalCommandRunner.clientSetSidebar(
mainSidebarModel.tempWidth.get(),
mainSidebarModel.tempCollapsed.get()
).finally(() => {
mobx.action(() => {
mainSidebarModel.isDragging.set(false);
})();
});
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "";
}
@boundMethod
toggleCollapsed() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
let tempCollapsed = mainSidebarModel.getCollapsed();
let width = MagicLayout.MainSidebarDefaultWidth;
let newWidth;
if (tempCollapsed) {
newWidth = width;
} else {
newWidth = MagicLayout.MainSidebarMinWidth;
}
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(newWidth, !tempCollapsed);
}
render() {
let { className, children } = this.props;
let mainSidebarModel = GlobalModel.mainSidebarModel;
let width = mainSidebarModel.getWidth();
let isCollapsed = mainSidebarModel.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width }}>
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
<div
className="sidebar-handle"
style={{
position: "absolute",
top: 0,
[this.props.position === "left" ? "right" : "left"]: 0,
bottom: 0,
width: "5px",
cursor: "col-resize",
}}
onMouseDown={this.startResizing}
onDoubleClick={this.toggleCollapsed}
></div>
</div>
);
}
}
export { export {
CmdStrCode, CmdStrCode,
Toggle, Toggle,
@ -1286,5 +1450,6 @@ export {
LinkButton, LinkButton,
Status, Status,
Modal, Modal,
ResizableSidebar,
ShowWaveShellInstallPrompt, ShowWaveShellInstallPrompt,
}; };

View File

@ -27,6 +27,12 @@ let MagicLayout = {
ScreenSidebarWidthPadding: 5, ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200, ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28, ScreenSidebarHeaderHeight: 28,
MainSidebarMinWidth: 75,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
}; };
let m = MagicLayout; let m = MagicLayout;

View File

@ -3,14 +3,13 @@
.main-sidebar { .main-sidebar {
padding: 0; padding: 0;
min-width: 20rem;
max-width: 20rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
font-size: 12.5px; font-size: 12.5px;
line-height: 20px; line-height: 20px;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
z-index: 20;
.title-bar-drag { .title-bar-drag {
-webkit-app-region: drag; -webkit-app-region: drag;
@ -24,7 +23,6 @@
&.collapsed { &.collapsed {
width: 6em; width: 6em;
min-width: 6em; min-width: 6em;
.arrow-container, .arrow-container,
.collapse-button { .collapse-button {
transform: rotate(180deg); transform: rotate(180deg);
@ -34,7 +32,7 @@
margin-top: 26px; margin-top: 26px;
.top, .top,
.workspaces-item, .workspaces,
.middle, .middle,
.bottom, .bottom,
.separator { .separator {
@ -50,7 +48,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.logo-container img { .logo-container {
width: 45px; width: 45px;
} }
@ -86,10 +84,14 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
.logo-container {
flex-shrink: 0;
width: 100px;
}
.spacer { .spacer {
flex-grow: 1; flex-grow: 1;
} }
img { img {
width: 100px; width: 100px;
} }
@ -150,7 +152,6 @@
margin-left: 6px; margin-left: 6px;
border-radius: 4px; border-radius: 4px;
opacity: 1; opacity: 1;
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
width: inherit; width: inherit;
max-width: inherit; max-width: inherit;
min-width: inherit; min-width: inherit;
@ -177,6 +178,7 @@
float: right; float: right;
margin-right: 6px; margin-right: 6px;
letter-spacing: 6px; letter-spacing: 6px;
margin-left: auto;
} }
&:hover { &:hover {
:not(.disabled) .hotkey { :not(.disabled) .hotkey {

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RemoteType } from "../../types/types"; import type { ClientDataType, RemoteType } from "../../types/types";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import { compareLoose } from "semver"; import { compareLoose } from "semver";
@ -19,6 +19,7 @@ import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model"; import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
import { isBlank, openLink } from "../../util/util"; import { isBlank, openLink } from "../../util/util";
import { ResizableSidebar } from "../common/common";
import * as constants from "../appconst"; import * as constants from "../appconst";
import "./sidebar.less"; import "./sidebar.less";
@ -26,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
class SideBarItem extends React.Component<{ class SideBarItem extends React.Component<{
frontIcon: React.ReactNode; frontIcon: React.ReactNode;
contents: React.ReactNode | string; contents: React.ReactNode | string;
@ -59,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
} }
} }
@mobxReact.observer interface MainSideBarProps {
class MainSideBar extends React.Component<{}, {}> { parentRef: React.RefObject<HTMLElement>;
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false); clientData: ClientDataType;
}
@boundMethod @mobxReact.observer
toggleCollapsed() { class MainSideBar extends React.Component<MainSideBarProps, {}> {
mobx.action(() => { sidebarRef = React.createRef<HTMLDivElement>();
this.collapsed.set(!this.collapsed.get());
})();
}
handleSessionClick(sessionId: string) { handleSessionClick(sessionId: string) {
GlobalCommandRunner.switchSession(sessionId); GlobalCommandRunner.switchSession(sessionId);
@ -208,106 +205,116 @@ class MainSideBar extends React.Component<{}, {}> {
} }
render() { render() {
const isCollapsed = this.collapsed.get(); let clientData = this.props.clientData;
const clientData = GlobalModel.clientData.get();
let needsUpdate = false; let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) { if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0; needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
} }
let mainSidebar = GlobalModel.mainSidebarModel;
let isCollapsed = mainSidebar.getCollapsed();
return ( return (
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}> <ResizableSidebar
<div className="title-bar-drag" /> className="main-sidebar"
<div className="contents"> position="left"
<div className="logo"> enableSnap={true}
<If condition={isCollapsed}> parentRef={this.props.parentRef}
<div className="logo-container" onClick={this.toggleCollapsed}> >
<img src="public/logos/wave-logo.png" /> {(toggleCollapse) => (
<React.Fragment>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={toggleCollapse}>
<img src="public/logos/wave-logo.png" />
</div>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
</div>
<div className="spacer" />
<div className="collapse-button" onClick={toggleCollapse}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div> </div>
</If> <div className="separator" />
<If condition={!isCollapsed}> <div className="top">
<div className="logo-container"> <SideBarItem
<img src="public/logos/wave-dark.png" /> key="history"
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
key="connections"
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div> </div>
<div className="spacer" /> <div className="separator" />
<div className="collapse-button" onClick={this.toggleCollapsed}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div>
<div className="separator" />
<div className="top">
<SideBarItem
key="history"
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
key="connections"
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div>
<div className="separator" />
<SideBarItem
key="workspaces"
className="workspaces"
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<CenteredIcon
key="add-workspace"
className="add-workspace hoverEffect"
onClick={this.handleNewSession}
>
<i className="fa-sharp fa-solid fa-plus"></i>
</CenteredIcon>,
]}
/>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<SideBarItem <SideBarItem
key="update-available" key="workspaces"
className="updateBanner" className="workspaces"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />} frontIcon={<WorkspacesIcon className="icon" />}
contents="Update Available" contents="Workspaces"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")} endIcons={[
<CenteredIcon
key="add-workspace"
className="add-workspace hoverEffect"
onClick={this.handleNewSession}
>
<i className="fa-sharp fa-solid fa-plus"></i>
</CenteredIcon>,
]}
/> />
</If> <div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<If condition={GlobalModel.isDev}> <div className="bottom">
<SideBarItem <If condition={needsUpdate}>
key="apps" <SideBarItem
frontIcon={<AppsIcon className="icon" />} key="update-available"
contents="Apps" className="updateBanner"
onClick={this.handlePluginsClick} frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]} contents="Update Available"
/> onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
</If> />
<SideBarItem </If>
key="settings" <If condition={GlobalModel.isDev}>
frontIcon={<SettingsIcon className="icon" />} <SideBarItem
contents="Settings" key="apps"
onClick={this.handleSettingsClick} frontIcon={<AppsIcon className="icon" />}
/> contents="Apps"
<SideBarItem onClick={this.handlePluginsClick}
key="documentation" endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />} />
contents="Documentation" </If>
onClick={() => openLink("https://docs.waveterm.dev")} <SideBarItem
/> key="settings"
<SideBarItem frontIcon={<SettingsIcon className="icon" />}
key="discord" contents="Settings"
frontIcon={<i className="fa-brands fa-discord icon" />} onClick={this.handleSettingsClick}
contents="Discord" />
onClick={() => openLink("https://discord.gg/XfvZ334gwU")} <SideBarItem
/> key="documentation"
</div> frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
</div> contents="Documentation"
</div> onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
key="discord"
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</React.Fragment>
)}
</ResizableSidebar>
); );
} }
} }

View File

@ -8,12 +8,12 @@
&.is-hidden { &.is-hidden {
display: none; display: none;
} }
max-width: calc(100% - 20.5em);
background: @background-session; background: @background-session;
border: 1px solid @base-border; border: 1px solid @base-border;
border-radius: 8px; border-radius: 8px;
transition: width 0.2s ease; // transition: width 0.2s ease;
margin-bottom: 0.5em; margin-bottom: 0.5em;
margin-right: 0.5em;
.center-message { .center-message {
display: flex; display: flex;
@ -24,6 +24,3 @@
color: @text-secondary; color: @text-secondary;
} }
} }
.collapsed + .session-view {
max-width: calc(100% - 6.7em);
}

View File

@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> {
if (session == null) { if (session == null) {
return ( return (
<div className="session-view"> <div className="session-view">
<div className="center-message"><div>(no active workspace)</div></div> <div className="center-message">
<div>(no active workspace)</div>
</div>
</div> </div>
); );
} }
let activeScreen = session.getActiveScreen(); let activeScreen = session.getActiveScreen();
let cmdInputHeight = model.inputModel.cmdInputHeight.get(); let cmdInputHeight = model.inputModel.cmdInputHeight.get();
if (cmdInputHeight == 0) { if (cmdInputHeight == 0) {
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools) cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools)
} }
cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px
let isHidden = GlobalModel.activeMainView.get() != "session"; let isHidden = GlobalModel.activeMainView.get() != "session";
let mainSidebarModel = GlobalModel.mainSidebarModel;
// Has to calc manually because when tabs overflow, the width of the session view is increased for some reason causing inconsistent width.
// 6px is the right margin of session view.
let width = window.innerWidth - 6 - mainSidebarModel.getWidth();
return ( return (
<div className={cn("session-view", { "is-hidden": isHidden })} data-sessionid={session.sessionId}> <div
className={cn("session-view", { "is-hidden": isHidden })}
data-sessionid={session.sessionId}
style={{
width: `${width}px`,
}}
>
<ScreenTabs key={"tabs-" + session.sessionId} session={session} /> <ScreenTabs key={"tabs-" + session.sessionId} session={session} />
<ErrorBoundary> <ErrorBoundary>
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} /> <ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />

View File

@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import * as mobxReact from "mobx-react";
import { import {
handleJsonFetchResponse, handleJsonFetchResponse,
base64ToString, base64ToString,
@ -2617,6 +2618,74 @@ class ClientSettingsViewModel {
})(); })();
} }
} }
class MainSidebarModel {
tempWidth: OV<number> = mobx.observable.box(null, {
name: "MainSidebarModel-tempWidth",
});
tempCollapsed: OV<boolean> = mobx.observable.box(null, {
name: "MainSidebarModel-tempCollapsed",
});
isDragging: OV<boolean> = mobx.observable.box(false, {
name: "MainSidebarModel-isDragging",
});
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void {
let width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth));
mobx.action(() => {
this.tempWidth.set(width);
this.tempCollapsed.set(newCollapsed);
})();
}
getWidth(): number {
let clientData = GlobalModel.clientData.get();
let width = clientData.clientopts.mainsidebar.width;
if (this.isDragging.get()) {
if (this.tempWidth.get() == null && width == null) {
return MagicLayout.MainSidebarDefaultWidth;
}
if (this.tempWidth.get() == null) {
return width;
}
return this.tempWidth.get();
}
// Set by CLI and collapsed
if (this.getCollapsed() && width != MagicLayout.MainSidebarMinWidth) {
this.setTempWidthAndTempCollapsed(MagicLayout.MainSidebarMinWidth, true);
return MagicLayout.MainSidebarMinWidth;
}
// Set by CLI and not collapsed
if (!this.getCollapsed()) {
if (width <= MagicLayout.MainSidebarMinWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
let snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold;
if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
this.setTempWidthAndTempCollapsed(width, false);
return width;
}
this.setTempWidthAndTempCollapsed(width, this.getCollapsed());
return width;
}
getCollapsed(): boolean {
let clientData = GlobalModel.clientData.get();
let collapsed = clientData.clientopts.mainsidebar.collapsed;
if (this.isDragging.get()) {
if (this.tempCollapsed.get() == null && collapsed == null) {
return false;
}
if (this.tempCollapsed.get() == null) {
return collapsed;
}
return this.tempCollapsed.get();
}
return collapsed;
}
}
class BookmarksModel { class BookmarksModel {
bookmarks: OArr<BookmarkType> = mobx.observable.array([], { bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
@ -3387,6 +3456,7 @@ class Model {
connectionViewModel: ConnectionsViewModel; connectionViewModel: ConnectionsViewModel;
clientSettingsViewModel: ClientSettingsViewModel; clientSettingsViewModel: ClientSettingsViewModel;
modalsModel: ModalsModel; modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, { clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData", name: "clientData",
}); });
@ -3413,6 +3483,7 @@ class Model {
this.remotesModalModel = new RemotesModalModel(); this.remotesModalModel = new RemotesModalModel();
this.remotesModel = new RemotesModel(); this.remotesModel = new RemotesModel();
this.modalsModel = new ModalsModel(); this.modalsModel = new ModalsModel();
this.mainSidebarModel = new MainSidebarModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus(); let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running", name: "model-wavesrv-running",
@ -4980,6 +5051,11 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false); return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
} }
clientSetSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false);
}
editBookmark(bookmarkId: string, desc: string, cmdstr: string) { editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
let kwargs = { let kwargs = {
nohist: "1", nohist: "1",

View File

@ -524,6 +524,10 @@ type ClientOptsType = {
noreleasecheck: boolean; noreleasecheck: boolean;
acceptedtos: number; acceptedtos: number;
confirmflags: ConfirmFlagsType; confirmflags: ConfirmFlagsType;
mainsidebar: {
collapsed: boolean;
width: number;
};
}; };
type ReleaseInfoType = { type ReleaseInfoType = {

View File

@ -92,6 +92,7 @@ var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass"
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
var ConfirmFlags = []string{"hideshellprompt"} var ConfirmFlags = []string{"hideshellprompt"}
var SidebarNames = []string{"main"}
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"} var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
@ -218,6 +219,7 @@ func init() {
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand) registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
registerCmdFn("client:accepttos", ClientAcceptTosCommand) registerCmdFn("client:accepttos", ClientAcceptTosCommand)
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand) registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
registerCmdFn("sidebar:open", SidebarOpenCommand) registerCmdFn("sidebar:open", SidebarOpenCommand)
registerCmdFn("sidebar:close", SidebarCloseCommand) registerCmdFn("sidebar:close", SidebarCloseCommand)
@ -4463,6 +4465,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
return update, nil return update, nil
} }
func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
}
// Handle collapsed
collapsed, ok := pk.Kwargs["collapsed"]
if !ok {
return nil, fmt.Errorf("collapsed key not provided")
}
collapsedValue := resolveBool(collapsed, false)
// Handle width
var width int
if w, exists := pk.Kwargs["width"]; exists {
width, err = resolveNonNegInt(w, 0)
if err != nil {
return nil, fmt.Errorf("error resolving width: %v", err)
}
} else if clientData.ClientOpts.MainSidebar != nil {
width = clientData.ClientOpts.MainSidebar.Width
}
// Initialize SidebarCollapsed if it's nil
if clientData.ClientOpts.MainSidebar == nil {
clientData.ClientOpts.MainSidebar = new(sstore.SidebarValueType)
}
// Set the sidebar values
var sv sstore.SidebarValueType
sv.Collapsed = collapsedValue
if width != 0 {
sv.Width = width
}
clientData.ClientOpts.MainSidebar = &sv
// Update client data
err = sstore.SetClientOpts(ctx, clientData.ClientOpts)
if err != nil {
return nil, fmt.Errorf("error updating client data: %v", err)
}
// Retrieve updated client data
clientData, err = sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
}
update := &sstore.ModelUpdate{
ClientData: clientData,
}
return update, nil
}
func validateOpenAIAPIToken(key string) error { func validateOpenAIAPIToken(key string) error {
if len(key) > MaxOpenAIAPITokenLen { if len(key) > MaxOpenAIAPITokenLen {
return fmt.Errorf("invalid openai token, too long") return fmt.Errorf("invalid openai token, too long")

View File

@ -275,11 +275,17 @@ func (tdata *TelemetryData) Scan(val interface{}) error {
return quickScanJson(tdata, val) return quickScanJson(tdata, val)
} }
type SidebarValueType struct {
Collapsed bool `json:"collapsed"`
Width int `json:"width"`
}
type ClientOptsType struct { type ClientOptsType struct {
NoTelemetry bool `json:"notelemetry,omitempty"` NoTelemetry bool `json:"notelemetry,omitempty"`
NoReleaseCheck bool `json:"noreleasecheck,omitempty"` NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
AcceptedTos int64 `json:"acceptedtos,omitempty"` AcceptedTos int64 `json:"acceptedtos,omitempty"`
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"` ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
} }
type FeOptsType struct { type FeOptsType struct {