haskell / haskeline

A Haskell library for line input in command-line programs.
https://hackage.haskell.org/package/haskeline
BSD 3-Clause "New" or "Revised" License
223 stars 75 forks source link

Consider using control sequences in the prompt on Windows too #130

Open judah opened 4 years ago

judah commented 4 years ago

Splitting out a comment from @mpilgrem, originally on #88:

The native consoles on Windows 10 are ANSI-capable, and there is no longer any version of Windows that has mainstream support by Microsoft that is not ANSI-capable, although ANSI-capability needs to be 'turned on' because of backwards compatibility with consoles on legacy Windows. I think GHCi and stack exec -- [command] turns it on.

A little knowledge is a dangerous thing, but when I disabled fixEsc in drawLineDiff (in module System.Console.Haskeline.Backend.Win32) as follows:

let fixEsc = id -- was: filter ((/= '\ESC') . baseChar)
in  drawLineDiffWin (fixEsc xs1, fixEsc ys1) (fixEsc xs2, fixEsc ys2)

then haskeline worked as expected. I tested it with:

module Main where

import System.Console.Haskeline

main :: IO ()
main = runInputT defaultSettings loop
 where
  prompt = "\ESC[34m\STXBlue\ESC[39m\STX\n\ESC[31m\STXRed\ESC[39m\STX: "
  loop :: InputT IO ()
  loop = do
    minput <- getInputLine prompt
    case minput of
      Nothing -> pure ()
      Just "quit" -> pure ()
      Just input -> do outputStrLn $ "Input was: " ++ input
                       loop
judah commented 4 years ago

This change seems reasonable to me. I would like to still be a little cautious about older versions, since GHC (which Haskeline depends on) still claims to support Windows Vista: https://www.haskell.org/ghc/download_ghc_8_8_1.html#windows64

But maybe we can just use something like CPP to check whether we're on a new enough version of Windows.

I found this documentation which suggests that we can enable it explicitly with ENABLE_VIRTUAL_TERMINAL_PROCESSING: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences?redirectedfrom=MSDN#output-sequences

It looks like GHC (the compiler binary, and ghci) enables that mode itself, but probably not for compiled code: https://github.com/ghc/ghc/blob/70e56b272492b65e41a149ec39a939e794fea66b/compiler/main/SysTools/Terminal.hs#L72 Were you testing a standalone binary or within ghci?

I don't have a version of Windows easily available at the moment, but please feel free to send a PR.

mpilgrem commented 4 years ago

I was testing within GHCi/stack exec --.

Understood on backwards capability - disabling the existing 'blanket' fixEsc does shift responsibility for handling legacy Windows sensibly to the code calling the haskeline library. Where it is the ultimate users who decide whether or not the prompt will contain any 'ANSI' control characters (as in the case of GHCi users and the :set prompt <prompt> command etc) I would not expect that to be problematic; a user on legacy Windows is unlikely to set the prompt to include 'ANSI' control characters.

I think there may be no simple CPP approach to distinguish legacy Windows from Windows 10; when I was looking at 'CPP-type solutions' for the ansi-terminal package, I did not find one.

mokus0 commented 4 years ago

it would be nice if this were at least an option I could enable - I have a haskeline app that uses ansi escapes for coloring, and it works fine on windows except for the prompt, which ends up mangled. I'm more than willing to opt-out of backward compatibility here.

judah commented 4 years ago

@mokus0 you should follow these instructions for control sequences in the prompt: https://github.com/judah/haskeline/wiki/ControlSequencesInPrompt That approach will make your application use control sequences on Unix-like OSes but omit them on Windows. (It also improves some logic around how line widths are calculated.)

mpilgrem commented 2 years ago

It has been about two and a half years since I proposed pull request #126. It is also about two and half years since Microsoft ceased mainstream support for the last mainstream-supported version of Windows that was not ANSI-capable (Windows 7, service pack 1 on 14 January 2020). Cygwin and, consequently, MSYS2 have announced that they will cease to support Windows Vista by the end of 2022 (Microsoft dropped its mainstream support for Vista on 11 April 2017). Perhaps now is a good time to place more weight on the needs of modern Windows users over those who are still on unsupported legacy versions?

aryairani commented 2 years ago

I think there may be no simple CPP approach to distinguish legacy Windows from Windows 10; when I was looking at 'CPP-type solutions' for the ansi-terminal package, I did not find one.

How about this? https://stackoverflow.com/a/55080889

import System.Win32.Info.Version -- from Win32 package

main :: IO ()
main = do
   osVersionInfo <- getVersionEx
   print (dwBuildNumber osVersionInfo)
mpilgrem commented 2 years ago

Thanks @aryairani, that looks like it may be a non-CPP solution. However, I think the underlying question remains: is it sensible for modern versions of haskeline to seek to continue to support operating systems when the vendor of the OS will no longer do so (not even for a fee).

aryairani commented 2 years ago

Maybe, maybe not; but if supporting the old OS is to get the consensus needed to support the new OS (the \STX approach is problematic for us, so I feel the new OS is not supported), then I'd say let's support both; and have a separate conversation about removing legacy windows support.

mpilgrem commented 2 years ago

Understood. When I have a moment, I'll revisit https://github.com/judah/haskeline/pull/126 accordingly.

mpilgrem commented 2 years ago

It turns out my optimisim at https://github.com/judah/haskeline/issues/130#issuecomment-1216788939 was unfounded. The underlying Win32 API GetVersionEx functions do not, in fact, return the version of Windows on modern Windows. See: https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexw. On my Windows 11 system, the System.Win32.Info.Version.getVersionEx returns:

OSVERSIONINFOEX {dwMajorVersion = 6, dwMinorVersion = 2, dwBuildNumber = 9200, dwPlatformId = 2, szCSDVersion = "", wServicePackMajor = 0, wServicePackMinor = 0, wSuiteMask = 256, wProductType = VerNTWorkStation}

which, as the documentation explains, is the Windows 8 OS version value (6.2).

aryairani commented 2 years ago

Doh, sorry. It looks like the functions we'd want now would be like IsWindows10OrGreater, but I don't see wrappers for them in the Win32 package yet.

mpilgrem commented 2 years ago

When I have another moment, I'll see if I can add IsWindowsVersionOrGreater to Win32. For 'ANSI' codes, you would need something more fine grained than IsWindows10OrGreater.

mpilgrem commented 2 years ago

Actually, IsWindowsVersionOrGreater is not fine-grained as all versions of Windows 10 are major 10, minor 0. I don't know if haskeline has the appetite to either depend upon, or copy, the complex approach that ansi-terminal takes to deciding whether a terminal still needs emulation on Windows.

mpilgrem commented 2 years ago

I have modified #126 to use the same logic as the ansi-terminal package to detect whether the Windows terminal is ANSI-capable or not. However, I cannot test on legacy Windows because I no longer have access to machines using it.