From 844eb58920a12351e99aacca8bf10d9568a7a074 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 15 Feb 2023 20:32:17 -0800 Subject: [PATCH] line collapsing --- src/main.tsx | 164 +++++++++++++++++++++++++++++++++++++++++---------- src/model.ts | 12 ++++ src/sh2.less | 103 ++++++++++++++++++++++++++++++-- 3 files changed, 243 insertions(+), 36 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index ec39e28a..4bca95c0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -30,7 +30,7 @@ type OMap = mobx.ObservableMap; type HeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void; -type RendererComponentProps = {sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType}; +type RendererComponentProps = {sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, collapsed : boolean}; type RendererComponentType = { new(props : RendererComponentProps) : React.Component }; type InterObsValue = { @@ -121,9 +121,11 @@ function getCwdStr(remote : RemoteType, state : FeStateType) : string { return cwd; } -function getLineDateStr(ts : number) : string { +function getLineDateTimeStr(ts : number) : string { let lineDate = new Date(ts); let nowDate = new Date(); + + if (nowDate.getFullYear() != lineDate.getFullYear()) { return dayjs(lineDate).format("ddd L LTS"); } @@ -140,6 +142,44 @@ function getLineDateStr(ts : number) : string { } } +function getTodayStr() : string { + return getDateStr(new Date()); +} + +function getYesterdayStr() : string { + let d = new Date(); + d.setDate(d.getDate()-1); + return getDateStr(d); +} + +const DOW_STRS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +function getDateStr(d : Date) : string { + let yearStr = String(d.getFullYear()); + let monthStr = String(d.getMonth()+1); + if (monthStr.length == 1) { + monthStr = "0" + monthStr; + } + let dayStr = String(d.getDate()); + if (dayStr.length == 1) { + dayStr = "0" + dayStr; + } + let dowStr = DOW_STRS[d.getDay()]; + return dowStr + " " + yearStr + "-" + monthStr + "-" + dayStr; +} + +function getLineDateStr(todayDate : string, yesterdayDate : string, ts : number) : string { + let lineDate = new Date(ts); + let dateStr = getDateStr(lineDate); + if (dateStr == todayDate) { + return "today"; + } + if (dateStr == yesterdayDate) { + return "yesterday"; + } + return dateStr; +} + @mobxReact.observer class LineAvatar extends React.Component<{line : LineType, cmd : Cmd}, {}> { render() { @@ -171,11 +211,11 @@ class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}> render() { let {sw, line} = this.props; - let formattedTime = getLineDateStr(line.ts); + let formattedTime = getLineDateTimeStr(line.ts); let isSelected = mobx.computed(() => (sw.selectedLine.get() == line.linenum), {name: "computed-isSelected"}).get(); let isFocused = mobx.computed(() => (sw.focusType.get() == "cmd"), {name: "computed-isFocused"}).get(); return ( -
+
@@ -218,7 +258,7 @@ class Prompt extends React.Component<{rptr : RemotePtrType, festate : FeStateTyp } @mobxReact.observer -class ImageRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void}, {}> { +class ImageRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void, collapsed : boolean}, {}> { elemRef : React.RefObject = React.createRef(); imageDivRef : React.RefObject = React.createRef(); imageLoaded : mobx.IObservableValue = mobx.observable.box(false, {name: "imageLoaded"}); @@ -267,11 +307,11 @@ class ImageRenderer extends React.Component<{sw : ScreenWindow, line : LineType, } checkLoad() : void { - let {line, staticRender, visible} = this.props; + let {line, staticRender, visible, collapsed} = this.props; if (staticRender) { return; } - let vis = visible && visible.get(); + let vis = visible && visible.get() && !collapsed; let curVis = this.imageLoaded.get(); if (vis && !curVis) { this.loadImage(); @@ -316,8 +356,9 @@ class ImageRenderer extends React.Component<{sw : ScreenWindow, line : LineType, if (imageModel != null) { let dataVersion = imageModel.dataBuf.dataVersion.get(); } + let collapsed = this.props.collapsed; return ( -
+
...
@@ -326,7 +367,7 @@ class ImageRenderer extends React.Component<{sw : ScreenWindow, line : LineType, } @mobxReact.observer -class TerminalRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void}, {}> { +class TerminalRenderer extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : () => void, collapsed : boolean}, {}> { termLoaded : mobx.IObservableValue = mobx.observable.box(false, {name: "linecmd-term-loaded"}); elemRef : React.RefObject = React.createRef(); @@ -373,11 +414,11 @@ class TerminalRenderer extends React.Component<{sw : ScreenWindow, line : LineTy } checkLoad() : void { - let {line, staticRender, visible} = this.props; + let {line, staticRender, visible, collapsed} = this.props; if (staticRender) { return; } - let vis = visible && visible.get(); + let vis = visible && visible.get() && !collapsed; let curVis = this.termLoaded.get(); if (vis && !curVis) { this.loadTerminal(); @@ -428,7 +469,7 @@ class TerminalRenderer extends React.Component<{sw : ScreenWindow, line : LineTy } render() { - let {sw, line, width, staticRender, visible} = this.props; + let {sw, line, width, staticRender, visible, collapsed} = this.props; let isVisible = visible.get(); // for reaction let isPhysicalFocused = mobx.computed(() => sw.getIsFocused(line.linenum), {name: "computed-getIsFocused"}).get(); let isFocused = mobx.computed(() => { @@ -440,7 +481,7 @@ class TerminalRenderer extends React.Component<{sw : ScreenWindow, line : LineTy let termHeight = termHeightFromRows(usedRows); let termLoaded = this.termLoaded.get(); return ( -
+
@@ -460,7 +501,7 @@ class MarkdownRenderer extends React.Component<{sw : ScreenWindow, line : LineTy } @mobxReact.observer -class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType}, {}> { +class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, collapsed : OV, topBorder : boolean}, {}> { lineRef : React.RefObject = React.createRef(); rtnStateDiff : mobx.IObservableValue = mobx.observable.box(null, {name: "linecmd-rtn-state-diff"}); rtnStateDiffFetched : boolean = false; @@ -471,8 +512,8 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width } checkStateDiffLoad() : void { - let {line, staticRender, visible} = this.props; - if (staticRender) { + let {line, staticRender, visible, collapsed} = this.props; + if (staticRender || collapsed.get()) { return; } if (!visible) { @@ -618,13 +659,21 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width handleResizeButton() { console.log("resize button"); } + + @boundMethod + handleCollapsedClick() { + mobx.action(() => { + let isCollapsed = this.props.collapsed.get(); + this.props.collapsed.set(!isCollapsed); + })(); + } render() { - let {sw, line, width, staticRender, visible} = this.props; + let {sw, line, width, staticRender, visible, topBorder} = this.props; let model = GlobalModel; let lineid = line.lineid; let isVisible = visible.get(); - let formattedTime = getLineDateStr(line.ts); + let formattedTime = getLineDateTimeStr(line.ts); let cmd = model.getCmd(line); if (cmd == null) { return ( @@ -648,14 +697,18 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width return isPhysicalFocused && swFocusType == "cmd-fg" }, {name: "computed-isFgFocused"}).get(); let isStatic = staticRender; + let isRunning = cmd.isRunning() + let isCollapsed = this.props.collapsed.get(); let rsdiff = this.rtnStateDiff.get(); // console.log("render", "#" + line.linenum, termHeight, usedRows, cmd.getStatus(), (this.rtnStateDiff.get() != null), (!cmd.isRunning() ? "cmd-done" : "running")); let mainDivCn = cn( "line", "line-cmd", {"focus": isFocused}, - {"cmd-done": !cmd.isRunning()}, + {"cmd-done": !isRunning}, {"has-rtnstate": cmd.getRtnState()}, + {"collapsed": isCollapsed}, + {"top-border": topBorder}, ); let RendererComponent : RendererComponentType = TerminalRenderer; if (line.renderer == "image") { @@ -668,8 +721,12 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
+
+ + +
-
+
{line.userid}
{formattedTime}
@@ -677,7 +734,7 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
-
+
{this.renderCmdText(cmd, remote)}
@@ -691,8 +748,8 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width
- - + +
state unchanged
@@ -711,7 +768,7 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width } @mobxReact.observer -class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType}, {}> { +class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : HeightChangeCallbackType, collapsed : OV, topBorder : boolean}, {}> { render() { let line = this.props.line; if (line.archived) { @@ -2087,6 +2144,7 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line lastOffsetWidth : number = 0; ignoreNextScroll : boolean = false; visibleMap : Map>; // lineid => OV + collapsedMap : Map>; // lineid => OV lastLinesLength : number = 0; lastSelectedLine : number = 0; @@ -2098,6 +2156,7 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line this.linesRef = React.createRef(); this.computeAnchorLine_throttled = throttle(100, this.computeAnchorLine.bind(this), {noLeading: true, noTrailing: false}); this.visibleMap = new Map(); + this.collapsedMap = new Map(); this.computeVisibleMap_debounced = debounce(1000, this.computeVisibleMap.bind(this)); } @@ -2368,25 +2427,67 @@ class LinesView extends React.Component<{sw : ScreenWindow, width : number, line this.restoreAnchorOffset("height-change"); this.computeVisibleMap_debounced(); } + + hasTopBorder(lines : LineType[], idx : number) : boolean { + if (idx == 0) { + return false; + } + let curLineNumStr = String(lines[idx].linenum); + let prevLineNumStr = String(lines[idx-1].linenum); + return !this.collapsedMap.get(curLineNumStr).get() || !this.collapsedMap.get(prevLineNumStr).get(); + } + + getDateSepStr(lines : LineType[], idx : number, prevStr : string, todayStr : string, yesterdayStr : string) : string { + let curLineDate = new Date(lines[idx].ts); + let curLineFormat = dayjs(curLineDate).format("ddd YYYY-MM-DD"); + if (idx == 0) { + return ; + } + let prevLineDate = new Date(lines[idx].ts); + let prevLineFormat = dayjs(prevLineDate).format("YYYY-MM-DD"); + return null; + } render() { let {sw, width, lines} = this.props; let selectedLine = sw.selectedLine.get(); // for re-rendering let line : LineType = null; - let idx : number = 0; for (let i=0; i{dateSepStr}
+ lineElements.push(sepElem); + } + let topBorder = (dateSepStr == null) && this.hasTopBorder(lines, idx); + let lineElem = ; + lineElements.push(lineElem); } return (
- - - + {lineElements}
); } @@ -2495,15 +2596,16 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> { let session = GlobalModel.getSessionById(sw.sessionId); let isActive = sw.isActive(); let selectedLine = sw.selectedLine.get(); + let lines = win.getNonArchivedLines(); return (
{sw.name.get()}
- 0}> - + 0}> + - +
[session="{session.name.get()}" screen="{screen.name.get()}" window="{sw.name.get()}"]
diff --git a/src/model.ts b/src/model.ts index e907af6d..d3c3eeed 100644 --- a/src/model.ts +++ b/src/model.ts @@ -639,6 +639,18 @@ class Window { this.windowId = windowId; } + getNonArchivedLines() : LineType[] { + let rtn : LineType[] = []; + for (let i=0; i { if (win.curremote != null && win.curremote.remoteid != "") { diff --git a/src/sh2.less b/src/sh2.less index ccacb014..028ff7cb 100644 --- a/src/sh2.less +++ b/src/sh2.less @@ -439,6 +439,7 @@ html, body, #main { .avatar { cursor: pointer; + margin-right: 0; } .line-header { @@ -483,6 +484,13 @@ html, body, #main { margin-left: 50px; .mono-font(); } + + &.collapsed { + height: 0; + padding: 0; + margin: 0; + overflow: hidden; + } } .terminal-wrapper { @@ -496,6 +504,13 @@ html, body, #main { margin: 0; } + &.collapsed { + height: 0; + padding: 0; + margin: 0; + overflow: hidden; + } + .terminal-connectelem { overflow-y: hidden; overflow-x: hidden; @@ -575,15 +590,59 @@ html, body, #main { } .line { - margin: 0px 5px 5px 5px; + margin: 0px 5px 0px 5px; padding: 10px 5px 5px 12px; display: flex; line-height: 1.25; - border-top: 1px solid #777; overflow: hidden; flex-shrink: 0; position: relative; + &.collapsed { + margin: 2px 5px 0px 5px; + padding: 0px 5px 0px 12px; + + .avatar { + height: 20px; + width: 38px; + + &.num-4 { + font-size: 13px; + } + + &.num-5 { + font-size: 11px; + } + + &.num-6 { + font-size: 9px; + } + } + + .meta-wrap .meta.meta-line1 { + display: none; + } + + .focus-indicator { + height: calc(100% - 8px); + top: 4px; + } + + .line-star { + padding-top: 0; + padding-bottom: 0; + font-size: 20px; + margin-top: -4px; + margin-bottom: -2px; + } + } + + &.top-border, &.collapsed.top-border { + border-top: 1px solid #777; + padding-top: 5px; + margin-top: 5px; + } + &:nth-child(2) { margin: 0px 5px 5px 5px; padding: 0px 5px 5px 12px; @@ -626,7 +685,7 @@ html, body, #main { } .avatar { - height: 38px; + max-height: 38px; width: 38px; background-color: #555; display: flex; @@ -702,6 +761,18 @@ html, body, #main { } } + .collapsed-indicator { + width: 20px; + font-size: 20px; + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + margin-left: 2px; + margin-right: 2px; + } + .meta { display: flex; flex-direction: row; @@ -716,9 +787,9 @@ html, body, #main { } .ts { - color: #ddd; + color: #aaa; margin-top: 5px; - font-size: 12px; + .mono-font(11px); } .termopts { @@ -841,6 +912,28 @@ body .xterm .xterm-viewport { .lines-spacer { flex-grow: 1; } + + .line-sep { + display: flex; + align-items: center; + .mono-font(11px); + color: #aaa; + } + + .line-sep::before, .line-sep::after { + content: ''; + height: 2px; + background-color: #ccc; + flex-grow: 1; + } + + .line-sep::before { + margin-right: 1rem; + } + + .line-sep::after { + margin-left: 1rem; + } } .cmd-input {