Skip to content

Conversation

paco-sevilla
Copy link

@paco-sevilla paco-sevilla commented Mar 23, 2025

This PR integrates Xterm.js as a new element Xterm. See discussion (#1846).

Use-cases for Xterm in nicegui:

  • Provide a Terminal & Shell for native (electron-like) applications
  • Provide a access to the Shell of a server through a web application, e.g. for a robot (this is the example in this PR, which has obvious security risks!)
  • Display the output of a subprocess containing ANSI escape codes (this is what I'm mostly interested in)

Example

The example connects the new Xterm element to /bin/bash running in a pty.fork() process. This allows using Bash from your browser.

Warning

Exposing a terminal through a server is not secure. Use this example with care.

The page looks as follows:
image

@evnchn
Copy link
Collaborator

evnchn commented Mar 23, 2025

Xterm.js is quite a well-used project at this point.

I have seen it used in Proxmox, as well as various third-party router plugins which provide SSH.

So, I can stand behind including it for NiceGUI.


If I understand correctly, now we need to do npm install to grab xterm right?

Check out npm.json and npm.py to see how other elements officially in NiceGUI currently fetches the dependencies from NPM (though that's up for change later, but should stick until rest of 2.X)

@paco-sevilla
Copy link
Author

@evnchn Thanks for your comment.
I'm aware of those files (npm.py and npm.json). As I wrote above and in #1846, if the authors of NiceGUI think that adding Xterm.js is valuable, I can work on a proper integration: add the module to the npm files, move terminal.py to nicegui/elements, add tests, ...

This PR just adds an example, very similar to the one here: https://github.com/zauberzeug/nicegui/tree/main/examples/signature_pad

@rodja
Copy link
Member

rodja commented Mar 25, 2025

I think xterm.js would be a wonderful (and very powerful) NiceGUI element. But let me discuss it with @falkoschindler first. Might be at the end of the week.

@falkoschindler falkoschindler self-requested a review March 27, 2025 10:22
@falkoschindler
Copy link
Contributor

Thanks for the pull request, @paco-sevilla!

@rodja and I agreed that it would be great to integrate Xterm.js in NiceGUI. For such a useful UI element, it only adds ~300KB of minified JS and a few lines of CSS. (I haven't checked the addons though.)

Instead of ui.terminal, we would prefer the name ui.xterm. For non-Quasar elements we tend to use the original brand name to avoid confusion with similar elements - like ui.aggrid was originally called ui.table, which is now a Quasar table...

So we'd appreciate it very much if you could convert the example into an integrated UI element, either on this PR or a new one. As @evnchn pointed out, we currently manage our dependencies with npm.json and npm.py, which might be a bit tricky to use. Let us know if you need help with anything!

@falkoschindler falkoschindler added feature Type/scope: New feature or enhancement 🟡 medium Priority: Relevant, but not essential in progress Status: Someone is working on it 🌿 intermediate Difficulty: Requires moderate understanding labels Mar 31, 2025
@KiddoV
Copy link

KiddoV commented Apr 10, 2025

+1

@evnchn
Copy link
Collaborator

evnchn commented May 3, 2025

@falkoschindler If you are working on npm.json integration for #4656 (comment), perhaps you can also give this PR a look.

Notably, the files this PR needs:

node_modules/@xterm/xterm/lib/xterm.js
node_modules/@xterm/xterm/css/xterm.css

@evnchn
Copy link
Collaborator

evnchn commented May 10, 2025

Add this to npm.json

  "xterm": {
    "package": "@xterm/xterm",
    "destination": "nicegui/elements/lib",
    "keep": [
      "package/lib/xterm.js",
      "package/css/xterm.css"
    ],
    "rename": {
      "package/lib/": "",
      "package/css/": ""
    },
    "version": "5.5.0"
  }

Shift things into nicegui/elements/, move to using self.add_resource for the CSS, and it should be much more better integrated.

falkoschindler pushed a commit that referenced this pull request May 11, 2025
…4733)

This PR lets npm.py run on Windows as well. It also allows it to be ran
repeatedly without throwing errors

Core changes: 
- Use `tempfile.TemporaryDirectory()`
- `unlink` before `rename` so that the old file is overwritten
- Try-except the failing line `if not any(destination.iterdir()):`
`destination.rmdir()`

---

Admittedly, this is a minor concern, because by all means you should be
on Linux or using a Dev Container, according to the [contributing
guide](https://github.com/zauberzeug/nicegui/blob/42d24d40129785038d6fce9c2aafe24c62482478/CONTRIBUTING.md).

But, both has failed me so far, and so I'm still on Windows. And, I need
`npm.py` to work, so as to help out in #4656 and #4520.

I personally believe that making `npm.py` runnable on cross-platform may
speed up development of new elements, as we lower the barrier to custom
element development.

---

It is best to answer the following before we proceed: 

- [ ] What's the purpose of the `if not any(destination.iterdir()):`
`destination.rmdir()`?
  - It seems to always fail on Windows, so I add a try-except
  - But I don't want to just carelessly do that and call it a day
- [x] Is the output on Linux still working? 
- You may want to grab the `npm.json` with pinned version in #4632, and
test.
- It's the same on my side, minus Tailwind, which they changed stuff
upstream and so we'll never get the exact same copy.
falkoschindler added a commit that referenced this pull request May 19, 2025
This PR addresses the most basic demand highlighted in #2508, that is to
color individual lines in `ui.log`.

#### Implementation details

Note that, the user may have already styled the `ui.label` via
`default_classes`, `default_style` and/or `default_props`.

Since: 
- It would be way too much parameters if we allow 3 (add/remove/replace)
* 3 (classes/style/props) = 9 parameters to skip into `log.push`
- Users are not supposed to receive the `ui.label` instances and style
it themselves since
  - Function returns None, and, 
  - They will be deleted automatically via `self.remove(0)`

I have elected to make the `classes`, `style`, and `props`, should they
be passed in, **replace existing one and be final**.

#### Test script

```py
from nicegui import ui

ui.label.default_classes('text-gray-500')
ui.label.default_style('font-size: 2rem')
ui.label.default_props('my_label_props')

ui.add_css('''
*[my_label_props] {
    font-style: italic;
}
*[my_log_props] {
    font-weight: bold;
}
''')

ui.label('This is how a normal label looks like')

ui.log().push('This is how a log label looks like', classes='text-red-500', style='font-size: 1rem', props='my_log_props')

ui.run()
```

<img width="380" alt="{3B0DEA0A-E006-4A54-950E-58F06FBD5E3D}"
src="https://github.com/user-attachments/assets/f54a8b7e-2ead-4976-8977-9593fc51aee4"
/>

<img width="514" alt="{F0E4BAC4-A13B-494A-A350-7D0F8560B501}"
src="https://github.com/user-attachments/assets/652e594c-b72d-481b-b6b5-ce174ee77401"
/>

#### What this PR doesn't do

- Let you style individual characters in each line
- Let you put custom HTML in `ui.log`
- Accept ANSI escape sequences and display as colors appropriately
- Be a replacement to `Xterm.js` in #4520 

#### To-do

- [x] Pytest? 
- [x] Documentation? 
  - [x] Drafted documentation text

#### Questions left on the table, which is best if we can answer them

- [x] Why no `replace` for `props`, and I had to do
`label.props.clear()`?
  - Considered a missing future feature for NiceGUI

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
@paco-sevilla
Copy link
Author

Sorry that I've been offline for so long... I finally found some time to work on this.
I've added the new element as requested, including tests, and updated the example.

I'll add some documentation/demos to the website in the next few days.
But I'm happy to receive a first pass of comments 🙂

# Install packages
RUN apt-get update && apt-get install --no-install-recommends -y \
sudo git build-essential chromium chromium-driver \
sudo git build-essential chromium chromium-driver chromium-sandbox \
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now required to run tests in the devcontainer.

poetry.lock Outdated
@@ -1,12 +1,11 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poetry only supports Python 3.8 up to version 1.6.1.
A lock file generated with a newer version produces warnings (e.g. in the devcontainer).

However, I'm happy to revert this change, if preferred.

@paco-sevilla paco-sevilla changed the title Add example for Xterm.js connected to Bash Add Xterm element Jul 15, 2025
@falkoschindler falkoschindler added review Status: PR is open and needs review and removed in progress Status: Someone is working on it labels Jul 22, 2025
@evnchn
Copy link
Collaborator

evnchn commented Oct 6, 2025

This PR is so old it witnessed 2 changes of our JS dependency management 😮

@falkoschindler
Copy link
Contributor

Oh I should finally start reviewing it! Dependency management should be even easier now.
I'll move it up in my list. 🤞🏻

@falkoschindler
Copy link
Contributor

Ok, I reviewed most of the code and migrated the dependency management to use a separate package.json and the new esm parameter. I just had to remove the example audio, because the URL wasn't reachable anymore. We either need a different source or host the audio file ourselves.

Now I'm pondering about the purpose of ui.xterm... The first demos are short and simple, but could seem like a poor copy of ui.log or ui.codemirror. The example, on the other hand, is pretty impressive, but also very complex. So I wonder if we could provide a simpler API for connecting ui.xterm to, e.g., a PTY process:

process = await asyncio.create_subprocess_exec(...)
ui.xterm().connect(process)

Or:

pty_pid, pty_fd = pty.fork()
ui.xterm().connect(pty_pid, pty_fd)

Or could process and pty_pid, pty_fd be initializer parameters?

Maybe that's something for a later PR if it doesn't require breaking the API again. But I'd like to hear your thoughts. 🙂

@evnchn
Copy link
Collaborator

evnchn commented Oct 10, 2025

I like the idea of connecting ui.xterm() with something else, but I am expecting an explosion of things we connect to ui.xterm(), and I fear the element becomes super bulky.

Would propose the creation of binding functions which take 2+ arguments, first being the ui.xterm() element, second being whatever you want to bind to.

@paco-sevilla
Copy link
Author

I just had to remove the example audio, because the URL wasn't reachable anymore. We either need a different source or host the audio file ourselves.

I'm still able to download the audio (https://www.soundjay.com/buttons/beep-07a.mp3). It's just a short beeping sound (7kB).
If it's ok for you, we could include it (or something similar) in this repo and in NiceGUI. But I don't think the audio is that important.

Now I'm pondering about the purpose of ui.xterm... The first demos are short and simple, but could seem like a poor copy of ui.log or ui.codemirror.

Yeah, I sort of see your point... What makes Xterm special is actually the parsing of ANSI escape codes (which is not trivial).
So, I think most use cases of the Xterm element will involve a process/command running in the background (either a subprocess or a pty process). However, there are some caveats...

Caveats for subprocesses:

We could add a method like run_subprocess to the element, which takes the same args as asyncio.create_subprocess_exec but with stdout and stderr set to subrpocess.PIPE by default. The logic would be very similar to the last demo in this PR.

The problem is that most commands detect that stdout/stderr are not tty's and don't print outputs with ANSI codes, which makes having Xterm a bit useless (you might as well print the output to ui.log). This might surprise some users.
Many commands have a --color flag (like ls --color=always) to at least print the ANSI codes for text formatting, but they will still not print something like a progress bar.

Therefore, you actually want to use a PTY in most cases. But...

Caveats for pty:

The pty module in the standard library is very basic (and has an awkward API). If you want to —for example— resize the terminal, you'll probably want to use a third-party package like ptyprocess or shellous.

Even worse, all of the above only work on Unix systems. If you're on Windows, you'll need to use a different package: pywinpty. To my knowledge, there is no cross-platform solution out there (see also python/cpython#85829).

Final thoughts

This comment is already quite long 🙈 My proposal is:

  • If it's acceptable to have different behavior on Windows vs. Unix:
    We can add a run_subprocess method that takes the same args as asyncio.create_subprocess_exec but by default sets stdout and stderr (and stdin?) to subrpocess.PIPE on Windows and to a PTY file descriptor that we get from pty.openpty on Unix. Instead of a method, it could also be a separate Process class that ui.xterm can .bind() to.

  • Otherwise:
    We keep the API minimalistic and let the user decide how to connect to the Xterm element.

In any case, I think the work should happen in a separate PR. The current API shouldn't be affected.

@falkoschindler falkoschindler added this to the 3.1 milestone Oct 15, 2025
@falkoschindler
Copy link
Contributor

Ok, thanks a lot for the detailed explanation, @paco-sevilla!
You've convinced me that a subprocess or pty integration is non-trivial and that we shouldn't squeeze it into this PR. I'll finish my review and try to get it ready for version 3.1. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Type/scope: New feature or enhancement 🌿 intermediate Difficulty: Requires moderate understanding 🟡 medium Priority: Relevant, but not essential review Status: PR is open and needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants