Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typing in the character 'f' displays an extra space #5153

Closed
Alex-Bishka opened this issue Sep 10, 2024 · 7 comments
Closed

Typing in the character 'f' displays an extra space #5153

Alex-Bishka opened this issue Sep 10, 2024 · 7 comments
Labels
needs more info type/question A question on how to use the library

Comments

@Alex-Bishka
Copy link

When I type the character 'f' into the terminal it displays 'f '. So, when I type 'asdfh', for example, I expect to see 'asdfh'. Unfortunately, I instead see 'asdf h'.

However, when console logging the 'f' character before it is written, I do not see the extra space. I also tried hard coding the write, via terminal.current?.write('f') and the same issue occurred. I am logging the buffer below and it does not show any spaces b/w 'f' and characters that come after 'f'. I also logged the following console.log(key.includes('\x00')), which appears as false for 'f'. I cannot find any null characters here that might cause this issue.

This is only happening to me with the letter 'f' so far.

Here is the code I am working with:

import React, { useEffect, useRef } from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';


const runCommandHandler = async (command: string) => {
    try {
        const response = await fetch('http://localhost:5000/run-command', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ command }),
        });
        const data = await response.json();
        return data.result;
    } catch (error) {
        console.error('Error running command:', error);
        return `Command ${command} not recognized`;
    }
};

const TerminalContent: React.FC = () => {
    const terminalRef = useRef<HTMLDivElement>(null);
    const terminal = useRef<Terminal | null>(null);
    const commandRef = useRef<string>('');

    useEffect(() => {
        if (terminalRef.current) {
            // Initialize xterm.js
            terminal.current = new Terminal({
                convertEol: true, // Convert End Of Line characters to new lines
                disableStdin: false, // Enable input handling
            });
            terminal.current.open(terminalRef.current);
            terminal.current.write("Welcome to Alex's terminal!\r\n\n$ ");

            const handleKeyInput = async ({ key, domEvent }: { key: string; domEvent: KeyboardEvent }) => {
                switch (domEvent.key) {
                    case 'Enter':
                        // console.log('Command submitted:', commandRef.current);
                        terminal.current?.write('\r\n'); // New line on enter
                        
                        const result = await runCommandHandler(commandRef.current);
                        terminal.current?.write(`${result}\r\n`);

                        commandRef.current = '';
                        terminal.current?.write('$ ');
                        break;
                    case 'Backspace':
                        const cursorX = terminal.current?.buffer?.active?.cursorX;
                        if (cursorX && cursorX > 2) {
                            terminal.current?.write('\b \b');
                        }
                        break;
                    default:
                        commandRef.current += key;
                        terminal.current?.write(key); // Write any other key to terminal
                        // this is where  terminal.current?.write('f'); also causes the same behavior
                        break;
                }

                const buffer = terminal.current?.buffer.active;
                if (buffer) {
                    console.log("buffer")
                    console.log(buffer)
                    for (let i = 0; i < buffer.length; i++) {
                        console.log(buffer.getLine(i)?.translateToString(true));
                    }
                    console.log("end buffer")
                }
            };

            terminal.current.onKey(handleKeyInput);
            terminal.current?.onData(data => {console.log(`data: '${data}'`)})
        }
  
      return () => {
        console.log("dispose called")
        terminal.current?.dispose();
      };
    }, []);

    return (
      <div>
        <div
          ref={terminalRef}
          style={{
            width: '100%',
            whiteSpace: 'nowrap'
          }}
        ></div>
      </div>
    );
}

export default TerminalContent;

Details

  • Browser and browser version: Mozilla Firefox 130.0
  • OS version: Ubuntu 22.04.4 LTS
  • xterm.js version: ^5.3.0

Also note that I have the following versions for react/next:

"next": "13.4.12",
"react": "18.2.0",
"react-dom": "18.2.0",

Steps to reproduce

  1. Run the code above from a nextjs project
  2. type f into the terminal
@jerch
Copy link
Member

jerch commented Sep 10, 2024

I have literally the same OS and browser here, but cannot repro with our demo.

My guess is, that somehow your project setup makes things up. Isnt nextjs used for serverside rendering? If you use serverside rendering - this never gonna work properly with xtermjs, unless those serverside DOM shims implement an almost fully working DOM abstraction.

@Alex-Bishka
Copy link
Author

Alex-Bishka commented Sep 10, 2024

I'm skeptical it's a nextjs thing because I'm using client side code to render it with UseEffect.

That being said I'll be doing some further testing on this to confirm, especially looking into hydration. I'll also test on a vite react app (I should really have done this instead of next since I'm not really utilizing the server side rendering features of next) to see if I get the same effect.

Will post updates as I get through these tests.

@Alex-Bishka
Copy link
Author

Alex-Bishka commented Sep 10, 2024

Same code fails in a react vite app.

For reproduction, I did the following steps:

  1. npm create vite@latest
  2. select React for framework
  3. select TypeScript for variant
  4. npm install xterm
  5. updated App.tsx to the code below
  6. npm run dev
  7. typed 'f' into the terminal content window and 'f ' appeared

Code for App.tsx:

import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'


import React, { useEffect, useRef } from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';


const runCommandHandler = async (command: string) => {
    try {
        const response = await fetch('http://localhost:5000/run-command', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ command }),
        });
        const data = await response.json();
        return data.result;
    } catch (error) {
        console.error('Error running command:', error);
        return `Command ${command} not recognized`;
    }
};

const TerminalContent: React.FC = () => {
    const terminalRef = useRef<HTMLDivElement>(null);
    const terminal = useRef<Terminal | null>(null);
    const commandRef = useRef<string>('');

    useEffect(() => {
        if (terminalRef.current) {
            // Initialize xterm.js
            terminal.current = new Terminal({
                convertEol: true, // Convert End Of Line characters to new lines
                disableStdin: false, // Enable input handling
            });
            terminal.current.open(terminalRef.current);
            terminal.current.write("Welcome to Alex's terminal!\r\n\n$ ");

            const handleKeyInput = async ({ key, domEvent }: { key: string; domEvent: KeyboardEvent }) => {
                switch (domEvent.key) {
                    case 'Enter':
                        // console.log('Command submitted:', commandRef.current);
                        terminal.current?.write('\r\n'); // New line on enter
                        
                        if (commandRef.current) {
                            const result = await runCommandHandler(commandRef.current);
                            terminal.current?.write(`${result}\r\n`);
                        }

                        commandRef.current = '';
                        terminal.current?.write('$ ');
                        break;
                    case 'Backspace':
                        const cursorX = terminal.current?.buffer?.active?.cursorX;
                        if (cursorX && cursorX > 2) {
                            terminal.current?.write('\b \b');
                            if (commandRef.current.length > 0) {
                                commandRef.current = commandRef.current.slice(0, commandRef.current.length - 1)
                            }
                        }
                        break;
                    default:
                        commandRef.current += key;
                        terminal.current?.write(key); // Write any other key to terminal
                        break;
                }
                console.log(terminal.current?.buffer.active)
            };

            terminal.current.onKey(handleKeyInput);
        }
  
      return () => {
        console.log("dispose called")
        terminal.current?.dispose();
      };
    }, []);

    return (
      <div>
        <div
          ref={terminalRef}
          style={{
            width: '100%',
            whiteSpace: 'nowrap'
          }}
        ></div>
      </div>
    );
}

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <h1>Terminal App</h1>
        <TerminalContent/>
      </div>
    </>
  )
}

export default App

Same result occurs. Only happens to the character 'f' (not even 'F'). I feel like this test probably means that it's not a result of the other packages I had in my nextjs project or next itself. It could also be I am not properly writing to the terminal - I just picked up this library, so it's very possible there's just something wrong with my code above.

I will still look into hydration to be extra sure, but to me this is looking like something going wrong with xterm. Unless React framework variants aren't supported?

Edit:
Also for extra info, the node version I am running is v20.14.0.

@Alex-Bishka
Copy link
Author

Alex-Bishka commented Sep 10, 2024

Yup, so in my nextjs project I am can confirm I am using client side rendering as my page.tsx file (which is the parent of all components) utilizes client side code like UseState and UseEffect. Furthermore, I have marked this file with 'use client'. If I remove this marking, I am met with the following error:

Failed to compile

./src/app/page.tsx
ReactServerComponentsError:

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: https://nextjs.org/docs/getting-started/react-essentials

   ,-[/mnt/SharedData/Projects/PersonalWebsite/src/app/page.tsx:1:1]
 1 | // 'use client'
 2 | import React, { useRef, useEffect, useState } from 'react';
   :                                    ^^^^^^^^
 3 | import {
 4 |   AppBar,
 5 |   Button,
   `----

Maybe one of these should be marked as a client entry with "use client":
  ./src/app/page.tsx

This error occurred during the build process and can only be dismissed by fixing the error.

And from my understanding of nextjs, this means that no server side rendering is taking place. So, ignore my hydration point earlier, as that's not even occurring here.

@jerch
Copy link
Member

jerch commented Sep 10, 2024

Can you check on xtermjs.org, if it happens there too for you? Also maybe use newest version of xtermjs, which is 5.5.0.

Beside that - I have no clue, whats going on. Can you step-debug things from 'f' input down to terminal output?

@Alex-Bishka
Copy link
Author

The strange behavior does not happen on the site for me.

I'll try updating the version and see what happens. If that's still not working I can looking into step-debugging.

@Tyriar
Copy link
Member

Tyriar commented Sep 10, 2024

We're not experts in react so if you can't provide a repro case using the plain API it's not very actionable on our end. Chances are, like in the past with similar issues, this is probably related to one of the framework lifecycle events messing with things.

@Tyriar Tyriar closed this as completed Sep 10, 2024
@Tyriar Tyriar added the type/question A question on how to use the library label Sep 10, 2024
@xtermjs xtermjs locked and limited conversation to collaborators Sep 10, 2024
@Tyriar Tyriar converted this issue into discussion #5155 Sep 10, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
needs more info type/question A question on how to use the library
Projects
None yet
Development

No branches or pull requests

3 participants