auto resize terminal width to window size

This commit is contained in:
sawka 2022-07-14 00:54:31 -07:00
parent 8a710669ec
commit 879cb03da0
4 changed files with 187 additions and 136 deletions

View File

@ -18,20 +18,6 @@ function getLineId(line : LineType) : string {
return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid); return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid);
} }
@mobxReact.observer
class LineMeta extends React.Component<{line : LineType}, {}> {
render() {
let line = this.props.line;
return (
<div className="meta">
<div className="lineid">{line.lineid}</div>
<div className="user">{line.userid}</div>
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
</div>
);
}
}
function getLineDateStr(ts : number) : string { function getLineDateStr(ts : number) : string {
let lineDate = new Date(ts); let lineDate = new Date(ts);
let nowDate = new Date(); let nowDate = new Date();
@ -76,7 +62,7 @@ class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
} }
@mobxReact.observer @mobxReact.observer
class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}> { class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType, width: number}, {}> {
termLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false); termLoaded : mobx.IObservableValue<boolean> = mobx.observable.box(false);
constructor(props) { constructor(props) {
@ -89,7 +75,7 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
let cmd = model.getCmd(line); let cmd = model.getCmd(line);
if (cmd != null) { if (cmd != null) {
let termElem = document.getElementById("term-" + getLineId(line)); let termElem = document.getElementById("term-" + getLineId(line));
cmd.connectElem(termElem, sw.screenId, sw.windowId); cmd.connectElem(termElem, sw.screenId, sw.windowId, this.props.width);
mobx.action(() => this.termLoaded.set(true))(); mobx.action(() => this.termLoaded.set(true))();
} }
} }
@ -181,6 +167,8 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
} }
let termLoaded = this.termLoaded.get(); let termLoaded = this.termLoaded.get();
let cellHeightPx = 16; let cellHeightPx = 16;
let cellWidthPx = 8;
let termWidth = Math.max(Math.trunc((this.props.width - 20)/cellWidthPx), 10);
let usedRows = cmd.getUsedRows(sw.screenId, sw.windowId); let usedRows = cmd.getUsedRows(sw.screenId, sw.windowId);
let totalHeight = cellHeightPx * usedRows; let totalHeight = cellHeightPx * usedRows;
let remote = model.getRemote(cmd.remoteId); let remote = model.getRemote(cmd.remoteId);
@ -190,33 +178,36 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
let termOpts = cmd.getTermOpts(); let termOpts = cmd.getTermOpts();
let isFocused = cmd.getIsFocused(sw.screenId, sw.windowId); let isFocused = cmd.getIsFocused(sw.screenId, sw.windowId);
return ( return (
<div className="line line-cmd" id={"line-" + getLineId(line)}> <div className={cn("line", "line-cmd", {"focus": isFocused})} id={"line-" + getLineId(line)}>
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})} onClick={this.doRefresh}> <div className="line-header">
{lineid} <div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})} onClick={this.doRefresh}>
</div> {lineid}
<div className="line-content">
<div className="meta">
<div className="user" style={{display: "none"}}>{line.userid}</div>
<div className="ts">{formattedTime}</div>
</div> </div>
<div className="meta"> <div className="meta-wrap">
<div className="metapart-mono" style={{display: "none"}}> <div className="meta">
{line.cmdid} <div className="user" style={{display: "none"}}>{line.userid}</div>
({termOpts.rows}x{termOpts.cols}) <div className="ts">{formattedTime}</div>
width={this.props.width}, cellwidth={termWidth}
</div>
<div className="meta">
<div className="metapart-mono" style={{display: "none"}}>
{line.cmdid}
({termOpts.rows}x{termOpts.cols})
</div>
{this.renderCmdText(cmd, remote)}
</div> </div>
{this.renderCmdText(cmd, remote)}
</div>
<div className={cn("terminal-wrapper", {"focus": isFocused})} style={{overflowY: "hidden"}}>
<div className="terminal" id={"term-" + getLineId(line)} data-cmdid={line.cmdid} style={{height: totalHeight}}></div>
</div> </div>
</div> </div>
<div className={cn("terminal-wrapper", {"focus": isFocused})} style={{overflowY: "hidden"}}>
<div className="terminal" id={"term-" + getLineId(line)} data-cmdid={line.cmdid} style={{height: totalHeight}}></div>
</div>
</div> </div>
); );
} }
} }
@mobxReact.observer @mobxReact.observer
class Line extends React.Component<{sw : ScreenWindow, line : LineType}, {}> { class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number}, {}> {
render() { render() {
let line = this.props.line; let line = this.props.line;
if (line.linetype == "text") { if (line.linetype == "text") {
@ -350,10 +341,12 @@ class CmdInput extends React.Component<{}, {}> {
@mobxReact.observer @mobxReact.observer
class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> { class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
mutObs : any; mutObs : any;
rszObs : any
randomId : string; randomId : string;
width : mobx.IObservableValue<number> = mobx.observable.box(0);
scrollToBottom(reason : string) { scrollToBottom(reason : string) {
let elem = document.getElementById(this.getLinesId()); let elem = document.getElementById(this.getLinesDOMId());
if (elem == null) { if (elem == null) {
return; return;
} }
@ -374,23 +367,45 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
} }
componentDidMount() { componentDidMount() {
let elem = document.getElementById(this.getLinesId()); let elem = document.getElementById(this.getLinesDOMId());
if (elem == null) { if (elem != null) {
return; this.mutObs = new MutationObserver(this.handleDomMutation.bind(this));
this.mutObs.observe(elem, {childList: true});
elem.addEventListener("termresize", this.handleTermResize);
let {sw} = this.props;
if (sw && sw.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom("mount"), 0);
}
} }
this.mutObs = new MutationObserver(this.handleDomMutation.bind(this)); let wvElem = document.getElementById(this.getWindowViewDOMId());
this.mutObs.observe(elem, {childList: true}); if (wvElem != null) {
elem.addEventListener("termresize", this.handleTermResize); this.rszObs = new ResizeObserver(this.handleResize.bind(this));
let {sw} = this.props; this.rszObs.observe(wvElem);
if (sw && sw.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom("mount"), 0);
} }
} }
updateWidth(width : number) {
mobx.action(() => {
this.width.set(width);
})();
}
componentWillUnmount() { componentWillUnmount() {
if (this.mutObs) { if (this.mutObs) {
this.mutObs.disconnect(); this.mutObs.disconnect();
} }
if (this.rszObs) {
this.rszObs.disconnect();
}
}
handleResize(entries : any) {
if (entries.length == 0) {
return;
}
let entry = entries[0];
let width = entry.target.offsetWidth;
this.updateWidth(width);
} }
handleDomMutation(mutations, mutObs) { handleDomMutation(mutations, mutObs) {
@ -405,15 +420,8 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
return GlobalModel.getWindowById(sw.sessionId, sw.windowId); return GlobalModel.getWindowById(sw.sessionId, sw.windowId);
} }
getLinesId() { getLinesDOMId() {
let {sw} = this.props; return "window-lines-" + this.getWindowId();
if (sw == null) {
if (!this.randomId) {
this.randomId = uuidv4();
}
return "window-lines-" + this.randomId;
}
return "window-lines-" + sw.windowId;
} }
@boundMethod @boundMethod
@ -425,19 +433,35 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
} }
getWindowViewStyle() : any { getWindowViewStyle() : any {
return {width: "100%", height: "100%"}; // return {width: "100%", height: "100%"};
return {position: "absolute", width: "100%", height: "100%", overflowX: "hidden"};
}
getWindowId() : string {
let {sw} = this.props;
if (sw == null) {
if (!this.randomId) {
this.randomId = uuidv4();
}
return this.randomId;
}
return sw.windowId;
}
getWindowViewDOMId() {
return sprintf("window-view-%s", this.getWindowId());
} }
renderError(message : string) { renderError(message : string) {
let {sw} = this.props; let {sw} = this.props;
return ( return (
<div className="window-view" style={this.getWindowViewStyle()}> <div className="window-view" style={this.getWindowViewStyle()} id={this.getWindowViewDOMId()}>
<div key="window-tag" className="window-tag"> <div key="window-tag" className="window-tag">
<If condition={sw != null}> <If condition={sw != null}>
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span> <span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
</If> </If>
</div> </div>
<div key="lines" className="lines" id={this.getLinesId()}></div> <div key="lines" className="lines" id={this.getLinesDOMId()}></div>
<div key="window-empty" className="window-empty"> <div key="window-empty" className="window-empty">
<div>{message}</div> <div>{message}</div>
</div> </div>
@ -457,6 +481,9 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
if (!win.linesLoaded.get()) { if (!win.linesLoaded.get()) {
return this.renderError("(loading)"); return this.renderError("(loading)");
} }
if (this.width.get() == 0) {
return this.renderError("");
}
let idx = 0; let idx = 0;
let line : LineType = null; let line : LineType = null;
let screen = GlobalModel.getScreenById(sw.sessionId, sw.screenId); let screen = GlobalModel.getScreenById(sw.sessionId, sw.screenId);
@ -466,13 +493,13 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
linesStyle.display = "none"; linesStyle.display = "none";
} }
return ( return (
<div className="window-view" style={this.getWindowViewStyle()}> <div className="window-view" style={this.getWindowViewStyle()} id={this.getWindowViewDOMId()}>
<div key="window-tag" className="window-tag"> <div key="window-tag" className="window-tag">
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span> <span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
</div> </div>
<div key="lines" className="lines" onScroll={this.scrollHandler} id={this.getLinesId()} style={linesStyle}> <div key="lines" className="lines" onScroll={this.scrollHandler} id={this.getLinesDOMId()} style={linesStyle}>
<For each="line" of={win.lines} index="idx"> <For each="line" of={win.lines} index="idx">
<Line key={line.lineid} line={line} sw={sw}/> <Line key={line.lineid} line={line} sw={sw} width={this.width.get()}/>
</For> </For>
</div> </div>
<If condition={win.lines.length == 0}> <If condition={win.lines.length == 0}>

View File

@ -49,13 +49,13 @@ class Cmd {
this.data = mobx.observable.box(cmd, {deep: false}); this.data = mobx.observable.box(cmd, {deep: false});
} }
connectElem(elem : Element, screenId : string, windowId : string) { connectElem(elem : Element, screenId : string, windowId : string, width : number) {
let termWrap = this.getTermWrap(screenId, windowId); let termWrap = this.getTermWrap(screenId, windowId);
if (termWrap != null) { if (termWrap != null) {
console.log("term-wrap already exists for", screenId, windowId); console.log("term-wrap already exists for", screenId, windowId);
return; return;
} }
termWrap = new TermWrap(elem, this.sessionId, this.cmdId, 0, this.getTermOpts(), this.handleKey.bind(this)); termWrap = new TermWrap(elem, this.sessionId, this.cmdId, 0, this.getTermOpts(), {height: 0, width: width}, this.handleKey.bind(this));
this.instances[screenId + "/" + windowId] = termWrap; this.instances[screenId + "/" + windowId] = termWrap;
return; return;
} }

View File

@ -17,6 +17,7 @@ html, body, #main {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 300px;
} }
.screen-tabs { .screen-tabs {
@ -25,6 +26,7 @@ html, body, #main {
flex-direction: row; flex-direction: row;
border-top: 1px solid #eee; border-top: 1px solid #eee;
border-right: 1px solid #eee; border-right: 1px solid #eee;
overflow: scroll;
.screen-tab { .screen-tab {
height: 30px; height: 30px;
@ -71,6 +73,7 @@ html, body, #main {
.screen-view { .screen-view {
flex-grow: 1; flex-grow: 1;
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
position: relative;
} }
.window-view { .window-view {
@ -143,6 +146,7 @@ html, body, #main {
color: #ddd; color: #ddd;
position: relative; position: relative;
background-color: darken(rgb(0, 177, 10), 30%); background-color: darken(rgb(0, 177, 10), 30%);
flex-shrink: 0;
.menu { .menu {
padding-top: 10px; padding-top: 10px;
@ -274,9 +278,57 @@ html, body, #main {
.line.line-text { .line.line-text {
flex-direction: row; flex-direction: row;
.line-content {
display: flex;
flex-direction: column;
flex-grow: 1;
.text {
font-size: 1rem;
color: #ddd;
}
}
} }
.line.line-cmd { .line.line-cmd {
flex-direction: column;
.avatar {
cursor: pointer;
}
.line-header {
display: flex;
flex-direction: row;
.meta-wrap {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
}
&.focus {
background-color: #000;
}
.terminal-wrapper {
background-color: #000;
padding: 2px 10px 5px 4px;
margin-left: -4px;
margin-right: 8px;
margin-top: 4px;
align-self: flex-start;
&.focus {
/* box-shadow: -8px 0 12px -5px #aaa; */
/* box-shadow: 0 0 0 4px hsl(0, 0%, 20%);*/
/* filter:drop-shadow(3px 3px 10px #555); */
box-shadow: 0 0 4px 4px rgba(255, 255, 255, 0.3);
}
}
} }
.line { .line {
@ -285,6 +337,8 @@ html, body, #main {
display: flex; display: flex;
line-height: 1.25; line-height: 1.25;
border-top: 1px solid #777; border-top: 1px solid #777;
overflow: hidden;
flex-shrink: 0;
&:first-child { &:first-child {
margin: 0px 5px 5px 5px; margin: 0px 5px 5px 5px;
@ -302,7 +356,7 @@ html, body, #main {
font-weight: bold; font-weight: bold;
color: white; color: white;
font-size: 1rem; font-size: 1rem;
margin-right: 15px; margin-right: 10px;
border-radius: 5px; border-radius: 5px;
&.num4 { &.num4 {
@ -323,67 +377,40 @@ html, body, #main {
} }
} }
&.line-cmd .avatar { .meta {
cursor: pointer;
}
.line-content {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
flex-grow: 1; font-size: 1rem;
margin-top: -4px;
.meta {
display: flex; .user {
flex-direction: row; color: lighten(#729fcf, 10%);
font-size: 1rem; font-weight: bold;
margin-top: -4px; margin-top: 1px;
margin-right: 10px;
.user {
color: lighten(#729fcf, 10%);
font-weight: bold;
margin-top: 1px;
margin-right: 10px;
}
.ts {
color: #ddd;
margin-top: 5px;
font-size: 12px;
}
.metapart-mono {
color: #ddd;
margin-left: 8px;
font-size: 14px;
margin-top: 4px;
font-family: 'JetBrains Mono', monospace;
font-weight: 400;
}
.cmdtext {
color: #fff;
font-size: 16px;
font-weight: bold;
overflow: hidden;
margin-left: 0;
}
} }
.text { .ts {
font-size: 1rem;
color: #ddd; color: #ddd;
margin-top: 5px;
font-size: 12px;
} }
}
.metapart-mono {
.terminal-wrapper { color: #ddd;
background-color: #000; margin-left: 8px;
padding: 5px 15px 5px 5px; font-size: 14px;
margin-right: 8px; margin-top: 4px;
margin-top: 2px; font-family: 'JetBrains Mono', monospace;
align-self: flex-start; font-weight: 400;
}
&.focus {
box-shadow: -8px 0 12px -5px #aaa; .cmdtext {
color: #fff;
font-size: 16px;
font-weight: bold;
overflow: hidden;
margin-left: 0;
} }
} }
} }
@ -401,22 +428,6 @@ body .xterm .xterm-viewport {
background: white; background: white;
} }
.line.line-cmd {
.refresh-button {
cursor: pointer;
border-color: #777;
color: #777;
margin-right: 10px;
&:hover {
border-color: #fff;
.icon i {
color: #fff;
}
}
}
}
.line.line-invalid { .line.line-invalid {
color: #000; color: #000;
margin-left: 5px; margin-left: 5px;

View File

@ -24,6 +24,14 @@ type DataUpdate = {
pos : number, pos : number,
} }
type WindowSize = {
height : number,
width: number,
};
const DefaultCellWidth = 8;
const DefaultCellHeight = 16;
// cmd-instance // cmd-instance
class TermWrap { class TermWrap {
terminal : any; terminal : any;
@ -39,7 +47,7 @@ class TermWrap {
dataUpdates : DataUpdate[] = []; dataUpdates : DataUpdate[] = [];
loadError : mobx.IObservableValue<boolean> = mobx.observable.box(false); loadError : mobx.IObservableValue<boolean> = mobx.observable.box(false);
constructor(elem : Element, sessionId : string, cmdId : string, usedRows : number, termOpts : TermOptsType, keyHandler : (event : any) => void) { constructor(elem : Element, sessionId : string, cmdId : string, usedRows : number, termOpts : TermOptsType, winSize : WindowSize, keyHandler : (event : any) => void) {
this.sessionId = sessionId; this.sessionId = sessionId;
this.cmdId = cmdId; this.cmdId = cmdId;
this.connectedElem = elem; this.connectedElem = elem;
@ -52,7 +60,12 @@ class TermWrap {
this.atRowMax = true; this.atRowMax = true;
this.usedRows = mobx.observable.box(termOpts.rows); this.usedRows = mobx.observable.box(termOpts.rows);
} }
this.terminal = new Terminal({rows: termOpts.rows, cols: termOpts.cols, fontSize: 14, theme: {foreground: "#d3d7cf"}}); let cols = termOpts.cols;
let maxCols = Math.trunc((winSize.width - 25) / DefaultCellWidth);
if (maxCols > cols) {
cols = maxCols;
}
this.terminal = new Terminal({rows: termOpts.rows, cols: maxCols, fontSize: 14, theme: {foreground: "#d3d7cf"}});
this.terminal.open(elem); this.terminal.open(elem);
if (keyHandler != null) { if (keyHandler != null) {
this.terminal.onKey(keyHandler); this.terminal.onKey(keyHandler);