mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-31 23:11:28 +01:00
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:
parent
bbf471566f
commit
189e632ff7
@ -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,
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 };
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 (⌘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}>
|
||||
...▼
|
||||
</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> </div>
|
||||
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
||||
<div className="renderer">
|
||||
<i className="fa-sharp fa-solid fa-fill" />
|
||||
{renderer}
|
||||
</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 (⌘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 }}>
|
||||
showing in sidebar =>
|
||||
@ -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>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)`,
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user