Compare commits

...

13 Commits

Author SHA1 Message Date
Evan Simkowitz 5ce7b92232 Simplify logic for startResizing width calculation (#269)
* Apply Red's suggestion and move width logic to getWidth

* remove unnecessary

* a little more

* remove comment
2024-01-30 22:34:00 -08:00
Evan Simkowitz 81a9e74991 Fix onMouseDown causing width to snap back to persisted width (#268)
* Fix onMouseDown causing width to snap back to persisted width

* var name change

* remove logs

* remove redundant call
2024-01-30 22:23:00 -08:00
Evan Simkowitz 679e85a84a Fix sidebar width not persisting when un-collapsing (#267)
* Do not persist collapsed width to db

* remove console.logs

* one more

* one more

* remove setTemp from getWidth and add to the startResize call
2024-01-30 22:04:25 -08:00
Evan Simkowitz da69a0f583 Fix broken merge (#266) 2024-01-30 20:56:32 -08:00
Red J Adaya 37ab1bca90 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
2024-01-30 20:17:49 -08:00
Evan Simkowitz 40757fa7f4 Prevent the status indicator flickering for quick-returning commands (#265)
* add status-indicator-visible

* save

* save

* Prevent the status indicator flickering for quick returns

* flx regression

* reduce delay, reset spinnerVisible when there's no more running commands

* clean up code reuse

* move code around

* slight optimizations to prevent rendering before spinner is visible

* rename var

* revert shouldSync change as it broke the sync
2024-01-30 16:31:38 -08:00
Evan Simkowitz e576f7f07d Sync RotateIcon spin as well (#262)
* Sync RotateIcon spin as well

* make shouldSync a boolean value
2024-01-30 14:16:33 -08:00
Evan Simkowitz 9e804232c2 Fix SidebarItem key error (#263) 2024-01-30 11:54:35 -08:00
sawka d3dbac6e77 fix missing react key 2024-01-30 11:29:12 -08:00
Mike Sawka 198de02a65 code to restore indicator state on refresh (#260)
* code to restore indicator state on refresh

* fix style
2024-01-29 23:51:01 -08:00
Mike Sawka 8f64d6a589 set animation startTime to sync spinners for status indicators (#259)
* set animation startTime to sync spinners for status indicators

* catch animationstart (hover interaction) to restart startTime.

* use const

---------

Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
2024-01-29 23:48:37 -08:00
Evan Simkowitz 1a51d93a54 Fix add-workspace so that only the plus button shows pointer (#258) 2024-01-29 14:10:58 -08:00
sawka 9ab3b2a5b7 only run prettier on js/ts/css/less files 2024-01-29 13:35:09 -08:00
20 changed files with 845 additions and 281 deletions
+19 -2
View File
@@ -1,4 +1,21 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+12 -4
View File
@@ -30,6 +30,7 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: any) {
super(props);
@@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> {
let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get();
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 (!dcWait) {
@@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="session-view" />
</div>
<If condition={dcWait}>
@@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary>
<PluginsView />
<WorkspaceView />
+164 -2
View File
@@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { RemoteType, StatusIndicatorLevel } from "../../types/types";
import { RemoteType } from "../../types/types";
import ReactDOM from "react-dom";
import { GlobalModel } from "../../model/model";
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import * as appconst from "../appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
import { MagicLayout } from "../magiclayout";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
@@ -1265,6 +1266,166 @@ 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();
const { parentRef, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
if (position === "right") {
this.startX = parentRect.right - event.clientX;
} else {
this.startX = event.clientX - parentRect.left;
}
const mainSidebarModel = GlobalModel.mainSidebarModel;
const collapsed = mainSidebarModel.getCollapsed();
this.resizeStartWidth = mainSidebarModel.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
mainSidebarModel.isDragging.set(true);
})();
}
@boundMethod
onMouseMove(event: MouseEvent) {
event.preventDefault();
const { parentRef, enableSnap, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
const mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
let delta: number, newWidth: number;
if (position === "right") {
delta = parentRect.right - event.clientX - this.startX;
} else {
delta = event.clientX - parentRect.left - this.startX;
}
newWidth = this.resizeStartWidth + delta;
if (enableSnap) {
const minWidth = MagicLayout.MainSidebarMinWidth;
const snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
const dragResistance = MagicLayout.MainSidebarDragResistance;
let dragDirection: string;
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() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const tempCollapsed = mainSidebarModel.getCollapsed();
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
}
render() {
const { className, children } = this.props;
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth();
const 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 {
CmdStrCode,
Toggle,
@@ -1286,5 +1447,6 @@ export {
LinkButton,
Status,
Modal,
ResizableSidebar,
ShowWaveShellInstallPrompt,
};
+21 -1
View File
@@ -48,13 +48,33 @@
}
}
/*
The following accounts for a debounce in the status indicator. We will only display the status indicator icon if the parent indicates that it should be visible AND one of the following is true:
1. There is a status to be shown.
2. There is a spinner to be shown and the required delay has passed.
*/
.status-indicator-visible {
&.spinner-visible,
&.output,
&.error,
&.success {
.positional-icon-visible;
}
// This is set by the timeout in the status indicator component.
&.spinner-visible {
#spinner {
visibility: visible;
}
}
}
.status-indicator {
#spinner,
#indicator {
visibility: hidden;
}
.spin #spinner {
visibility: visible;
stroke: @term-white;
}
&.error #indicator {
+171 -10
View File
@@ -2,17 +2,27 @@ import React from "react";
import { StatusIndicatorLevel } from "../../../types/types";
import cn from "classnames";
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
import { boundMethod } from "autobind-decorator";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg";
interface PositionalIconProps {
children?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement>;
}
export class FrontIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className={cn("front-icon", "positional-icon", this.props.className)}>
<div
ref={this.props.divRef}
className={cn("front-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
@@ -22,7 +32,11 @@ export class FrontIcon extends React.Component<PositionalIconProps> {
export class CenteredIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className={cn("centered-icon", "positional-icon", this.props.className)} onClick={this.props.onClick}>
<div
ref={this.props.divRef}
className={cn("centered-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
@@ -43,35 +57,182 @@ export class ActionsIcon extends React.Component<ActionsIconProps> {
}
}
class SyncSpin extends React.Component<{
classRef?: React.RefObject<HTMLDivElement>;
children?: React.ReactNode;
shouldSync?: boolean;
}> {
listenerAdded: boolean = false;
componentDidMount() {
this.syncSpinner();
}
componentDidUpdate() {
this.syncSpinner();
}
componentWillUnmount(): void {
const classRef = this.props.classRef;
if (classRef.current != null && this.listenerAdded) {
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem != null) {
svgElem.removeEventListener("animationstart", this.handleAnimationStart);
}
}
}
@boundMethod
handleAnimationStart(e: AnimationEvent) {
const classRef = this.props.classRef;
if (classRef.current == null) {
return;
}
const svgElem = classRef.current.querySelector("svg");
if (svgElem == null) {
return;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
syncSpinner() {
const { classRef, shouldSync } = this.props;
const shouldSyncVal = shouldSync ?? true;
if (!shouldSyncVal || classRef.current == null) {
return;
}
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem == null) {
return;
}
if (!this.listenerAdded) {
svgElem.addEventListener("animationstart", this.handleAnimationStart);
this.listenerAdded = true;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
render() {
return this.props.children;
}
}
interface StatusIndicatorProps {
/**
* The level of the status indicator. This will determine the color of the status indicator.
*/
level: StatusIndicatorLevel;
className?: string;
/**
* If true, a spinner will be shown around the status indicator.
*/
runningCommands?: boolean;
}
/**
* This component is used to show the status of a command. It will show a spinner around the status indicator if there are running commands. It will also delay showing the spinner for a short time to prevent flickering.
*/
@mobxReact.observer
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
timeout: NodeJS.Timeout;
clearSpinnerTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
mobx.action(() => {
this.spinnerVisible.set(false);
})();
}
/**
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
*/
updateMountCallback() {
const runningCommands = this.props.runningCommands ?? false;
if (runningCommands && !this.timeout) {
this.timeout = setTimeout(
mobx.action(() => {
this.spinnerVisible.set(true);
}),
100
);
} else if (!runningCommands) {
this.clearSpinnerTimeout();
}
}
componentDidUpdate(): void {
this.updateMountCallback();
}
componentDidMount(): void {
this.updateMountCallback();
}
componentWillUnmount(): void {
this.clearSpinnerTimeout();
}
render() {
const { level, className, runningCommands } = this.props;
const spinnerVisible = this.spinnerVisible.get();
let statusIndicator = null;
if (level != StatusIndicatorLevel.None || runningCommands) {
let levelClass = null;
if (level != StatusIndicatorLevel.None || spinnerVisible) {
let indicatorLevelClass = null;
switch (level) {
case StatusIndicatorLevel.Output:
levelClass = "output";
indicatorLevelClass = "output";
break;
case StatusIndicatorLevel.Success:
levelClass = "success";
indicatorLevelClass = "success";
break;
case StatusIndicatorLevel.Error:
levelClass = "error";
indicatorLevelClass = "error";
break;
}
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = (
<CenteredIcon className={cn(className, levelClass, "status-indicator")}>
<SpinnerIndicator className={runningCommands ? "spin" : null} />
<CenteredIcon
divRef={this.iconRef}
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
</CenteredIcon>
);
}
return statusIndicator;
return (
<SyncSpin classRef={this.iconRef} shouldSync={runningCommands}>
{statusIndicator}
</SyncSpin>
);
}
}
export class RotateIcon extends React.Component<{
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
render() {
return (
<SyncSpin classRef={this.iconRef}>
<RotateIconSvg className={this.props.className ?? ""} />
</SyncSpin>
);
}
}
+81 -89
View File
@@ -13,8 +13,6 @@ import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../mod
import { termHeightFromRows } from "../../util/textmeasure";
import type {
LineType,
RemoteType,
RemotePtrType,
RenderModeType,
RendererOpts,
RendererPluginType,
@@ -24,11 +22,6 @@ import type {
} from "../../types/types";
import cn from "classnames";
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
import { ReactComponent as PinIcon } from "../assets/icons/pin.svg";
import { ReactComponent as PlusIcon } from "../assets/icons/plus.svg";
import { ReactComponent as MinusIcon } from "../assets/icons/minus.svg";
import type { LineContainerModel } from "../../model/model";
import { renderCmdText } from "../common/common";
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
@@ -44,12 +37,13 @@ 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";
import { ReactComponent as QuestionIcon } from "../assets/icons/line/question.svg";
import { ReactComponent as RotateIcon } from "../assets/icons/line/rotate.svg";
import { ReactComponent as WarningIcon } from "../assets/icons/line/triangle-exclamation.svg";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
import { ReactComponent as FillIcon } from "../assets/icons/line/fill.svg";
import { ReactComponent as GearIcon } from "../assets/icons/line/gear.svg";
import { RotateIcon } from "../common/icons/icons";
import "./lines.less";
dayjs.extend(localizedFormat);
@@ -59,12 +53,12 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> {
render() {
let { line, cmd } = this.props;
let lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
let status = cmd != null ? cmd.getStatus() : "done";
let rtnstate = cmd != null ? cmd.getRtnState() : false;
let exitcode = cmd != null ? cmd.getExitCode() : 0;
let isComment = line.linetype == "text";
const { line, cmd } = this.props;
const lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
const status = cmd != null ? cmd.getStatus() : "done";
const rtnstate = cmd != null ? cmd.getRtnState() : false;
const exitcode = cmd != null ? cmd.getExitCode() : 0;
const isComment = line.linetype == "text";
let icon = null;
let iconTitle = null;
if (isComment) {
@@ -142,7 +136,7 @@ class LineCmd extends React.Component<
}
checkStateDiffLoad(): void {
let { screen, line, staticRender, visible } = this.props;
const { screen, line, staticRender, visible } = this.props;
if (staticRender) {
return;
}
@@ -153,7 +147,7 @@ class LineCmd extends React.Component<
}
return;
}
let cmd = screen.getCmd(line);
const cmd = screen.getCmd(line);
if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) {
return;
}
@@ -167,15 +161,15 @@ class LineCmd extends React.Component<
if (this.rtnStateDiffFetched) {
return;
}
let { line } = this.props;
const { line } = this.props;
this.rtnStateDiffFetched = true;
let usp = new URLSearchParams({
const usp = new URLSearchParams({
linenum: String(line.linenum),
screenid: line.screenid,
lineid: line.lineid,
});
let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
let fetchHeaders = GlobalModel.getFetchHeaders();
const url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
const fetchHeaders = GlobalModel.getFetchHeaders();
fetch(url, { headers: fetchHeaders })
.then((resp) => {
if (!resp.ok) {
@@ -233,7 +227,7 @@ class LineCmd extends React.Component<
</React.Fragment>
);
}
let isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
@@ -252,7 +246,7 @@ class LineCmd extends React.Component<
// TODO: this might not be necessary anymore because we're using this.lastHeight
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
let elem = this.lineRef.current;
const elem = this.lineRef.current;
if (elem == null) {
return { height: 0 };
}
@@ -266,25 +260,25 @@ class LineCmd extends React.Component<
}
checkCmdText() {
let metaElem = this.cmdTextRef.current;
const metaElem = this.cmdTextRef.current;
if (metaElem == null || metaElem.childNodes.length == 0) {
return;
}
let metaElemWidth = metaElem.offsetWidth;
const metaElemWidth = metaElem.offsetWidth;
if (metaElemWidth == 0) {
return;
}
let metaChild = metaElem.firstChild;
const metaChild = metaElem.firstChild;
if (metaChild == null) {
return;
}
let children = metaChild.childNodes;
const children = metaChild.childNodes;
let childWidth = 0;
for (let i = 0; i < children.length; i++) {
let ch = children[i];
childWidth += ch.offsetWidth;
}
let isOverflow = childWidth > metaElemWidth;
const isOverflow = childWidth > metaElemWidth;
if (isOverflow && isOverflow != this.isOverflow.get()) {
mobx.action(() => {
this.isOverflow.set(isOverflow);
@@ -297,16 +291,16 @@ class LineCmd extends React.Component<
if (this.props.onHeightChange == null) {
return;
}
let { line } = this.props;
const { line } = this.props;
let curHeight = 0;
let elem = this.lineRef.current;
const elem = this.lineRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
}
if (this.lastHeight == curHeight) {
return;
}
let lastHeight = this.lastHeight;
const lastHeight = this.lastHeight;
this.lastHeight = curHeight;
this.props.onHeightChange(line.linenum, curHeight, lastHeight);
// console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight);
@@ -314,13 +308,13 @@ class LineCmd extends React.Component<
@boundMethod
handleClick() {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
let sel = window.getSelection();
const sel = window.getSelection();
if (this.lineRef.current != null) {
let selText = sel.toString();
const selText = sel.toString();
if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) {
return;
}
@@ -330,7 +324,7 @@ class LineCmd extends React.Component<
@boundMethod
clickStar() {
let { line } = this.props;
const { line } = this.props;
if (!line.star || line.star == 0) {
GlobalCommandRunner.lineStar(line.lineid, 1);
} else {
@@ -340,7 +334,7 @@ class LineCmd extends React.Component<
@boundMethod
clickPin() {
let { line } = this.props;
const { line } = this.props;
if (!line.pinned) {
GlobalCommandRunner.linePin(line.lineid, true);
} else {
@@ -350,19 +344,19 @@ class LineCmd extends React.Component<
@boundMethod
clickBookmark() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.lineBookmark(line.lineid);
}
@boundMethod
clickDelete() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.lineDelete(line.lineid, true);
}
@boundMethod
clickRestart() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.lineRestart(line.lineid, true);
}
@@ -375,7 +369,7 @@ class LineCmd extends React.Component<
@boundMethod
clickMoveToSidebar() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.screenSidebarAddLine(line.lineid);
}
@@ -390,20 +384,20 @@ class LineCmd extends React.Component<
}
getIsHidePrompt(): boolean {
let { line } = this.props;
const { line } = this.props;
let rendererPlugin: RendererPluginType = null;
let isNoneRenderer = line.renderer == "none";
const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
const hidePrompt = rendererPlugin?.hidePrompt;
return hidePrompt;
}
getTerminalRendererHeight(cmd: Cmd): number {
let { screen, line, width, renderMode } = this.props;
const { screen, line, width } = this.props;
let height = 45 + 24; // height of zero height terminal
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
if (usedRows > 0) {
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
}
@@ -412,7 +406,7 @@ class LineCmd extends React.Component<
@boundMethod
onAvatarRightClick(e: any): void {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -426,20 +420,20 @@ class LineCmd extends React.Component<
}
renderSimple() {
let { screen, line } = this.props;
let cmd = screen.getCmd(line);
const { screen, line } = this.props;
const cmd = screen.getCmd(line);
let height: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd);
} else {
// header is 16px tall with hide-prompt, 36px otherwise
let { screen, line, width } = this.props;
let hidePrompt = this.getIsHidePrompt();
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
const { screen, line, width } = this.props;
const hidePrompt = this.getIsHidePrompt();
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows;
}
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let mainDivCn = cn("line", "line-cmd", "line-simple");
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const mainDivCn = cn("line", "line-cmd", "line-simple");
return (
<div
className={mainDivCn}
@@ -479,15 +473,16 @@ class LineCmd extends React.Component<
if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
}
else {
} else {
formattedTime = lineutil.getLineDateTimeStr(line.ts);
}
let renderer = line.renderer;
return (
<div key="meta1" className="meta meta-line1">
<SmallLineAvatar line={line} cmd={cmd} />
<div title={timeTitle} className="ts">{formattedTime}</div>
<div title={timeTitle} className="ts">
{formattedTime}
</div>
<div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer">
@@ -506,7 +501,7 @@ class LineCmd extends React.Component<
}
getRendererOpts(cmd: Cmd): RendererOpts {
let { screen } = this.props;
const { screen } = this.props;
return {
maxSize: screen.getMaxContentSize(),
idealSize: screen.getIdealContentSize(),
@@ -516,9 +511,9 @@ class LineCmd extends React.Component<
}
makeRendererModelInitializeParams(): RendererModelInitializeParams {
let { screen, line } = this.props;
let context = lineutil.getRendererContext(line);
let cmd = screen.getCmd(line); // won't be null
const { screen, line } = this.props;
const context = lineutil.getRendererContext(line);
const cmd = screen.getCmd(line); // won't be null
let savedHeight = screen.getContentHeight(context);
if (savedHeight == null) {
if (line.contentheight != null && line.contentheight != -1) {
@@ -527,7 +522,7 @@ class LineCmd extends React.Component<
savedHeight = 0;
}
}
let api = {
const api = {
saveHeight: (height: number) => {
screen.setContentHeight(lineutil.getRendererContext(line), height);
},
@@ -581,12 +576,12 @@ class LineCmd extends React.Component<
};
render() {
let { screen, line, width, staticRender, visible } = this.props;
let isVisible = visible.get();
const { screen, line, width, staticRender, visible } = this.props;
const isVisible = visible.get();
if (staticRender || !isVisible) {
return this.renderSimple();
}
let cmd = screen.getCmd(line);
const cmd = screen.getCmd(line);
if (cmd == null) {
return (
<div
@@ -600,19 +595,17 @@ class LineCmd extends React.Component<
</div>
);
}
let status = cmd.getStatus();
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
let isSelected = mobx
const isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected",
})
.get();
let isPhysicalFocused = mobx
const isPhysicalFocused = mobx
.computed(() => screen.getIsFocused(line.linenum), {
name: "computed-getIsFocused",
})
.get();
let isFocused = mobx
const isFocused = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@@ -621,7 +614,7 @@ class LineCmd extends React.Component<
{ name: "computed-isFocused" }
)
.get();
let shouldCmdFocus = mobx
const shouldCmdFocus = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@@ -630,7 +623,7 @@ class LineCmd extends React.Component<
{ name: "computed-shouldCmdFocus" }
)
.get();
let isInSidebar = mobx
const isInSidebar = mobx
.computed(
() => {
return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid);
@@ -638,11 +631,10 @@ class LineCmd extends React.Component<
{ name: "computed-isInSidebar" }
)
.get();
let isStatic = staticRender;
let isRunning = cmd.isRunning();
let isExpanded = this.isCmdExpanded.get();
let rsdiff = this.rtnStateDiff.get();
let mainDivCn = cn(
const isRunning = cmd.isRunning();
const isExpanded = this.isCmdExpanded.get();
const rsdiff = this.rtnStateDiff.get();
const mainDivCn = cn(
"line",
"line-cmd",
{ selected: isSelected },
@@ -651,18 +643,18 @@ class LineCmd extends React.Component<
{ "has-rtnstate": cmd.getRtnState() }
);
let rendererPlugin: RendererPluginType = null;
let isNoneRenderer = line.renderer == "none";
const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
let rendererType = lineutil.getRendererType(line);
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
let termFontSize = GlobalModel.termFontSize.get();
const rendererType = lineutil.getRendererType(line);
const hidePrompt = rendererPlugin?.hidePrompt;
const termFontSize = GlobalModel.termFontSize.get();
let rtnStateDiffSize = termFontSize - 2;
if (rtnStateDiffSize < 10) {
rtnStateDiffSize = Math.max(termFontSize, 10);
}
let containerType = screen.getContainerType();
const containerType = screen.getContainerType();
return (
<div
className={mainDivCn}
@@ -681,7 +673,7 @@ class LineCmd extends React.Component<
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate"/>
<i className="fa-sharp fa-regular fa-arrows-rotate" />
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash" />
@@ -821,7 +813,7 @@ class Line extends React.Component<
{}
> {
render() {
let line = this.props.line;
const line = this.props.line;
if (line.archived) {
return null;
}
@@ -847,7 +839,7 @@ class LineText extends React.Component<
> {
@boundMethod
clickHandler() {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -856,7 +848,7 @@ class LineText extends React.Component<
@boundMethod
onAvatarRightClick(e: any): void {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -870,19 +862,19 @@ class LineText extends React.Component<
}
render() {
let { screen, line, renderMode } = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let isSelected = mobx
const { screen, line } = this.props;
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected",
})
.get();
let isFocused = mobx
const isFocused = mobx
.computed(() => screen.getFocusType() == "cmd", {
name: "computed-isFocused",
})
.get();
let mainClass = cn("line", "line-text", "focus-parent");
const mainClass = cn("line", "line-text", "focus-parent");
return (
<div
className={mainClass}
+6
View File
@@ -27,6 +27,12 @@ let MagicLayout = {
ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28,
MainSidebarMinWidth: 75,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
};
let m = MagicLayout;
+23 -18
View File
@@ -3,14 +3,13 @@
.main-sidebar {
padding: 0;
min-width: 20rem;
max-width: 20rem;
display: flex;
flex-direction: column;
position: relative;
font-size: 12.5px;
line-height: 20px;
backdrop-filter: blur(4px);
z-index: 20;
.title-bar-drag {
-webkit-app-region: drag;
@@ -24,7 +23,6 @@
&.collapsed {
width: 6em;
min-width: 6em;
.arrow-container,
.collapse-button {
transform: rotate(180deg);
@@ -34,7 +32,7 @@
margin-top: 26px;
.top,
.workspaces-item,
.workspaces,
.middle,
.bottom,
.separator {
@@ -50,7 +48,7 @@
justify-content: center;
align-items: center;
.logo-container img {
.logo-container {
width: 45px;
}
@@ -86,10 +84,14 @@
display: flex;
flex-direction: row;
.logo-container {
flex-shrink: 0;
width: 100px;
}
.spacer {
flex-grow: 1;
}
img {
width: 100px;
}
@@ -127,9 +129,7 @@
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
.index {
font-size: 0.8em;
text-align: center;
width: 2em;
font-size: 10px;
}
.hotkey {
float: left !important;
@@ -152,7 +152,6 @@
margin-left: 6px;
border-radius: 4px;
opacity: 1;
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
width: inherit;
max-width: inherit;
min-width: inherit;
@@ -179,6 +178,7 @@
float: right;
margin-right: 6px;
letter-spacing: 6px;
margin-left: auto;
}
&:hover {
:not(.disabled) .hotkey {
@@ -190,15 +190,20 @@
}
&:not(:hover) .status-indicator {
.positional-icon-visible;
.status-indicator-visible;
}
.add_workspace {
float: right;
padding: 2px;
margin-right: 6px;
transition: transform 0.3s ease-in-out;
svg {
fill: @base-color;
&.workspaces {
cursor: default;
.add-workspace {
cursor: pointer;
.positional-icon-visible;
float: right;
padding: 2px;
margin-right: 6px;
.fa-plus {
font-size: 13px;
}
}
}
+114 -98
View File
@@ -7,19 +7,19 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
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 { compareLoose } from "semver";
import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
import { isBlank, openLink } from "../../util/util";
import { ResizableSidebar } from "../common/common";
import * as constants from "../appconst";
import "./sidebar.less";
@@ -27,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
class SideBarItem extends React.Component<{
frontIcon: React.ReactNode;
contents: React.ReactNode | string;
@@ -60,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
}
}
@mobxReact.observer
class MainSideBar extends React.Component<{}, {}> {
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
interface MainSideBarProps {
parentRef: React.RefObject<HTMLElement>;
clientData: ClientDataType;
}
@boundMethod
toggleCollapsed() {
mobx.action(() => {
this.collapsed.set(!this.collapsed.get());
})();
}
@mobxReact.observer
class MainSideBar extends React.Component<MainSideBarProps, {}> {
sidebarRef = React.createRef<HTMLDivElement>();
handleSessionClick(sessionId: string) {
GlobalCommandRunner.switchSession(sessionId);
@@ -176,9 +172,9 @@ class MainSideBar extends React.Component<{}, {}> {
getSessions() {
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
let sessionList = [];
let activeSessionId = GlobalModel.activeSessionId.get();
for (let session of GlobalModel.sessionList) {
const sessionList: Session[] = [];
const activeSessionId = GlobalModel.activeSessionId.get();
for (const session of GlobalModel.sessionList) {
if (!session.archived.get() || session.sessionId == activeSessionId) {
sessionList.push(session);
}
@@ -190,6 +186,7 @@ class MainSideBar extends React.Component<{}, {}> {
const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0);
return (
<SideBarItem
key={session.sessionId}
className={`${isActive ? "active" : ""}`}
frontIcon={<span className="index">{index + 1}</span>}
contents={session.name.get()}
@@ -208,97 +205,116 @@ class MainSideBar extends React.Component<{}, {}> {
}
render() {
let isCollapsed = this.collapsed.get();
let clientData = GlobalModel.clientData.get();
let clientData = this.props.clientData;
let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
}
let mainSidebar = GlobalModel.mainSidebarModel;
let isCollapsed = mainSidebar.getCollapsed();
return (
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={this.toggleCollapsed}>
<img src="public/logos/wave-logo.png" />
<ResizableSidebar
className="main-sidebar"
position="left"
enableSnap={true}
parentRef={this.props.parentRef}
>
{(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>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
<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="spacer" />
<div className="collapse-button" onClick={this.toggleCollapsed}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div>
<div className="separator" />
<div className="top">
<SideBarItem
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
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div>
<div className="separator" />
<SideBarItem
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<div
key="add_workspace"
className="add_workspace hoverEffect"
onClick={this.handleNewSession}
>
<AddIcon />
</div>,
]}
/>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<div className="separator" />
<SideBarItem
className="updateBanner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
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>,
]}
/>
</If>
<If condition={GlobalModel.isDev}>
<SideBarItem
frontIcon={<AppsIcon className="icon" />}
contents="Apps"
onClick={this.handlePluginsClick}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/>
</If>
<SideBarItem
frontIcon={<SettingsIcon className="icon" />}
contents="Settings"
onClick={this.handleSettingsClick}
/>
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</div>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<SideBarItem
key="update-available"
className="updateBanner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
/>
</If>
<If condition={GlobalModel.isDev}>
<SideBarItem
key="apps"
frontIcon={<AppsIcon className="icon" />}
contents="Apps"
onClick={this.handlePluginsClick}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/>
</If>
<SideBarItem
key="settings"
frontIcon={<SettingsIcon className="icon" />}
contents="Settings"
onClick={this.handleSettingsClick}
/>
<SideBarItem
key="documentation"
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
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>
);
}
}
+29 -28
View File
@@ -5,28 +5,25 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, Choose, When, Otherwise } from "tsx-control-statements/components";
import { If } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model";
import { renderCmdText, Button } from "../../common/common";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { renderCmdText } from "../../common/common";
import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
import "./cmdinput.less";
import { RotateIcon } from "../../common/icons/icons";
import { AIChat } from "./aichat";
import "./cmdinput.less";
dayjs.extend(localizedFormat);
const TDots = "â‹®";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef();
@@ -37,11 +34,11 @@ class CmdInput extends React.Component<{}, {}> {
}
updateCmdInputHeight() {
let elem = this.cmdInputRef.current;
const elem = this.cmdInputRef.current;
if (elem == null) {
return;
}
let height = elem.offsetHeight;
const height = elem.offsetHeight;
if (height == GlobalModel.inputModel.cmdInputHeight) {
return;
}
@@ -50,7 +47,7 @@ class CmdInput extends React.Component<{}, {}> {
})();
}
componentDidUpdate(prevProps, prevState, snapshot: {}): void {
componentDidUpdate(): void {
this.updateCmdInputHeight();
}
@@ -87,7 +84,7 @@ class CmdInput extends React.Component<{}, {}> {
e.preventDefault();
e.stopPropagation();
let inputModel = GlobalModel.inputModel;
const inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
inputModel.resetHistory();
} else {
@@ -113,9 +110,9 @@ class CmdInput extends React.Component<{}, {}> {
}
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let screen = GlobalModel.getActiveScreen();
const model = GlobalModel;
const inputModel = model.inputModel;
const screen = GlobalModel.getActiveScreen();
let ri: RemoteInstanceType = null;
let rptr: RemotePtrType = null;
if (screen != null) {
@@ -129,15 +126,13 @@ class CmdInput extends React.Component<{}, {}> {
feState = ri.festate;
}
feState = feState || {};
let infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get();
let aiChatShow = inputModel.aIChatShow.get();
let infoMsg = inputModel.infoMsg.get();
let hasInfo = infoMsg != null;
let focusVal = inputModel.physicalInputFocused.get();
let inputMode: string = inputModel.inputMode.get();
let textAreaInputKey = screen == null ? "null" : screen.screenId;
let win = GlobalModel.getScreenLinesById(screen.screenId);
const infoShow = inputModel.infoShow.get();
const historyShow = !infoShow && inputModel.historyShow.get();
const aiChatShow = inputModel.aIChatShow.get();
const focusVal = inputModel.physicalInputFocused.get();
const inputMode: string = inputModel.inputMode.get();
const textAreaInputKey = screen == null ? "null" : screen.screenId;
const win = GlobalModel.getScreenLinesById(screen.screenId);
let numRunningLines = 0;
if (win != null) {
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
@@ -232,11 +227,17 @@ class CmdInput extends React.Component<{}, {}> {
{focusVal && (
<div className="cmd-btn hoverEffect">
<If condition={historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>close (esc)</div>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
close (esc)
</div>
</If>
<If condition={!historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>history (ctrl-r)</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>AI (ctrl-space)</div>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
history (ctrl-r)
</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>
AI (ctrl-space)
</div>
</If>
</div>
)}
+1 -1
View File
@@ -287,7 +287,7 @@
}
&:not(:hover) .status-indicator {
.positional-icon-visible;
.status-indicator-visible;
}
&:hover {
+2 -5
View File
@@ -8,12 +8,12 @@
&.is-hidden {
display: none;
}
max-width: calc(100% - 20.5em);
background: @background-session;
border: 1px solid @base-border;
border-radius: 8px;
transition: width 0.2s ease;
// transition: width 0.2s ease;
margin-bottom: 0.5em;
margin-right: 0.5em;
.center-message {
display: flex;
@@ -24,6 +24,3 @@
color: @text-secondary;
}
}
.collapsed + .session-view {
max-width: calc(100% - 6.7em);
}
+17 -4
View File
@@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> {
if (session == null) {
return (
<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>
);
}
let activeScreen = session.getActiveScreen();
let cmdInputHeight = model.inputModel.cmdInputHeight.get();
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 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 (
<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} />
<ErrorBoundary>
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />
+86 -7
View File
@@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
import { boundMethod } from "autobind-decorator";
import { debounce } from "throttle-debounce";
import * as mobxReact from "mobx-react";
import {
handleJsonFetchResponse,
base64ToString,
@@ -2617,6 +2618,77 @@ 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 {
const width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth));
mobx.action(() => {
this.tempWidth.set(width);
this.tempCollapsed.set(newCollapsed);
})();
}
/**
* Gets the intended width for the sidebar. If the sidebar is being dragged, returns the tempWidth. If the sidebar is collapsed, returns the default width.
* @param ignoreCollapse If true, returns the persisted width even if the sidebar is collapsed.
* @returns The intended width for the sidebar or the default width if the sidebar is collapsed. Can be overridden using ignoreCollapse.
*/
getWidth(ignoreCollapse: boolean = false): number {
const clientData = GlobalModel.clientData.get();
let width = clientData?.clientopts?.mainsidebar?.width ?? MagicLayout.MainSidebarDefaultWidth;
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()) {
if (ignoreCollapse) {
return width;
} else {
return MagicLayout.MainSidebarMinWidth;
}
} else {
if (width <= MagicLayout.MainSidebarMinWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
const snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold;
if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
}
return width;
}
getCollapsed(): boolean {
const clientData = GlobalModel.clientData.get();
const 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 {
bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
@@ -3387,6 +3459,7 @@ class Model {
connectionViewModel: ConnectionsViewModel;
clientSettingsViewModel: ClientSettingsViewModel;
modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData",
});
@@ -3413,6 +3486,7 @@ class Model {
this.remotesModalModel = new RemotesModalModel();
this.remotesModel = new RemotesModel();
this.modalsModel = new ModalsModel();
this.mainSidebarModel = new MainSidebarModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running",
@@ -4100,15 +4174,15 @@ class Model {
if ("openaicmdinfochat" in update) {
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
}
if ("screenstatusindicator" in update) {
this.getScreenById_single(update.screenstatusindicator.screenid)?.setStatusIndicator(
update.screenstatusindicator.status
);
if ("screenstatusindicators" in update) {
for (const indicator of update.screenstatusindicators) {
this.getScreenById_single(indicator.screenid)?.setStatusIndicator(indicator.status);
}
}
if ("screennumrunningcommands" in update) {
this.getScreenById_single(update.screennumrunningcommands.screenid)?.setNumRunningCmds(
update.screennumrunningcommands.num
);
for (const snc of update.screennumrunningcommands) {
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
}
}
}
@@ -4980,6 +5054,11 @@ class CommandRunner {
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) {
let kwargs = {
nohist: "1",
+6 -2
View File
@@ -326,8 +326,8 @@ type ModelUpdateType = {
remoteview?: RemoteViewType;
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
alertmessage?: AlertMessageType;
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
screenstatusindicators?: ScreenStatusIndicatorUpdateType[];
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType[];
};
type HistoryViewDataType = {
@@ -524,6 +524,10 @@ type ClientOptsType = {
noreleasecheck: boolean;
acceptedtos: number;
confirmflags: ConfirmFlagsType;
mainsidebar: {
collapsed: boolean;
width: number;
};
};
type ReleaseInfoType = {
+58
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 RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
var ConfirmFlags = []string{"hideshellprompt"}
var SidebarNames = []string{"main"}
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
@@ -218,6 +219,7 @@ func init() {
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
registerCmdFn("sidebar:open", SidebarOpenCommand)
registerCmdFn("sidebar:close", SidebarCloseCommand)
@@ -4463,6 +4465,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
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 {
if len(key) > MaxOpenAIAPITokenLen {
return fmt.Errorf("invalid openai token, too long")
+3
View File
@@ -158,6 +158,7 @@ func (ws *WSState) ReplaceShell(shell *wsshell.WSShell) {
return
}
// returns all state required to display current UI
func (ws *WSState) handleConnection() error {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
@@ -167,6 +168,8 @@ func (ws *WSState) handleConnection() error {
}
remotes := remote.GetAllRemoteRuntimeState()
update.Remotes = remotes
// restore status indicators
update.ScreenStatusIndicators, update.ScreenNumRunningCommands = sstore.GetCurrentIndicatorState()
update.Connect = true
err = ws.Shell.WriteJson(update)
if err != nil {
+16
View File
@@ -190,6 +190,22 @@ func ScreenMemSetIndicatorLevel(screenId string, level StatusIndicatorLevel) {
ScreenMemStore[screenId].StatusIndicator = StatusIndicatorLevel_None
}
func GetCurrentIndicatorState() ([]*ScreenStatusIndicatorType, []*ScreenNumRunningCommandsType) {
MemLock.Lock()
defer MemLock.Unlock()
indicators := []*ScreenStatusIndicatorType{}
numRunningCommands := []*ScreenNumRunningCommandsType{}
for screenId, screenMem := range ScreenMemStore {
if screenMem.StatusIndicator > 0 {
indicators = append(indicators, &ScreenStatusIndicatorType{ScreenId: screenId, Status: screenMem.StatusIndicator})
}
if screenMem.NumRunningCommands > 0 {
numRunningCommands = append(numRunningCommands, &ScreenNumRunningCommandsType{ScreenId: screenId, Num: screenMem.NumRunningCommands})
}
}
return indicators, numRunningCommands
}
// safe because we return a copy
func GetScreenMemState(screenId string) *ScreenMemState {
MemLock.Lock()
+14 -8
View File
@@ -275,11 +275,17 @@ func (tdata *TelemetryData) Scan(val interface{}) error {
return quickScanJson(tdata, val)
}
type SidebarValueType struct {
Collapsed bool `json:"collapsed"`
Width int `json:"width"`
}
type ClientOptsType struct {
NoTelemetry bool `json:"notelemetry,omitempty"`
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
AcceptedTos int64 `json:"acceptedtos,omitempty"`
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
NoTelemetry bool `json:"notelemetry,omitempty"`
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
AcceptedTos int64 `json:"acceptedtos,omitempty"`
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
}
type FeOptsType struct {
@@ -1506,10 +1512,10 @@ func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, sc
}
}
update.ScreenStatusIndicator = &ScreenStatusIndicatorType{
update.ScreenStatusIndicators = []*ScreenStatusIndicatorType{{
ScreenId: screenId,
Status: newStatus,
}
}}
return nil
}
@@ -1539,10 +1545,10 @@ func ResetStatusIndicator(screenId string) error {
func IncrementNumRunningCmds_Update(update *ModelUpdate, screenId string, delta int) {
newNum := ScreenMemIncrementNumRunningCommands(screenId, delta)
log.Printf("IncrementNumRunningCmds_Update: screenId=%s, newNum=%d\n", screenId, newNum)
update.ScreenNumRunningCommands = &ScreenNumRunningCommandsType{
update.ScreenNumRunningCommands = []*ScreenNumRunningCommandsType{{
ScreenId: screenId,
Num: newNum,
}
}}
}
func IncrementNumRunningCmds(screenId string, delta int) {
+2 -2
View File
@@ -63,8 +63,8 @@ type ModelUpdate struct {
SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"`
OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"`
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"`
ScreenNumRunningCommands *ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
ScreenStatusIndicators []*ScreenStatusIndicatorType `json:"screenstatusindicators,omitempty"`
ScreenNumRunningCommands []*ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
}
func (*ModelUpdate) UpdateType() string {