gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.35k stars 673 forks source link

Minimal PowerShell Sample #945

Open tig opened 3 years ago

tig commented 3 years ago
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()
tig commented 3 years ago

See this: https://stackoverflow.com/questions/64178641/how-to-add-an-event-action-handler-in-powershell/64232782#64232782

tig commented 3 years ago

@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?

BDisp commented 3 years ago

I hope that could fix the #931 issue.

mklement0 commented 3 years ago

@tig:

mklement0 commented 3 years ago

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.

tig commented 3 years ago

@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

mklement0 commented 3 years ago

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.
BDisp commented 3 years ago

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

mklement0 commented 3 years ago

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=
BDisp commented 3 years ago

@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? > =

mklement0 commented 3 years ago

@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.

BDisp commented 3 years ago

@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

BDisp commented 3 years ago

@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.

mklement0 commented 3 years ago

@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:

Linux (Ubuntu 18.04):

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.

BDisp commented 3 years ago

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.

mklement0 commented 3 years ago

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.

BDisp commented 3 years ago

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.

mklement0 commented 3 years ago

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.
BDisp commented 3 years ago

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.

mklement0 commented 3 years ago

To shed some more light on the original problem:

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.

mklement0 commented 3 years ago

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.

tig commented 1 year ago

Started thinking about this again.

'tis a bummer this is not fixed: https://github.com/PowerShell/PowerShell/issues/6724

ca0abinary commented 1 year ago

Thanks for a really nice example! There are others out there, but I like how cleanly this one is implemented.

ca0abinary commented 1 year ago

I wrote a slight modification of your example which installs the ConsoleGuiTools module to help make life easier.

using namespace Terminal.Gui

if (!$(Get-Module Microsoft.PowerShell.ConsoleGuiTools)) {
  Install-Module Microsoft.PowerShell.ConsoleGuiTools
}

Import-Module Microsoft.PowerShell.ConsoleGuiTools 
$module = (Get-Module Microsoft.PowerShell.ConsoleGuiTools -List).ModuleBase
Add-Type -Path (Join-path $module Terminal.Gui.dll)

[Application]::Init()

$topLevel = [TopLevel]@{}

$topLevel.Add([StatusBar]@{ 
  Visible = $true
  Items = @(
    [StatusItem]::new([int][Key]("CtrlMask") -bor [int][Key]("Q"), '~CTRL-Q~ Quit', {
      [Application]::RequestStop()
    })
  )
})

$dialog = [Dialog]@{
  X = [Pos]::Center()
  Y = [Pos]::Center()
  Title = 'Dialog'
}

$text = [TextField]@{
  X = [Pos]::Center()
  Y = [Pos]::Center()
  Width = 20
  Text = 'Howdy!'
}
$dialog.Add($text)

$quit = [Button]@{
  X = [Pos]::Center()
  Y = [Pos]::Center() + 1
  Text = 'Quit'
}
$quit.add_Clicked({ [Application]::RequestStop() })
$dialog.Add($quit)

$topLevel.Add($dialog)

[Application]::Run($topLevel)
[Application]::Shutdown()

Write-Output "Got ""$($text.Text.ToString())"" from user input"

edit 1: I struggled with status bar items and the Key class being not CLS compliant. Updated this example to show my workaround.

mklement0 commented 1 year ago

Thanks, @ca0abinary - that's the definitely the simplest option in v7.2+.

I took the liberty of integrating your Microsoft.PowerShell.ConsoleGuiTools workaround into this StackOverflow answer (which is where the code in the initial post came from), and I've streamlined it a bit (no need to actually import the module) - though your sample code here has more features.

I've also updated the answer, which was written pre-v1, to reflect the status quo.