More Prompt Updates / Line Action Implementation (#357)

* termfontsize-sm (for meta1).  add some small margin.  remove rootIndicator.  switch cwd/remote to red if root

* break LineHeader out into its own react component

* actual set our --term colors as the *theme* for xtermjs

* move line-actions into its own div (absolute position).  hover/background colors

* add duration, format renderer, spacing, remove meta-wrap

* simplify cmdtext output.  simpler multi-line rendering.

* adjust position/height of line actions for better interaction with long lines

* fix horiz scrolling

* remove unused

* quick fix for text lines
This commit is contained in:
Mike Sawka 2024-02-28 18:23:14 -08:00 committed by GitHub
parent bbf471566f
commit 189e632ff7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 415 additions and 473 deletions

View File

@ -17,12 +17,12 @@ body {
body {
&.is-dev .sidebar {
background-color: var(--sidebar-dev-bg-color);
background-color: var(--app-panel-bg-color-dev);
}
}
body .sidebar {
background-color: var(--sidebar-bg-color);
background-color: var(--app-panel-bg-color);
}
textarea {
@ -117,7 +117,7 @@ svg.icon {
cursor: pointer;
}
.hideScrollbarUntillHover {
.scrollbar-hide-until-hover {
overflow: scroll;
&::-webkit-scrollbar-thumb,

View File

@ -7,7 +7,7 @@ import cn from "classnames";
class TabIcon extends React.Component<{ icon: string; color: string }> {
render() {
let { icon, color, className } = this.props;
let { icon, color } = this.props;
let iconClass = "";
if (icon === "default" || icon === "square") {
iconClass = "fa-solid fa-square fa-fw";

View File

@ -2,10 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
.term-prompt {
font-weight: normal;
font-size: var(--termfontsize);
line-height: var(--termlineheight);
// we are explicitly *not* setting font size properties here so prompt will inherit from caller
font-family: var(--termfontfamily);
font-weight: normal;
color: var(--term-gray);
i {
@ -62,11 +61,7 @@
color: var(--term-bright-green);
}
.term-prompt-end-user {
color: var(--term-bright-green);
}
.term-prompt-end-root {
&.term-prompt-isroot .term-prompt-cwd {
color: var(--term-bright-red);
}
}

View File

@ -94,11 +94,7 @@ class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<stri
if (remote && remote.remotecanonicalname) {
remoteTitle = "connected to " + remote.remotecanonicalname;
}
let cwdElem = (
<span title="current directory" className="term-prompt-cwd">
{cwd}
</span>
);
let cwdElem = <span className="term-prompt-cwd">{cwd}</span>;
let remoteElem = null;
if (remoteStr != "local") {
remoteElem = (
@ -107,12 +103,6 @@ class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<stri
</span>
);
}
let rootIndicatorElem = null;
if (isRoot) {
rootIndicatorElem = <span className="term-prompt-end-root">#</span>;
} else {
rootIndicatorElem = <span className="term-prompt-end-user">$</span>;
}
let branchElem = null;
let pythonElem = null;
let condaElem = null;
@ -142,9 +132,14 @@ class Prompt extends React.Component<{ rptr: RemotePtrType; festate: Record<stri
);
}
return (
<span className={cn("term-prompt", { "term-prompt-color": this.props.color })}>
{remoteElem} {condaElem} {pythonElem}
{cwdElem} {branchElem} {rootIndicatorElem}
<span
className={cn(
"term-prompt",
{ "term-prompt-color": this.props.color },
{ "term-prompt-isroot": isRoot }
)}
>
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem}
</span>
);
}

View File

@ -1,18 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
/**
* The file contains barebones of styling to appy themes to Prompt.
* @TODO: Find a way to change the theme system-wide. atm, we are captruing colors in main.less
*/
const themes = [
{
id: "default",
terminal: { foreground: "#eceeec", background: "black" },
},
];
const getTheme = (_id = "default") => themes.find(({ id }) => id === _id);
export { getTheme };

View File

@ -2,30 +2,13 @@
margin: 0;
padding: calc(var(--termpad) * 2) var(--termpad) calc(var(--termpad) * 2 + 1px) calc(var(--termpad) * 3);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
position: relative;
font-weight: normal;
font-family: var(--termfontfamily);
&.line-cmd {
flex-direction: column;
scroll-margin-bottom: 20px;
position: relative;
}
&.line-text {
// old formatting for text lines (requires override)
flex-direction: row;
padding-top: 5px;
.line-content {
display: flex;
flex-direction: column;
flex-grow: 1;
margin-left: 10px;
}
}
scroll-margin-bottom: 20px;
&.line-invalid {
color: var(--term-text-white);
@ -118,7 +101,6 @@
display: inline-block;
height: 1em;
margin-left: 0.5em;
margin-right: 0.5em;
vertical-align: text-top;
fill: var(--primary-background-color);
}
@ -152,152 +134,118 @@
display: block;
}
.line-header {
.line-header + div:not(.zero-height) {
margin-top: calc(var(--termpad) + 2px);
}
&:hover .line-actions {
background-color: var(--app-panel-bg-color);
.line-icon {
visibility: visible;
}
}
.line-actions {
position: absolute;
right: calc(var(--termpad) * 2);
top: 0;
font-size: 14px;
color: var(--line-actions-inactive-color);
border-radius: 4px;
padding-left: 4px;
padding-right: 4px;
line-height: 1.2;
display: flex;
flex-direction: row;
align-items: center;
.line-icon {
visibility: hidden;
&.active {
visibility: visible;
}
&:hover {
color: var(--line-actions-active-color);
}
padding: 4px;
cursor: pointer;
}
.line-icon + .line-icon:not(.line-icon-shrink-left) {
margin-left: 3px;
}
}
.line-header {
display: flex;
flex-direction: column;
width: 100%;
font-weight: normal;
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
line-height: var(--termlineheight);
&.is-expanded {
height: auto;
.meta + .meta {
margin-top: 2px;
}
.line-icon {
display: block;
visibility: hidden;
cursor: pointer;
padding: 3px;
border-radius: 50%;
}
.line-icon-show {
visibility: visible;
}
.line-icon + .line-icon {p
margin-left: 5px;
}
.line-icon.active {
visibility: visible;
display: block;
}
&:hover .line-icon {
visibility: visible;
display: block;
opacity: 1;
}
.meta.meta-line1 {
color: var(--term-gray);
}
.meta.meta-line2 {
margin-left: -2px;
}
}
.line-header + div:not(.zero-height) {
margin-top: var(--termpad);
}
.meta-wrap {
flex: 1 1 0px;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.meta {
display: flex;
flex-direction: row;
.user {
color: var(--line-meta-user-color);
font-weight: bold;
margin-top: 1px;
margin-right: 10px;
}
.ts,
.termopts,
.renderer {
.meta {
display: flex;
}
flex-direction: row;
.renderer {
margin-left: 3px;
svg {
width: 1em;
.user {
color: var(--line-meta-user-color);
font-weight: bold;
margin-top: 1px;
margin-right: 10px;
}
&.meta-line1 {
display: flex;
flex-direction: row;
font-size: var(--termfontsize-sm);
line-height: var(--termlineheight-sm);
color: var(--term-gray);
}
.meta-divider {
margin-left: var(--termpad);
margin-right: var(--termpad);
}
.ts,
.termopts,
.renderer {
display: flex;
}
.renderer .renderer-icon {
margin-right: 0.5em;
fill: var(--line-svg-fill-color);
}
.metapart-mono {
margin-left: 8px;
white-space: nowrap;
}
}
.settings {
display: none;
margin-left: 0.5em;
cursor: pointer;
width: 1em;
height: 1em;
border-radius: 50%;
line-height: 1em;
svg {
fill: var(--line-svg-fill-color);
&:hover {
fill: var(--line-svg-hover-fill-color);
}
}
}
.termopts {
display: none;
.resize-button {
cursor: pointer;
padding-left: 3px;
padding-right: 3px;
}
}
.metapart-mono {
margin-left: 8px;
white-space: nowrap;
}
.cmdtext {
overflow: hidden;
margin-left: 0;
}
.cmdtext-overflow {
flex-shrink: 0;
padding-right: 2px;
color: var(--term-text-white);
cursor: pointer;
}
.meta-cmdtext {
color: var(--term-bright-white);
}
}
.cmdtext-expanded-wrapper {
padding-left: 6px;
overflow-y: auto;
max-height: 60px;
border-left: 2px solid #333;
margin-left: 3px;
.cmdtext-expanded {
overflow: auto;
max-height: calc(var(--termlineheight) * 3.3);
white-space: pre;
color: var(--term-bright-white);
font-weight: bold;
width: calc(100% - var(--termpad) * 2);
color: var(--term-text-white);
padding-bottom: 5px;
&.is-multiline {
border-left: 2px solid #333;
margin-left: 3px;
padding-left: 6px;
}
}
}

View File

@ -24,8 +24,7 @@ import { Prompt } from "@/common/prompt/prompt";
import * as lineutil from "./lineutil";
import { ErrorBoundary } from "@/common/error/errorboundary";
import * as appconst from "@/app/appconst";
import { ReactComponent as FillIcon } from "@/assets/icons/line/fill.svg";
import * as util from "@/util/util";
import "./line.less";
@ -35,6 +34,225 @@ function cmdHasError(cmd: Cmd): boolean {
return cmd.getStatus() == "error" || cmd.getExitCode() != 0;
}
function getIsHidePrompt(line: LineType): boolean {
let rendererPlugin: RendererPluginType = null;
const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
const hidePrompt = rendererPlugin?.hidePrompt;
return hidePrompt;
}
@mobxReact.observer
class LineActions extends React.Component<{ screen: LineContainerType; line: LineType; cmd: Cmd }, {}> {
@boundMethod
clickStar() {
const { line } = this.props;
if (!line.star || line.star == 0) {
GlobalCommandRunner.lineStar(line.lineid, 1);
} else {
GlobalCommandRunner.lineStar(line.lineid, 0);
}
}
@boundMethod
clickPin() {
const { line } = this.props;
if (!line.pinned) {
GlobalCommandRunner.linePin(line.lineid, true);
} else {
GlobalCommandRunner.linePin(line.lineid, false);
}
}
@boundMethod
clickBookmark() {
const { line } = this.props;
GlobalCommandRunner.lineBookmark(line.lineid);
}
@boundMethod
clickDelete() {
const { line } = this.props;
GlobalCommandRunner.lineDelete(line.lineid, true);
}
@boundMethod
clickRestart() {
const { line } = this.props;
GlobalCommandRunner.lineRestart(line.lineid, true);
}
@boundMethod
clickMinimize() {
const { line } = this.props;
const isMinimized = line.linestate["wave:min"];
GlobalCommandRunner.lineMinimize(line.lineid, !isMinimized, true);
}
@boundMethod
clickMoveToSidebar() {
const { line } = this.props;
GlobalCommandRunner.screenSidebarAddLine(line.lineid);
}
@boundMethod
clickRemoveFromSidebar() {
GlobalCommandRunner.screenSidebarRemove();
}
@boundMethod
handleResizeButton() {
console.log("resize button");
}
@boundMethod
handleLineSettings(e: any): void {
e.preventDefault();
e.stopPropagation();
let { line } = this.props;
if (line != null) {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(line.linenum);
})();
GlobalModel.modalsModel.pushModal(appconst.LINE_SETTINGS);
}
}
render() {
let { line, screen } = this.props;
const isMinimized = line.linestate["wave:min"];
const containerType = screen.getContainerType();
return (
<div className="line-actions">
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate fa-fw" />
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash fa-fw" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
</div>
<If condition={containerType == appconst.LineContainer_Main}>
<div
key="minimize"
title={`${isMinimized ? "Show Output" : "Hide Output"}`}
className={cn("line-icon", isMinimized ? "active" : "")}
onClick={this.clickMinimize}
>
<If condition={isMinimized}>
<i className="fa-sharp fa-regular fa-circle-plus fa-fw" />
</If>
<If condition={!isMinimized}>
<i className="fa-sharp fa-regular fa-circle-minus fa-fw" />
</If>
</div>
<div className="line-icon line-sidebar" onClick={this.clickMoveToSidebar} title="Move to Sidebar">
<i className="fa-sharp fa-solid fa-right-to-line fa-fw" />
</div>
<div
key="settings"
title="Line Settings"
className="line-icon line-icon-shrink-left"
onClick={this.handleLineSettings}
>
<i className="fa-sharp fa-regular fa-ellipsis-vertical fa-fw" />
</div>
</If>
<If condition={containerType == appconst.LineContainer_Sidebar}>
<div
className="line-icon line-sidebar"
onClick={this.clickRemoveFromSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-left-to-line fa-fw" />
</div>
</If>
</div>
);
}
}
@mobxReact.observer
class LineHeader extends React.Component<{ screen: LineContainerType; line: LineType; cmd: Cmd }, {}> {
renderCmdText(cmd: Cmd): any {
if (cmd == null) {
return (
<div className="metapart-mono cmdtext">
<span className="term-bright-green">(cmd not found)</span>
</div>
);
}
const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return (
<React.Fragment>
<div
key="meta2"
className={cn(
"meta meta-line2 cmdtext-expanded no-highlight-scrollbar scrollbar-hide-until-hover",
{
"is-multiline": isMultiLine,
}
)}
>
{lineutil.getFullCmdText(cmd.getCmdStr())}
</div>
</React.Fragment>
);
}
renderMeta1(cmd: Cmd) {
let { line } = this.props;
let formattedTime: string = "";
let restartTs = cmd.getRestartTs();
let timeTitle: string = null;
if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
} else {
formattedTime = lineutil.getLineDateTimeStr(line.ts);
}
let renderer = line.renderer;
let durationMs = cmd.getDurationMs();
return (
<div key="meta1" className="meta meta-line1">
<SmallLineAvatar line={line} cmd={cmd} />
<div className="meta-divider">|</div>
<Prompt rptr={cmd.remote} festate={cmd.getRemoteFeState()} color={false} />
<div className="meta-divider">|</div>
<div title={timeTitle} className="ts">
{formattedTime} <If condition={durationMs > 0}>({util.formatDuration(durationMs)})</If>
</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="meta-divider">|</div>
<div className="renderer">
<i className="fa-sharp fa-solid fa-fill renderer-icon" />
{renderer}
</div>
</If>
</div>
);
}
render() {
let { line, cmd } = this.props;
const hidePrompt = getIsHidePrompt(line);
return (
<div key="header" className={cn("line-header", { "hide-prompt": hidePrompt })}>
{this.renderMeta1(cmd)}
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div>
);
}
}
@mobxReact.observer
class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> {
render() {
@ -193,14 +411,7 @@ class LineCmd extends React.Component<
{}
> {
lineRef: React.RefObject<any> = React.createRef();
cmdTextRef: React.RefObject<any> = React.createRef();
lastHeight: number;
isOverflow: OV<boolean> = mobx.observable.box(false, {
name: "line-overflow",
});
isCmdExpanded: OV<boolean> = mobx.observable.box(false, {
name: "cmd-expanded",
});
constructor(props) {
super(props);
@ -208,53 +419,6 @@ class LineCmd extends React.Component<
componentDidMount() {
this.componentDidUpdate(null, null, null);
this.checkCmdText();
}
@boundMethod
handleExpandCmd(): void {
mobx.action(() => {
this.isCmdExpanded.set(true);
})();
}
renderCmdText(cmd: Cmd): any {
if (cmd == null) {
return (
<div className="metapart-mono cmdtext">
<span className="term-bright-green">(cmd not found)</span>
</div>
);
}
if (this.isCmdExpanded.get()) {
return (
<React.Fragment>
<div key="meta2" className="meta meta-line2">
<div className="metapart-mono cmdtext">
<Prompt rptr={cmd.remote} festate={cmd.getRemoteFeState()} color={true} />
</div>
</div>
<div key="meta3" className="meta meta-line3 cmdtext-expanded-wrapper">
<div className="cmdtext-expanded">{lineutil.getFullCmdText(cmd.getCmdStr())}</div>
</div>
</React.Fragment>
);
}
const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
<Prompt rptr={cmd.remote} festate={cmd.getRemoteFeState()} color={true} />
<span> </span>
<span className="meta-cmdtext">{lineutil.getSingleLineCmdText(cmd.getCmdStr())}</span>
</div>
<If condition={this.isOverflow.get() || isMultiLine}>
<div className="cmdtext-overflow" onClick={this.handleExpandCmd}>
...&#x25BC;
</div>
</If>
</div>
);
}
// TODO: this might not be necessary anymore because we're using this.lastHeight
@ -268,34 +432,6 @@ class LineCmd extends React.Component<
componentDidUpdate(prevProps, prevState, snapshot: { height: number }): void {
this.handleHeightChange();
this.checkCmdText();
}
checkCmdText() {
const metaElem = this.cmdTextRef.current;
if (metaElem == null || metaElem.childNodes.length == 0) {
return;
}
const metaElemWidth = metaElem.offsetWidth;
if (metaElemWidth == 0) {
return;
}
const metaChild = metaElem.firstChild;
if (metaChild == null) {
return;
}
const children = metaChild.childNodes;
let childWidth = 0;
for (let i = 0; i < children.length; i++) {
let ch = children[i];
childWidth += ch.offsetWidth;
}
const isOverflow = childWidth > metaElemWidth;
if (isOverflow && isOverflow != this.isOverflow.get()) {
mobx.action(() => {
this.isOverflow.set(isOverflow);
})();
}
}
@boundMethod
@ -334,78 +470,6 @@ class LineCmd extends React.Component<
GlobalCommandRunner.screenSelectLine(String(line.linenum), "cmd");
}
@boundMethod
clickStar() {
const { line } = this.props;
if (!line.star || line.star == 0) {
GlobalCommandRunner.lineStar(line.lineid, 1);
} else {
GlobalCommandRunner.lineStar(line.lineid, 0);
}
}
@boundMethod
clickPin() {
const { line } = this.props;
if (!line.pinned) {
GlobalCommandRunner.linePin(line.lineid, true);
} else {
GlobalCommandRunner.linePin(line.lineid, false);
}
}
@boundMethod
clickBookmark() {
const { line } = this.props;
GlobalCommandRunner.lineBookmark(line.lineid);
}
@boundMethod
clickDelete() {
const { line } = this.props;
GlobalCommandRunner.lineDelete(line.lineid, true);
}
@boundMethod
clickRestart() {
const { line } = this.props;
GlobalCommandRunner.lineRestart(line.lineid, true);
}
@boundMethod
clickMinimize() {
const { line } = this.props;
const isMinimized = line.linestate["wave:min"];
GlobalCommandRunner.lineMinimize(line.lineid, !isMinimized, true);
}
@boundMethod
clickMoveToSidebar() {
const { line } = this.props;
GlobalCommandRunner.screenSidebarAddLine(line.lineid);
}
@boundMethod
clickRemoveFromSidebar() {
GlobalCommandRunner.screenSidebarRemove();
}
@boundMethod
handleResizeButton() {
console.log("resize button");
}
getIsHidePrompt(): boolean {
const { line } = this.props;
let rendererPlugin: RendererPluginType = null;
const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
const hidePrompt = rendererPlugin?.hidePrompt;
return hidePrompt;
}
getTerminalRendererHeight(cmd: Cmd): number {
const { screen, line, width } = this.props;
let height = 45 + 24; // height of zero height terminal
@ -440,7 +504,7 @@ class LineCmd extends React.Component<
} else {
// header is 16px tall with hide-prompt, 36px otherwise
const { screen, line, width } = this.props;
const hidePrompt = this.getIsHidePrompt();
const hidePrompt = getIsHidePrompt(line);
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows;
}
@ -463,48 +527,6 @@ class LineCmd extends React.Component<
);
}
@boundMethod
handleLineSettings(e: any): void {
e.preventDefault();
e.stopPropagation();
let { line } = this.props;
if (line != null) {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(line.linenum);
})();
GlobalModel.modalsModel.pushModal(appconst.LINE_SETTINGS);
}
}
renderMeta1(cmd: Cmd) {
let { line } = this.props;
let formattedTime: string = "";
let restartTs = cmd.getRestartTs();
let timeTitle: string = null;
if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
} 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>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer">
<i className="fa-sharp fa-solid fa-fill" />
{renderer}&nbsp;
</div>
</If>
</div>
);
}
getRendererOpts(cmd: Cmd): RendererOpts {
const { screen } = this.props;
return {
@ -583,7 +605,6 @@ class LineCmd extends React.Component<
render() {
const { screen, line, width, staticRender, visible } = this.props;
const isMinimized = line.linestate["wave:min"];
const isVisible = visible.get();
if (staticRender || !isVisible) {
return this.renderSimple();
@ -640,7 +661,6 @@ class LineCmd extends React.Component<
)
.get();
const isRunning = cmd.isRunning();
const isExpanded = this.isCmdExpanded.get();
const cmdError = cmdHasError(cmd);
const mainDivCn = cn(
"line",
@ -660,6 +680,7 @@ class LineCmd extends React.Component<
const hidePrompt = rendererPlugin?.hidePrompt;
const termFontSize = GlobalModel.getTermFontSize();
const containerType = screen.getContainerType();
const isMinimized = line.linestate["wave:min"] && containerType == appconst.LineContainer_Main;
return (
<div
className={mainDivCn}
@ -672,73 +693,8 @@ class LineCmd extends React.Component<
<If condition={isSelected || cmdError}>
<div key="mask" className={cn("line-mask", { "error-mask": cmdError })}></div>
</If>
<div
key="header"
className={cn("line-header", { "is-expanded": isExpanded }, { "hide-prompt": hidePrompt })}
>
<div key="meta" className="meta-wrap">
{this.renderMeta1(cmd)}
<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" />
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark", "hoverEffect")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark" />
</div>
<If condition={containerType == appconst.LineContainer_Main}>
<div
key="minimize"
title={`${isMinimized ? "Maximise" : "Minimize"}`}
className={cn(
"line-icon",
"line-minimize",
"hoverEffect",
isMinimized ? "line-icon-show" : ""
)}
onClick={this.clickMinimize}
>
<If condition={isMinimized}>
<i className="fa-sharp fa-regular fa-circle-plus" />
</If>
<If condition={!isMinimized}>
<i className="fa-sharp fa-regular fa-circle-minus" />
</If>
</div>
<div
className="line-icon line-sidebar"
onClick={this.clickMoveToSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-right-to-line" />
</div>
<div
key="settings"
title="Line Settings"
className="line-icon"
onClick={this.handleLineSettings}
>
<i className="fa-sharp fa-regular fa-ellipsis-vertical" />
</div>
</If>
<If condition={containerType == appconst.LineContainer_Sidebar}>
<div
className="line-icon line-sidebar"
onClick={this.clickRemoveFromSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-left-to-line" />
</div>
</If>
</div>
<LineActions screen={screen} line={line} cmd={cmd} />
<LineHeader screen={screen} line={line} cmd={cmd} />
<If condition={!isMinimized && isInSidebar}>
<div className="sidebar-message" style={{ fontSize: termFontSize }}>
&nbsp;&nbsp;showing in sidebar =&gt;
@ -872,7 +828,7 @@ class LineText extends React.Component<
name: "computed-isFocused",
})
.get();
const mainClass = cn("line", "line-text", "focus-parent");
const mainClass = cn("line", "line-text", "focus-parent", { selected: isSelected });
return (
<div
className={mainClass}
@ -881,13 +837,18 @@ class LineText extends React.Component<
data-screenid={line.screenid}
onClick={this.clickHandler}
>
<div className={cn("focus-indicator", { selected: isSelected }, { active: isSelected && isFocused })} />
<div className="line-content">
<div className="meta">
<If condition={isSelected}>
<div key="mask" className="line-mask"></div>
</If>
<div key="header" className="line-header">
<div className="meta meta-line1">
<SmallLineAvatar line={line} cmd={null} onRightClick={this.onAvatarRightClick} />
<div className="meta-divider">|</div>
<div className="ts">{formattedTime}</div>
</div>
<div className="text">{line.text}</div>
</div>
<div key="text" className="text">
{line.text}
</div>
</div>
);

View File

@ -28,12 +28,16 @@
font-size: var(--termfontsize);
}
.lines-spacer + .line-sep-labeled {
margin-top: var(--termpad);
}
.line-sep-labeled::before,
.line-sep-labeled::after {
content: "";
height: 1px;
flex-grow: 1;
background-color: var(--app-text-color);
background-color: var(--term-gray);
}
.line-sep-labeled::before {

View File

@ -9,6 +9,8 @@
--termfontsize: 12px;
--termlineheight: 15px;
--termpad: 7px; // padding value (scaled to termfontsize)
--termfontsize-sm: 10px;
--termlineheight-sm: 13px;
// other fonts
--fixed-font: "Martian Mono", sans-serif;
@ -43,6 +45,8 @@
--app-border-color: rgb(51, 51, 51);
--app-maincontent-bg-color: #333;
--app-border-radius: 10px;
--app-panel-bg-color: rgba(21, 23, 21, 1);
--app-panel-bg-color-dev: rgb(21, 23, 48);
// global generic colors
--app-black: rgb(0, 0, 0);
@ -150,8 +154,6 @@
--hotkey-text-color: rgb(195, 200, 194);
// sidebar colors
--sidebar-bg-color: rgba(21, 23, 21, 1);
--sidebar-dev-bg-color: rgb(21, 23, 48);
--sidebar-settings-color: rgb(255, 255, 255);
--sidebar-separator-color: var(--app-border-color);
--sidebar-highlight-color: rgba(241, 246, 243, 0.08);
@ -186,6 +188,8 @@
--line-status-success-fill: rgb(88, 193, 66);
--line-status-error-fill: #cc0000;
--line-status-warning-fill: #ffa500;
--line-actions-inactive-color: rgba(255, 255, 255, 0.5);
--line-actions-active-color: rgba(255, 255, 255, 1);
// view colors
// todo: bookmarks is a view, colors must be unified with --view* colors

View File

@ -324,7 +324,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
]}
/>
<div
className="middle hideScrollbarUntillHover"
className="middle scrollbar-hide-until-hover"
id="sidebar-middle"
style={{
maxHeight: `calc(100vh - ${this.middleHeightSubtractor.get()}px)`,

View File

@ -182,7 +182,7 @@ class ScreenTabs extends React.Component<
{/* Inner container ensures that hovering over the scrollbar doesn't trigger the hover effect on the tabs. This prevents weird flickering of the icons when the mouse is moved over the scrollbar. */}
<div
key="container-inner"
className="screen-tabs-container-inner no-highlight-scrollbar hideScrollbarUntillHover"
className="screen-tabs-container-inner no-highlight-scrollbar scrollbar-hide-until-hover"
>
<Reorder.Group
className="screen-tabs"

View File

@ -37,6 +37,10 @@ class Cmd {
return this.data.get().restartts;
}
getDurationMs(): number {
return this.data.get().durationms;
}
getAsWebCmd(lineid: string): WebCmd {
let cmd = this.data.get();
let remote = this.model.getRemote(this.remote.remoteid);

View File

@ -353,12 +353,16 @@ class Model {
if (fontSize > appconst.MaxFontSize) {
fontSize = appconst.MaxFontSize;
}
const fontSizeSm = fontSize - 2;
const monoFontSize = getMonoFontSize(fontSize);
const monoFontSizeSm = getMonoFontSize(fontSizeSm);
mobx.action(() => {
this.bumpRenderVersion();
this.setStyleVar("--termfontsize", fontSize + "px");
this.setStyleVar("--termlineheight", monoFontSize.height + "px");
this.setStyleVar("--termpad", Math.floor(monoFontSize.height / 2) + "px");
this.setStyleVar("--termpad", monoFontSize.pad + "px");
this.setStyleVar("--termfontsize-sm", fontSizeSm + "px");
this.setStyleVar("--termlineheight-sm", monoFontSizeSm.height + "px");
})();
}

View File

@ -3,6 +3,7 @@
import * as mobx from "mobx";
import { Terminal } from "xterm";
import type { ITheme } from "xterm";
//TODO: replace with `@xterm/addon-web-links` when it's available as stable
import { WebLinksAddon } from "xterm-addon-web-links";
import { sprintf } from "sprintf-js";
@ -10,7 +11,6 @@ import { boundMethod } from "autobind-decorator";
import { windowWidthToCols, windowHeightToRows } from "@/util/textmeasure";
import { boundInt } from "@/util/util";
import { GlobalModel } from "@/models";
import { getTheme } from "@/common/themes/themes";
type DataUpdate = {
data: Uint8Array;
@ -36,6 +36,30 @@ type TermWrapOpts = {
onUpdateContentHeight: (termContext: RendererContext, height: number) => void;
};
function getThemeFromCSSVars(): ITheme {
let theme: ITheme = {};
let rootStyle = getComputedStyle(document.documentElement);
theme.foreground = rootStyle.getPropertyValue("--term-white");
theme.background = rootStyle.getPropertyValue("--term-black");
theme.black = rootStyle.getPropertyValue("--term-black");
theme.red = rootStyle.getPropertyValue("--term-red");
theme.green = rootStyle.getPropertyValue("--term-green");
theme.yellow = rootStyle.getPropertyValue("--term-yellow");
theme.blue = rootStyle.getPropertyValue("--term-blue");
theme.magenta = rootStyle.getPropertyValue("--term-magenta");
theme.cyan = rootStyle.getPropertyValue("--term-cyan");
theme.white = rootStyle.getPropertyValue("--term-white");
theme.brightBlack = rootStyle.getPropertyValue("--term-bright-black");
theme.brightRed = rootStyle.getPropertyValue("--term-bright-red");
theme.brightGreen = rootStyle.getPropertyValue("--term-bright-green");
theme.brightYellow = rootStyle.getPropertyValue("--term-bright-yellow");
theme.brightBlue = rootStyle.getPropertyValue("--term-bright-blue");
theme.brightMagenta = rootStyle.getPropertyValue("--term-bright-magenta");
theme.brightCyan = rootStyle.getPropertyValue("--term-bright-cyan");
theme.brightWhite = rootStyle.getPropertyValue("--term-bright-white");
return theme;
}
// cmd-instance
class TermWrap {
terminal: any;
@ -86,13 +110,13 @@ class TermWrap {
let cols = windowWidthToCols(opts.winSize.width, opts.fontSize);
this.termSize = { rows: opts.termOpts.rows, cols: cols };
}
const { terminal } = getTheme();
let theme = getThemeFromCSSVars();
this.terminal = new Terminal({
rows: this.termSize.rows,
cols: this.termSize.cols,
fontSize: opts.fontSize,
fontFamily: opts.fontFamily,
theme: { foreground: terminal.foreground, background: terminal.background },
theme: theme,
});
this.terminal.loadAddon(
new WebLinksAddon((e, uri) => {

View File

@ -268,6 +268,26 @@ function getDateStr(d: Date): string {
return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr;
}
function formatDuration(ms: number): string {
if (ms < 1000) {
return ms + "ms";
}
if (ms < 10000) {
return (ms / 1000).toFixed(2) + "s";
}
if (ms < 100000) {
return (ms / 1000).toFixed(1) + "s";
}
if (ms < 60 * 60 * 1000) {
let mins = Math.floor(ms / 60000);
let secs = Math.floor((ms % 60000) / 1000);
return mins + "m" + secs + "s";
}
let hours = Math.floor(ms / (60 * 60 * 1000));
let mins = Math.floor((ms % (60 * 60 * 1000)) / 60000);
return hours + "h" + mins + "m";
}
function getRemoteConnVal(r: RemoteType): number {
switch (r.status) {
case "connected":
@ -400,4 +420,5 @@ export {
getRemoteName,
ces,
fireAndForget,
formatDuration,
};