tl;dr I've created a React wrapper to render an array of log messages into a terminal but resizing is giving a weird output (see screenshot). (There is a React-Wrapper on NPM but that wasn't working for my use-case - caused screen flickering)
I'm working on a feature for Guppy where I'm adding Xterm.js for the terminal output. The PR can be found here.
I've added xterm because of hyperlink scanning/parsing and that is working.
But I'm stuck with getting resize to work. If I'm starting the devServer in the app and wait for some text it will display with correct letter width. If I reduce the size I'm getting an output with an incorrect letter width. Like in the following screenshot:
It is always looking correct in the not resized state but after resize it will get the wrong display - so this will happen for enlarging & shrinking the screen width.
The output should look similar to the following screenshot (maybe with some wrapped lines):
I think this is caused by Fit addon or the way I'm handling resizing with the resize observer but I'm not sure.
The span style of xterm letter are getting a width of NaNpx
like in the following screenshot:
Is this caused by a media query I'm using? I haven't seen that yet maybe I have to temporarily disable all media queries to see if that's causing the behaviour.
What I have tried so far:
this.xterm.fit()
into a setTimeout(func, 0)
but with-out an effectThe code I'm using can be found on Github branch feature-terminal-links but here I'd like to extract the parts I've added to get Xterm to work with React:
XtermContainer
as a div so I can add Xterm styles and own styling. The following code is inside render
and will be our xterm.js container (innerRef
will be used later in ComponentDidMount
to intialize Xterm with that container):<XtermContainer
width={width}
height={height}
innerRef={node => (this.node = node)}
/>
componentDidMount
with the container above:componentDidMount() {
Terminal.applyAddon(webLinks);
Terminal.applyAddon(localLinks);
Terminal.applyAddon(fit);
this.xterm = new Terminal({
convertEol: true,
fontFamily: `'Fira Mono', monospace`,
fontSize: 15,
rendererType: 'dom', // default is canvas
});
this.xterm.setOption('theme', {
background: COLORS.blue[900],
foreground: COLORS.white,
});
this.xterm.open(this.node);
this.xterm.fit();
/* ... some addon setup code here (not relevant for the problem) ... */
}
this.xterm.fit()
if the size changes (in the repo there is a setTimeout
wrapper around for testing).<ResizeObserver onResize={() => this.xterm && this.xterm.fit()} />
componentDidUpdate(prevProps, prevState)
to update the terminal and scroll the terminal to the bottom if the component is getting new logs:componentDidUpdate(prevProps, prevState) {
if (prevProps.task.logs !== this.state.logs) {
if (this.state.logs.length === 0) {
this.xterm.clear();
}
for (const log of this.state.logs) {
/*
We need to track what we have added to xterm - feels hacky but it's working.
`this.xterm.clear()` and re-render everything caused screen flicker that's why I decided to not use it.
Todo: Check if there is a react-xterm wrapper that is not using xterm.clear or
create a wrapper component that can render the logs array (with-out flicker).
*/
if (!this.renderedLogs[log.id]) {
this.writeln(log.text);
this.xterm.scrollToBottom();
this.renderedLogs[log.id] = true;
}
}
}
}
Ideas I have to find the cause:
Update
OK, the resize obeserver is probably not needed as I'm getting the same behaviour after commenting out the <ResizeObserver/>
in render. So I think it's caused by xterm.js or the css in Guppy.
I have a fix for the issue. It's now working in the above mentioned feature branch. Not sure if there is a better solution but it's working for me.
I like to explain how I have fixed the resizing issue:
The problem was the OnlyOn
component that was used in DevelopmentServerPane
. It always rendered two TerminalOutput
components. One terminal was hidden with display: none
and the other was displayed with display: inline
- the style change was handled with a media query inside a styled-component.
After replacing OnlyOn
with React-responsive and using the render props to check mdMin
breakpoint it was working as expected. React-responsive is removing the not displayed mediaquery component from DOM so only one terminal in DOM at the same time.
I still don't know why there was a problem with the letter width but probably the two instances collided somehow. I couldn't create a minimal reproduction. I tried to recreate the issue in this Codesandbox but I have only resized one Terminal at a time and so I haven't got the issue there.
The code that fixed the problem (simplified version from the above mentioned repo):
import MediaQuery from 'react-responsive';
const BREAKPOINT_SIZES = {
sm: 900,
};
const BREAKPOINTS = {
mdMin: `(min-width: ${BREAKPOINT_SIZES.sm + 1}px)`,
};
const DevelopmentServerPane = () => (
<MediaQuery query={BREAKPOINTS['mdMin']}>
{matches =>
matches ? (
<div>{/* ... render Terminal for matching mdMin and above */}</div>
) : (
<div> {/* ... render Terminal for small screens */}</div>
)
}
</MediaQuery>
);