Gui.cs: Minimal PowerShell Sample

Created on 2 Oct 2020  路  21Comments  路  Source: migueldeicaza/gui.cs

using namespace Terminal.Gui

# Load the Terminal.Gui assembly and its dependencies (assumed to be in the
# the same directory).
# NOTE: `using assmembly <path>` seemingly only works with full, literal paths
# as of PowerShell Core 7.1.0-preview.7.
# The assumption here is that all relevant DLLs are stored in subfolder
# assemblies/bin/Release/*/publish of the script directory, as shown in 
#   https://stackoverflow.com/a/50004706/45375
#Add-Type -Path $PSScriptRoot/assemblies/bin/Release/*/publish/Terminal.Gui.dll

# Initialize the "GUI".
# Note: This must come before creating windows and controls.
[Application]::Init()

$win = [Window] @{
  Title = 'Hello World'
}

$edt = [TextField] @{
    X = [Pos]::Center()
    Y = [Pos]::Center()
    Width = 20
    Text = 'This text will be returned'
}
$win.Add($edt)

$btn = [Button] @{
  X = [Pos]::Center()
  Y = [Pos]::Center() + 1
  Text = 'Quit'
}
$win.Add($btn)
[Application]::Top.Add($win)

# Attach an event handler to the button.
# Note: Register-ObjectEvent -Action is NOT an option, because
# the [Application]::Run() method used to display the window is blocking.
$btn.add_Clicked({
  # Close the modal window.
  # This call is also necessary to stop printing garbage in response to mouse
  # movements later.
  [Application]::RequestStop()
})

# Show the window (takes over the whole screen). 
# Note: This is a blocking call.
[Application]::Run()

# As of 1.0.0-pre.4, at least on macOS, the following two statements
# are necessary on in order for the terminal to behave properly again.
[Application]::Shutdown() # Clears the screen too; required for being able to rerun the application in the same session.
#tput reset # To make PSReadLine work properly again, notably up- and down-arrow.
$edt.Text.ToString()
enhancement

All 21 comments

@mklement0,

tput reset # To make PSReadLine work properly again, notably up- and down-arrow.

What's up with this? What does tput reset do? Shouldn't terminal.gui do this?

I hope that could fix the #931 issue.

@tig:

  • tput reset resets the terminal to its default state.

  • Yes, such a call shouldn't be necessary - Terminal.Gui should restore the previous state - though I'm not 100% certain if PowerShell, specifically its PSReadLine module, is completely blameless here, as I've seen other strange behaviors after running programs that don't seem to surface in other shell.

Actually, resetting the terminal with tput init is better, because it doesn't clear the screen.(It is POSIX-mandated, though what it actually does is left to the implementation).

It is effective in preventing subsequent command-line editing problems on both macOS and Ubuntu, so it might indeed help in WSL too.

The problem only occurs _in-process_ in PowerShell and is _not_ related to the PSReadLine module, as it turns out (running Remove-Module PSReadLine first still exhibits the same up/down-arrow problems after).

Possibly related: there's a longstanding bug report that describes the opposite problem (up/down-arrow keys not working in a utility that displays an alternate screen): https://github.com/PowerShell/PowerShell/issues/7375

If you call the code via PowerShell's CLI - pwsh -noprofile -file script.ps1 - it works fine even without tput init, including when calling from, say, bash.

@mklement0 Do you know where I can find the source to tput?

I have a feeling this is relevant:

https://github.com/PowerShell/GraphicalTools/blob/6e8fb2f7f7bb2aec746e67225de2675132c8c601/src/Microsoft.PowerShell.ConsoleGuiTools/ConsoleGui.cs#L327

I thought I had removed this and moved it into gui.cs already.

        Console.Write("\u001b[?1h");

See https://github.com/migueldeicaza/gui.cs/issues/418

I can confirm that replacing tput init with "$([char] 0x1b)[?1h" (simpler PS Core alternative: "`e[?1h") in the sample code enough is sufficient.

Examining what escape sequences tput init emits in practice is elusive, because trying to capture them seems to change what is being emitted:

PS> (tput init) -replace '\e', '\u001b'
\u001b[!p\u001b[?3;4l\u001b[4l\u001b>

Even executing (tput init) (wrapping in parentheses) makes the command no longer effective; not only that, it actively _triggers_ the symptom, even if the arrow keys worked fine at the time.

I don't think the implementation of tput itself will tell us much, because it looks up the specific escape sequences to use in the terminfo "database" (a 2-level file hierarchy at /usr/share/terminfo/*/*) based on the predefined TERM environment variable, which indicate the terminal type being emulated. On macOS, that variable contains xterm-256, which results in file /usr/share/terminfo/78/xterm-256color getting consulted.

Based on man tput and man terminfo, tput init does the following:

init   If  the terminfo database is present and an entry for the user's
      terminal exists (see -Ttype, above), the following will occur:

      (1)    if present, the terminal's initialization strings will be
         output as detailed in the terminfo(5) section on Tabs and
         Initialization,

      (2)    any delays (e.g., newline) specified in the entry will be
         set in the tty driver,

      (3)    tabs  expansion will be turned on or off according to the
         specification in the entry, and

      (4)    if tabs are not  expanded,  standard  tabs  will  be  set
         (every 8 spaces).

      If  an  entry does not contain the information needed for any of
      the four  above  activities,  that  activity  will  silently  be
      skipped.

where terminfo(5) (man terminfo) states (identifiers such as is1, ... are terminal capabilities, listed in the same page):

      output is1 is2

      set the margins using
         mgc, smgl and smgr

      set tabs using
         tbc and hts

      print the file
         if

      and finally
         output is3.

The only sequence escape which worked to me, after testing many of them, was as shown in my PR #953.

On enter \x1b[?1049l
On exit \x1b[?1049h

The portable equivalent of VT100 escape sequence \x1b[?1h (which does work for macOS and Ubuntu terminals, however) appears to be tput rmkx; that is, the keypad_local aka rmkx TermInfo capability ("leave 'keyboard_transmit' mode") - the inverse is keypad_xmit aka smkx("enter 'keyboard_transmit' mode").

@BDisp, I get _similar_ escape sequences via tput, though the caveat is that I don't fully trust the captured versions:

PS> (tput rmkx) -replace '\e', '\x1b'; (tput smkx) -replace '\e', '\x1b'
\x1b[?1l\x1b>
\x1b[?1h\x1b=

@mklement0 that great, thanks. Work better than the other because the prompt is maintained sequential instead pushing to the bottom of the terminal. With the MacI don't have this hang on the terminal. It's only happens with the Linux.
What the difference in the end of the sequences?
>
=

@BDisp, for maximum robustness we shouldn't _hard-code_ these sequences, as they may vary by terminal emulation. - though perhaps, pragmatically speaking, that is fine nonetheless, given that terminal emulators today mostly seem to be Xterm-compatible (I don't know, however).

If we do have to make this terminal-agnostic: While we don't want to have to create an expensive tput child process, the question is whether ncurses allows us to query and submit the rmkx escape sequence _in-process_.

As for what > and = mean:

At the moment I'm not aware of a reverse Terminfo lookup, but the table of VT100 escape sequences at http://ascii-table.com/ansi-escape-sequences-vt-100.php - whose identifiers do NOT respond to the TermInfo capability names - suggest the following:

Esc= ... Set alternate keypad mode | DECKPAM
Esc> ... Set numeric keypad mode | DECKPNM

That is, they seem to act on the keys of the numeric keypad.

@mklement0 I ended up to use like https://github.com/migueldeicaza/gui.cs/issues/418#issuecomment-707648746

\x1b[?1h
\x1b[?1l

as documented at http://ascii-table.com/ansi-escape-sequences-vt-100.php

@mklement0 I'm experiencing another strange behavior with both escapes sequences on screen resizing as it's not refreshing well letting the screen all messed up. Do you know why this happens?
One clue is that the GUIdoes not receive notification of changes in relation to the screen size, always keeping the number of columns and rows unchanged.

@BDisp, I think that problem is not related to the escape sequences we've discussed per se:

Even without these sequences, I see the following behavior:

macOS:

  • compiled executable:

    • Terminal.app: OK

    • iTerm2.app: mostly OK while the app is running (there are aritifacts while resizing, but on releasing the mouse it redraws fine), but leaves remnants of the alternate screen after exiting

  • PowerShell script: broken in both Terminal.app and iTerm.app, though with different symptoms:

    • Terminal.app: broken (doesn't seem to be aware of the resizing)
    • iTerm.app: broken (doesn't seem to be aware of the resizing, artifacts while resizing)

Linux (Ubuntu 18.04):

  • compiled executable: OK

  • PowerShell script: broken (doesn't seem to be aware of the resizing)

So it seems that PowerShell is at least part of the problem - are you saying that with _compiled executables_ you only started seeing the problem after emitting the escape sequence on shutdown and therefore only in _subsequent_ runs?


Also, I've noticed that both terminal apps on macOS apparently don't draw the application on the alternate screen: you can actually scroll up while the application is running (using the terminal app's scroll bars) and see the previous terminal output.
Is this a known limitation?

Furthermore, the radio buttons and checkboxes in the example application aren't drawing, in both terminal apps.

iTerm2.app: mostly OK while the app is running (there are aritifacts while resizing, but on releasing the mouse it redraws fine), but leaves remnants of the alternate screen after exiting

I've noticed too. It's annoying.

So it seems that PowerShell is at least part of the problem - are you saying that with _compiled executables_ you only started seeing the problem after emitting the escape sequence on shutdown and therefore only in _subsequent_ runs?

I run with dotnet ./app.dll and I seeing the problem while the app is open and resizing the screen. I can exit and reopen again but the messed screen on resizing is worse than the terminal hang.

Also, I've noticed that both terminal apps on macOS apparently don't draw the application on the alternate screen: you can actually scroll up while the application is running (using the terminal app's scroll bars) and see the previous terminal output.
Is this a known limitation?

I noticed that too and is very annoying too on both Terminal and iTerm2.

Furthermore, the radio buttons and checkboxes in the example application aren't drawing, in both terminal apps.

Yes that true unfortunately. Mac does not handle very well with unicode. I tested with many configurations without success. I debugged into the code and the keys are sending correct but both the Terminal and iTerm2 don't process them. I think Mac is causing that.

I'm still a bit unclear: did emitting the escape sequence on shutdown make the resizing problem worse in your dotnet application on rerunning it in the same session?

Mac does not handle very well with unicode.

Ah, yes, it hadn't occurred to me that this was also the lack of Unicode support - I'll provide more feedback in #949.

The escape sequence works perfectly on shutdown but while running the app on the same session the screen does not redraws well. In WSL it's a messy screen and in linux the app does not resize as the screen size changes.

Perhaps if we take a step back and try to let ncurses handle restoring the previous terminal state - which is preferable anyway - the problem would go away:

On macOS, man curs_kernel covers ncurses functions related to saving and restoring the terminal state.

However, it suggests that pairing initscr() and endwin() performs that _automatically_, and I don't understand the relationship between def_prog_mode / def_shell_mode and reset_prog_mode / reset_shell_mode on the one hand, and savetty / resettty on the other:

   The following routines give low-level access to various curses capabilities.  
   Theses routines typically are used inside library routines.

   The def_prog_mode and def_shell_mode routines save the current terminal
   modes as the "program" (in curses) or "shell" (not in curses) state for
   use by the reset_prog_mode and reset_shell_mode routines.  This is done
   automatically  by initscr.  There is one such save area for each screen
   context allocated by newterm().

   The reset_prog_mode and reset_shell_mode routines restore the  terminal
   to  "program"  (in curses) or "shell" (out of curses) state.  These are
   done automatically by endwin and, after an endwin, by doupdate, so they
   normally are not called.

   The resetty and savetty routines save and restore the state of the ter-
   minal modes.  savetty saves the current state in a buffer  and  resetty
   restores the state to what it was at the last call to savetty.

I think that will be the way in the future to manage properly ncurses (save and restore previous state).
The strange is the GUI works with no problems before. I suspect about Net5.0 not working perfectly yet and also I'm using the preview 7 because I'm not using the preview version of Visual Studio.

To shed some more light on the original problem:

  • What breaks PowerShell is that the so-called application-cursor mode is not restored (which tput rmkx would do) when an _in-process_ (PowerShell-code-based) Terminal.Gui application exits.

    • Note that PowerShell is unusual in this respect, because the major POSIX-compatible shells do not turn this mode on for themselves, whereas PowerShell _requires it_. bash and zsh work properly whether the mode is set or not, whereas ksh is the _inverse_: it requires that the mode be OFF (see below).

    • By contrast, if a Terminal.Gui application is run _as an external application_ (child process), PowerShell compensates for Terminal.Gui not restoring the state by automatically turning application-cursor mode back on for itself.

  • By contrast, running a Terminal.Gui-based _.NET application_, invariably in a _child process_:

    • When run either with dotnet <DLL> or as a self-contained executable (dotnet publish -p:PublishSingleFile=true ...), application-cursor mode apparently _is_ properly restored to the pre-application-startup state on exit, _presumably thanks to ncurses_, because an application that just uses Console calls does _not_ restore the the mode.

      • So the question is: does ncurses only do this restoring _when the process exits_? This would explain the in-session problem.

    • The restoring that ncurses provides is apparently overridden when running _during development_ with dotnet run <project>.

In other words: The .NET Core runtime itself has the _inverse_ problem in that it _unconditionally_ leaves application-cursor mode ON on exiting (except if compensated for via ncurses):

PowerShell exhibits the same problematic behavior:

These misbehaviors rarely surface as a problem, at least in single-platform use, because bash and zsh handle cursor keys correctly _whether or not application-cursor mode is turned on or off_.

By contrast, ksh only works when application-cursor mode is OFF, so it surfaces there.

Unfortunately, I misspoke - please see the updated previous comment; in short: irrespective of whether the Terminal.Gui application is run in-process as PowerShell code or via an external executable, the application-cursor mode is unconditionally turned off. However, that is only problematic for PowerShell in the in-process case, because when it runs external applications it always turns that mode back on for itself.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

stevozilik picture stevozilik  路  8Comments

0x788 picture 0x788  路  3Comments

tig picture tig  路  7Comments

tig picture tig  路  4Comments

tig picture tig  路  3Comments