holzschu / a-shell

A terminal for iOS, with multiple windows
BSD 3-Clause "New" or "Revised" License
2.65k stars 116 forks source link

REGRESSION: Textual apps stopped working! 🙀 #817

Open Emasoft opened 1 month ago

Emasoft commented 1 month ago

I have not used textual apps in a-Shell since last year, but they always worked fine. But now (a-Shell v. 1.15) those apps do not even start. What happened? Just install Textual ( https://textual.textualize.io/getting_started/ ) and you'll see.

You can test any example from the textual repo:

https://github.com/Textualize/textual/tree/main/examples

They used to work fine, but now they do not work anymore.

You can also use this mini example to reproduce the issue:

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Footer

class FooterApp(App):
    BINDINGS = [
        Binding(key="q", action="quit", description="Quit the app"),
        Binding(
            key="question_mark",
            action="help",
            description="Show help screen",
            key_display="?",
        ),
        Binding(key="delete", action="delete", description="Delete the thing"),
        Binding(key="j", action="down", description="Scroll down", show=False),
    ]

    def compose(self) -> ComposeResult:
        yield Footer()

if __name__ == "__main__":
    app = FooterApp()
    app.run()

Please fix this! 🙏

Emasoft commented 1 week ago

I really beg you to fix this issue with Textual. 🙏😩🧎🏻‍➡️ Textual worked in the past, so it should be possible to fix this, right? Please, this is making using a-Shell a much worse experience.

holzschu commented 1 week ago

I ran a few tests, after upgrading textual and the examples directory. calculator.py still works, but the other commands still don't launch. A quick analysis shows that the difference is that calculator.py ends with CalculatorApp.run(inline=True), while the others end with app.run().

If I change the app.run() into app.run(inline=True), the other commands are working again. I assume Textual is trying to start a new terminal or a new window, which doesn't work here.

rcarmo commented 1 week ago

Hmmm. Came here to report a similar thing, but never figured out the inline part - will try!

Emasoft commented 1 week ago

@holzschu No, unfortunately the 'inline' mode is NOT equivalent to the normal mode.
The app looks the same, but all the mouse/touch interactions are disabled or maimed. In normal mode Terminal uses the terminal full screen mode (ANSI 'alt buffer screen'), a standard VT100 mode that is necessary to go FULL SCREEN and to use most of the interactive features, like mouse/touch drag scroll to read a document, drag a slider, drag&drop, resize a frame, pull down a drop-down menu, double-click, etc.

IMG_3486

In inline mode those features are not working because most of the mouse/touch events are captured by the terminal screen instead, like: drag-scroll the terminal buffer, select the text, etc. So in inline mode none of those mouse/touch events arrives at the textual app. You cannot even drag a scrollbar to read a text. This is why the inline mode is almost used only for Textual script for prompt autocompletion, quick menu choices or to render a progress bar, but never in real Textual apps. In fact almost all Textual app I installed for my work use the full screen mode, not the inline mode! And even if I hack the sources and try to force them in inline mode, they are unusable!!

Textual has indeed two separate 'drivers' for that:

You can see from the diff the different ANSI commands sent to the terminal:

https://editor.mergely.com/vPcAE2fm

It is very important to note that:

holzschu commented 6 days ago

I've looked into the problem. It's not going to be easy to fix: "inline" apps are not sending anything to the standard output. They're not sending the character codes to switch to alt-screen mode. They stop, for some reason, before they do anything.

Emasoft commented 5 days ago

@holzschu But that is exactly the intended behavior. Inline apps are using the normal screen mode, not the alternate screen. It is the alt-screen mode that makes a-Shell hang forever when launching a Textual app without the inline=True setting. Just look at the python code inside the driver classes I posted above. ANSI commands are sent, but they are different between the two modes.

Textual setting VT-100 mode Full Screen Interaction Works in a Shell?
Textual "inline=True" alt-screen buffer OFF (default) NO Mouse drag events captured by terminal. Good for small apps like autocomplete, progress bars, etc. YES
Textual "inline=False" (default) alt-screen buffer ON YES Mouse drag events captured by Textual UI. Good for real apps like text editors, etc. NO (but it worked before!)
holzschu commented 5 days ago

I reiterate what I just said: Textual apps running in non-inline mode are not sending anything. I have access to the debugger, I set a breakpoint in all the functions that send any kind of output to anywhere. They are never called. It's not an issue with how a-Shell deals with control characters to switch to the alt-screen, it's an issue with Textual apps not arriving at the point where they send this control character. That makes it considerably harder to fix. That was just for your information: there is no easy fix in sight.

Emasoft commented 5 days ago

Textual apps running in non-inline mode are not sending anything.

Are you saying that this function is never called?


    def start_application_mode(self):
        """Start application mode."""

        def _stop_again(*_) -> None:
            """Signal handler that will put the application back to sleep."""
            os.kill(os.getpid(), signal.SIGSTOP)

        # If we're working with an actual tty...
        # https://github.com/Textualize/textual/issues/4104
        if os.isatty(self.fileno):
            # Set up handlers to ensure that, if there's a SIGTTOU or a SIGTTIN,
            # we go back to sleep.
            signal.signal(signal.SIGTTOU, _stop_again)
            signal.signal(signal.SIGTTIN, _stop_again)
            try:
                # Here we perform a NOP tcsetattr. The reason for this is
                # that, if we're suspended and the user has performed a `bg`
                # in the shell, we'll SIGCONT *but* we won't be allowed to
                # do terminal output; so rather than get into the business
                # of spinning up application mode again and then finding
                # out, we perform a no-consequence change and detect the
                # problem right away.
                termios.tcsetattr(
                    self.fileno, termios.TCSANOW, termios.tcgetattr(self.fileno)
                )
            except termios.error:
                # There was an error doing the tcsetattr; there is no sense
                # in carrying on because we'll be doing a SIGSTOP (see
                # above).
                return
            finally:
                # We don't need to be hooking SIGTTOU or SIGTTIN any more.
                signal.signal(signal.SIGTTOU, signal.SIG_DFL)
                signal.signal(signal.SIGTTIN, signal.SIG_DFL)

        loop = asyncio.get_running_loop()

        def send_size_event() -> None:
            terminal_size = self._get_terminal_size()
            width, height = terminal_size
            textual_size = Size(width, height)
            event = events.Resize(textual_size, textual_size)
            asyncio.run_coroutine_threadsafe(
                self._app._post_message(event),
                loop=loop,
            )

        self._writer_thread = WriterThread(self._file)
        self._writer_thread.start()

        def on_terminal_resize(signum, stack) -> None:
            send_size_event()

        signal.signal(signal.SIGWINCH, on_terminal_resize)

        self.write("\x1b[?1049h")  # Alt screen

        self._enable_mouse_support()
        try:
            self.attrs_before = termios.tcgetattr(self.fileno)
        except termios.error:
            # Ignore attribute errors.
            self.attrs_before = None

        try:
            newattr = termios.tcgetattr(self.fileno)
        except termios.error:
            pass
        else:
            newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG])
            newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG])

            # VMIN defines the number of characters read at a time in
            # non-canonical mode. It seems to default to 1 on Linux, but on
            # Solaris and derived operating systems it defaults to 4. (This is
            # because the VMIN slot is the same as the VEOF slot, which
            # defaults to ASCII EOT = Ctrl-D = 4.)
            newattr[tty.CC][termios.VMIN] = 1

            try:
                termios.tcsetattr(self.fileno, termios.TCSANOW, newattr)
            except termios.error:
                pass

        self.write("\x1b[?25l")  # Hide cursor
        self.write("\x1b[?1004h")  # Enable FocusIn/FocusOut.
        self.write("\x1b[>1u")  # https://sw.kovidgoyal.net/kitty/keyboard-protocol/
        # Disambiguate escape codes https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
        self.write("\x1b[=1;u")
        self.flush()
        self._key_thread = Thread(target=self._run_input_thread)
        send_size_event()
        self._key_thread.start()
        self._request_terminal_sync_mode_support()
        self._enable_bracketed_paste()

        # Appears to fix an issue enabling mouse support in iTerm 3.5.0
        self._enable_mouse_support()

        # If we need to ask the app to signal that we've come back from a
        # SIGTSTP...
        if self._must_signal_resume:
            self._must_signal_resume = False
            asyncio.run_coroutine_threadsafe(
                self._app._post_message(self.SignalResume()),
                loop=loop,
            )

If so, then this is a Textual bug! You should report to them the issue, opening a bug report: https://github.com/Textualize/textual/issues

holzschu commented 4 days ago

Indeed, it either stops just before calling this function, or very soon after entering.

I don't agree with you that this is a Textual bug: a-Shell does a lot of things to emulate Unix functions that do not exist on iOS. One of these might have an uninteded side effects with what Textual does. That's why I need to investigate.

Emasoft commented 4 days ago

@holzschu I think it would be in the best interest of the Textual devs to support a-Shell as a platform. Even if it is not a bug by their part, they should collaborate with you in finding the cause. After all a-Shell is practically their dream target platform. a-Shell runs on touch devices with no keyboard, where the CLI would not be as useful and practical as an UI, but because there is no GUI option for programs in a-ShelI, a TUI like Textual is the only choice. By the way, I tested Textual on two ssh terminal iOS apps: WebSSH and ShellFish. Both works fine with Textual in non-inline mode. Looks like the issue must be caused by something unique of a-Shell.