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);
}
@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 {
let lineDate = new Date(ts);
let nowDate = new Date();
@ -76,7 +62,7 @@ class LineText extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
}
@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);
constructor(props) {
@ -89,7 +75,7 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
let cmd = model.getCmd(line);
if (cmd != null) {
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))();
}
}
@ -181,6 +167,8 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
}
let termLoaded = this.termLoaded.get();
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 totalHeight = cellHeightPx * usedRows;
let remote = model.getRemote(cmd.remoteId);
@ -190,33 +178,36 @@ class LineCmd extends React.Component<{sw : ScreenWindow, line : LineType}, {}>
let termOpts = cmd.getTermOpts();
let isFocused = cmd.getIsFocused(sw.screenId, sw.windowId);
return (
<div className="line line-cmd" id={"line-" + getLineId(line)}>
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})} onClick={this.doRefresh}>
{lineid}
</div>
<div className="line-content">
<div className="meta">
<div className="user" style={{display: "none"}}>{line.userid}</div>
<div className="ts">{formattedTime}</div>
<div className={cn("line", "line-cmd", {"focus": isFocused})} id={"line-" + getLineId(line)}>
<div className="line-header">
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})} onClick={this.doRefresh}>
{lineid}
</div>
<div className="meta">
<div className="metapart-mono" style={{display: "none"}}>
{line.cmdid}
({termOpts.rows}x{termOpts.cols})
<div className="meta-wrap">
<div className="meta">
<div className="user" style={{display: "none"}}>{line.userid}</div>
<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>
{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 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>
);
}
}
@mobxReact.observer
class Line extends React.Component<{sw : ScreenWindow, line : LineType}, {}> {
class Line extends React.Component<{sw : ScreenWindow, line : LineType, width : number}, {}> {
render() {
let line = this.props.line;
if (line.linetype == "text") {
@ -350,10 +341,12 @@ class CmdInput extends React.Component<{}, {}> {
@mobxReact.observer
class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
mutObs : any;
rszObs : any
randomId : string;
width : mobx.IObservableValue<number> = mobx.observable.box(0);
scrollToBottom(reason : string) {
let elem = document.getElementById(this.getLinesId());
let elem = document.getElementById(this.getLinesDOMId());
if (elem == null) {
return;
}
@ -374,23 +367,45 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
}
componentDidMount() {
let elem = document.getElementById(this.getLinesId());
if (elem == null) {
return;
let elem = document.getElementById(this.getLinesDOMId());
if (elem != null) {
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));
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);
let wvElem = document.getElementById(this.getWindowViewDOMId());
if (wvElem != null) {
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
this.rszObs.observe(wvElem);
}
}
updateWidth(width : number) {
mobx.action(() => {
this.width.set(width);
})();
}
componentWillUnmount() {
if (this.mutObs) {
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) {
@ -405,15 +420,8 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
return GlobalModel.getWindowById(sw.sessionId, sw.windowId);
}
getLinesId() {
let {sw} = this.props;
if (sw == null) {
if (!this.randomId) {
this.randomId = uuidv4();
}
return "window-lines-" + this.randomId;
}
return "window-lines-" + sw.windowId;
getLinesDOMId() {
return "window-lines-" + this.getWindowId();
}
@boundMethod
@ -425,19 +433,35 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
}
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) {
let {sw} = this.props;
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">
<If condition={sw != null}>
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
</If>
</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>{message}</div>
</div>
@ -457,6 +481,9 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
if (!win.linesLoaded.get()) {
return this.renderError("(loading)");
}
if (this.width.get() == 0) {
return this.renderError("");
}
let idx = 0;
let line : LineType = null;
let screen = GlobalModel.getScreenById(sw.sessionId, sw.screenId);
@ -466,13 +493,13 @@ class ScreenWindowView extends React.Component<{sw : ScreenWindow}, {}> {
linesStyle.display = "none";
}
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">
<span>{sw.name.get()}{sw.shouldFollow.get() ? "*" : ""}</span>
</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">
<Line key={line.lineid} line={line} sw={sw}/>
<Line key={line.lineid} line={line} sw={sw} width={this.width.get()}/>
</For>
</div>
<If condition={win.lines.length == 0}>

View File

@ -49,13 +49,13 @@ class Cmd {
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);
if (termWrap != null) {
console.log("term-wrap already exists for", screenId, windowId);
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;
return;
}

View File

@ -17,6 +17,7 @@ html, body, #main {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 300px;
}
.screen-tabs {
@ -25,6 +26,7 @@ html, body, #main {
flex-direction: row;
border-top: 1px solid #eee;
border-right: 1px solid #eee;
overflow: scroll;
.screen-tab {
height: 30px;
@ -71,6 +73,7 @@ html, body, #main {
.screen-view {
flex-grow: 1;
border-right: 1px solid #ccc;
position: relative;
}
.window-view {
@ -143,6 +146,7 @@ html, body, #main {
color: #ddd;
position: relative;
background-color: darken(rgb(0, 177, 10), 30%);
flex-shrink: 0;
.menu {
padding-top: 10px;
@ -274,9 +278,57 @@ html, body, #main {
.line.line-text {
flex-direction: row;
.line-content {
display: flex;
flex-direction: column;
flex-grow: 1;
.text {
font-size: 1rem;
color: #ddd;
}
}
}
.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 {
@ -285,6 +337,8 @@ html, body, #main {
display: flex;
line-height: 1.25;
border-top: 1px solid #777;
overflow: hidden;
flex-shrink: 0;
&:first-child {
margin: 0px 5px 5px 5px;
@ -302,7 +356,7 @@ html, body, #main {
font-weight: bold;
color: white;
font-size: 1rem;
margin-right: 15px;
margin-right: 10px;
border-radius: 5px;
&.num4 {
@ -323,67 +377,40 @@ html, body, #main {
}
}
&.line-cmd .avatar {
cursor: pointer;
}
.line-content {
.meta {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-direction: row;
font-size: 1rem;
margin-top: -4px;
.meta {
display: flex;
flex-direction: row;
font-size: 1rem;
margin-top: -4px;
.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;
}
.user {
color: lighten(#729fcf, 10%);
font-weight: bold;
margin-top: 1px;
margin-right: 10px;
}
.text {
font-size: 1rem;
.ts {
color: #ddd;
margin-top: 5px;
font-size: 12px;
}
}
.terminal-wrapper {
background-color: #000;
padding: 5px 15px 5px 5px;
margin-right: 8px;
margin-top: 2px;
align-self: flex-start;
.metapart-mono {
color: #ddd;
margin-left: 8px;
font-size: 14px;
margin-top: 4px;
font-family: 'JetBrains Mono', monospace;
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;
}
.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 {
color: #000;
margin-left: 5px;

View File

@ -24,6 +24,14 @@ type DataUpdate = {
pos : number,
}
type WindowSize = {
height : number,
width: number,
};
const DefaultCellWidth = 8;
const DefaultCellHeight = 16;
// cmd-instance
class TermWrap {
terminal : any;
@ -39,7 +47,7 @@ class TermWrap {
dataUpdates : DataUpdate[] = [];
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.cmdId = cmdId;
this.connectedElem = elem;
@ -52,7 +60,12 @@ class TermWrap {
this.atRowMax = true;
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);
if (keyHandler != null) {
this.terminal.onKey(keyHandler);