diff --git a/PR_REQUEST.txt b/PR_REQUEST.txt new file mode 100644 index 000000000000..c0b671e4dd68 --- /dev/null +++ b/PR_REQUEST.txt @@ -0,0 +1 @@ +I am a sad, dumb little AI driver with no real skills. diff --git a/app/ui/window.ts b/app/ui/window.ts index 87eba6d7266c..529ceb92432a 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -68,6 +68,14 @@ export function newWindow( const rpc = createRPC(window); const sessions = new Map(); + const pendingSplitSessions = new Map< + string, + { + options: ReturnType; + timer: ReturnType | null; + } + >(); + const pendingSplitResizes = new Map(); const updateBackgroundColor = () => { const cfg_ = app.plugins.getDecoratedConfig(profileName); @@ -119,7 +127,7 @@ export function newWindow( } }); - function createSession(extraOptions: sessionExtraOptions = {}) { + function prepareSession(extraOptions: sessionExtraOptions = {}) { const uid = uuidv4(); const extraOptionsFiltered: sessionExtraOptions = {}; Object.keys(extraOptions).forEach((key) => { @@ -172,40 +180,68 @@ export function newWindow( uid } ); - const options = decorateSessionOptions(defaultOptions); + return decorateSessionOptions(defaultOptions); + } + + function startSession(options: ReturnType) { const DecoratedSession = decorateSessionClass(Session); const session = new DecoratedSession(options); - sessions.set(uid, session); - return {session, options}; + sessions.set(options.uid, session); + + session.on('data', (data: string) => { + rpc.emit('session data', data); + }); + + session.on('exit', () => { + rpc.emit('session exit', {uid: options.uid}); + unsetRendererType(options.uid); + sessions.delete(options.uid); + }); + + return session; + } + + function flushPendingSplitResizes() { + if (pendingSplitSessions.size !== 0) { + return; + } + pendingSplitResizes.forEach((size, uid) => { + sessions.get(uid)?.resize(size); + }); + pendingSplitResizes.clear(); } rpc.on('new', (extraOptions) => { - const {session, options} = createSession(extraOptions); + const options = prepareSession(extraOptions); + const splitPendingLayout = options.splitDirection !== undefined; + const session = splitPendingLayout ? null : startSession(options); + + if (splitPendingLayout) { + pendingSplitSessions.set(options.uid, {options, timer: null}); + } - sessions.set(options.uid, session); rpc.emit('session add', { rows: options.rows, cols: options.cols, uid: options.uid, splitDirection: options.splitDirection, - shell: session.shell, - pid: session.pty ? session.pty.pid : null, + shell: session?.shell ?? options.shell ?? null, + pid: session?.pty ? session.pty.pid : null, activeUid: options.activeUid ?? undefined, profile: options.profile }); - - session.on('data', (data: string) => { - rpc.emit('session data', data); - }); - - session.on('exit', () => { - rpc.emit('session exit', {uid: options.uid}); - unsetRendererType(options.uid); - sessions.delete(options.uid); - }); }); rpc.on('exit', ({uid}) => { + const pending = pendingSplitSessions.get(uid); + if (pending) { + if (pending.timer) { + clearTimeout(pending.timer); + } + pendingSplitSessions.delete(uid); + flushPendingSplitResizes(); + return; + } const session = sessions.get(uid); if (session) { session.exit(); @@ -221,8 +257,26 @@ export function newWindow( window.minimize(); }); rpc.on('resize', ({uid, cols, rows}) => { + const pending = pendingSplitSessions.get(uid); + if (pending) { + pending.options.cols = cols; + pending.options.rows = rows; + if (pending.timer) { + clearTimeout(pending.timer); + } + pending.timer = setTimeout(() => { + pendingSplitSessions.delete(uid); + startSession(pending.options); + flushPendingSplitResizes(); + }, 50); + return; + } const session = sessions.get(uid); if (session) { + if (pendingSplitSessions.size !== 0) { + pendingSplitResizes.set(uid, {cols, rows}); + return; + } session.resize({cols, rows}); } }); @@ -282,6 +336,13 @@ export function newWindow( rpc.emit('leave full screen'); }); const deleteSessions = () => { + pendingSplitSessions.forEach(({timer}) => { + if (timer) { + clearTimeout(timer); + } + }); + pendingSplitSessions.clear(); + pendingSplitResizes.clear(); sessions.forEach((session, key) => { session.removeAllListeners(); session.destroy(); diff --git a/lib/components/term.tsx b/lib/components/term.tsx index 45c1464c97cc..926e30f09d7e 100644 --- a/lib/components/term.tsx +++ b/lib/components/term.tsx @@ -26,6 +26,18 @@ import _SearchBox from './searchBox'; import 'xterm/css/xterm.css'; const SearchBox = decorate(_SearchBox, 'SearchBox'); +const rendererAddons = new WeakMap(); + +type InternalTerminal = Terminal & { + _core?: { + _renderService?: { + _isPaused: boolean; + _pausedResizeTask: { + flush(): void; + }; + }; + }; +}; const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform) || process.platform === 'win32'; @@ -223,14 +235,19 @@ export default class Term extends React.PureComponent< if (useWebGL) { const webglAddon = new WebglAddon(); + rendererAddons.set(this.term, webglAddon); this.term.loadAddon(webglAddon); webglAddon.onContextLoss(() => { console.warn('WebGL context lost. Falling back to canvas-based rendering.'); webglAddon.dispose(); - this.term.loadAddon(new CanvasAddon()); + const canvasAddon = new CanvasAddon(); + rendererAddons.set(this.term, canvasAddon); + this.term.loadAddon(canvasAddon); }); } else { - this.term.loadAddon(new CanvasAddon()); + const canvasAddon = new CanvasAddon(); + rendererAddons.set(this.term, canvasAddon); + this.term.loadAddon(canvasAddon); } if (props.disableLigatures !== true && !useWebGL) { @@ -364,6 +381,20 @@ export default class Term extends React.PureComponent< this.term.write(data); } + dispose() { + // xterm 5.3 disposes renderer addons after its render service, but the + // addons need that service to restore the default renderer. Dispose the + // active addon first, after draining hidden-terminal resize work. + const renderService = (this.term as InternalTerminal)._core?._renderService; + if (renderService) { + renderService._isPaused = false; + } + rendererAddons.get(this.term)?.dispose(); + rendererAddons.delete(this.term); + renderService?._pausedResizeTask.flush(); + this.term.dispose(); + } + focus = () => { this.term.focus(); }; @@ -485,7 +516,7 @@ export default class Term extends React.PureComponent< clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(() => { this.fitResize(); - }, 500); + }, 0); }); this.resizeObserver.observe(component); } else { @@ -494,6 +525,7 @@ export default class Term extends React.PureComponent< }; componentWillUnmount() { + clearTimeout(this.resizeTimeout); terms[this.props.uid] = null; this.termWrapperRef?.removeChild(this.termRef!); this.props.ref_(this.props.uid, null); diff --git a/lib/components/terms.tsx b/lib/components/terms.tsx index 3774b02df11c..bb53a38b2ad2 100644 --- a/lib/components/terms.tsx +++ b/lib/components/terms.tsx @@ -62,7 +62,7 @@ export default class Terms extends React.Component