Skip to content

partial native Windows support via windows-curses #447

@asmith-kepler

Description

@asmith-kepler

Description

I have managed to get partial support for urwid on Windows 10 and Python 3 using the windows-curses package. A few things don't work, but it works well enough for my needs.

What works:

  • curses display
  • most widgets
  • most special keys
  • console resizing

What doesn't work:

  • unicode (including the default symbols for LineBox, etc.)
  • mouse events
  • high-colour modes (I don't think these are supported by windows-curses)

I don't know enough about the curses library or ANSI control sequences to debug further. I'm posting this here in the hopes that a) someone else finds it useful, and b) someone more knowledgeable can use it as a starting point to fix the remaining issues (especially unicode).

Patch

"""
Patch urwid on Windows and WSL.

On Windows, install the package "windows-curses".
"""
import curses
import platform
import re

import urwid
import urwid.curses_display
import urwid.raw_display

IS_WSL = ("Linux" in platform.platform()) and ("Microsoft" in platform.platform())
IS_WINDOWS = ("Windows" in platform.platform()) and ("Linux" not in platform.platform())

if IS_WSL:
    # Filter out SI/SO under WSL. See:
    # https://github.com/mitmproxy/mitmproxy/blob/8dfb8a9a7471e00b0f723de66d3b63bd089e9802/mitmproxy/tools/console/window.py#L316-L323
    wsl_patch_orig_write = urwid.raw_display.Screen.write
    wsl_patch_write_re = re.compile("[\x0e\x0f]")

    def wsl_patch_write(self, data):
        data = wsl_patch_write_re.sub("", data)
        return wsl_patch_orig_write(self, data)

    urwid.raw_display.Screen.write = wsl_patch_write

if IS_WINDOWS:
    def win_patch_tty_signal_keys(self, intr=None, quit=None, start=None, stop=None, susp=None, fileno=None):
        pass  # no signals on Windows; not needed for resize
    urwid.display_common.RealTerminal.tty_signal_keys = win_patch_tty_signal_keys

    win_patch_orig__start = urwid.curses_display.Screen._start
    def win_patch__start(self):
        win_patch_orig__start(self)
        # halfdelay() seems unnecessary and causes everything to slow down a lot.
        curses.nocbreak()  # exits halfdelay mode
        # keypad(1) is needed, or we get no special keys (cursor keys, etc.)
        self.s.keypad(1)
    urwid.curses_display.Screen._start = win_patch__start

    # The getch functions in urwid.curses_display really don't work well on Windows
    # for some reason. You end up with >1s delays on input. It looks like halfdelay()
    # might work differently under windows-curses (which wraps PDCurses), or possibly
    # not support delays and blocking/non-blocking modes exactly correctly. These rewrites
    # simplify urwid's implementation a lot: they ignore most of urwid's delay handling,
    # but they seem to work ok.
    def win_patch__getch(self, wait_tenths):
        if wait_tenths == 0:
            return self._getch_nodelay()

        self.s.nodelay(False)
        return self.s.getch()
    urwid.curses_display.Screen._getch = win_patch__getch

    def win_patch__getch_nodelay(self):
        self.s.nodelay(True)
        return self.s.getch()
    urwid.curses_display.Screen._getch_nodelay = win_patch__getch_nodelay

    # The Windows terminal uses a different colour numbering.
    def win_patch__setup_colour_pairs(self):
        if not self.has_color:
            return

        self.has_default_colors = False
        colour_correct = [
            curses.COLOR_BLACK,
            curses.COLOR_RED,
            curses.COLOR_GREEN,
            curses.COLOR_YELLOW,
            curses.COLOR_BLUE,
            curses.COLOR_MAGENTA,
            curses.COLOR_CYAN,
            curses.COLOR_WHITE,
        ]
        for fg in range(8):
            for bg in range(8):
                if fg == curses.COLOR_WHITE and \
                   bg == curses.COLOR_BLACK:
                    continue

                curses.init_pair(bg * 8 + 7 - fg, colour_correct[fg], colour_correct[bg])
    urwid.curses_display.Screen._setup_colour_pairs = win_patch__setup_colour_pairs

    # Default to curses on Windows
    win_patch_orig_main_loop___init__ = urwid.main_loop.MainLoop.__init__
    def win_patch_main_loop__init__(self, widget, palette=(), screen=None,
            handle_mouse=True, input_filter=None, unhandled_input=None,
            event_loop=None, pop_ups=False):
        if screen == None:
            screen = urwid.curses_display.Screen()
        win_patch_orig_main_loop___init__(self, widget, palette, screen,
            handle_mouse, input_filter, unhandled_input, event_loop, pop_ups)
    urwid.main_loop.MainLoop.__init__ = win_patch_main_loop__init__

    # Some special keys seem to have different key codes under Windows.
    urwid.escape._keyconv[351] = 'shift tab'
    urwid.escape._keyconv[358] = 'end'
    urwid.curses_display.KEY_RESIZE = 546

Usage

Save the above as urwid_win_patch.py. To use it:

import urwid
import urwid_win_patch

# use urwid normally

For convenience, the above patch also incorporates the change from issue 264 when using Windows Subsystem for Linux.

Tested With

  • Windows 10.0.18363.1256
  • python 3.6.5, 32-bit
  • windows-curses 2.2.0
  • urwid 2.1.2

Discussion

  • One obvious change was getting rid of signals. Windows doesn't support them and doesn't use them for windows resizing events; curses gives a RESIZEKEY code instead, which urwid already knows how to handle.
  • Curses delays and timeouts seem horribly broken. It's possible this is a bug in the curses library on Windows. (The windows-curses Python package is a wrapper around the PDcurses C library.) Whatever the cause, timeouts don't seem to work correctly, and halfdelay() mode in particular seemed to cause a lot of problems. I don't know enough about curses programming to understand fully the consequences/subtleties of what the original curses_display code was trying to do. What I have done is a a hack, but it seems to work.
  • The Windows console (or perhaps windows-curses) seems to enumerate the 8 standard colours in a different order. I've fixed that, above.
  • Some of the special keys seem to have different key codes under curses-windows. I have tested the following: arrow keys, escape, backspace, delete, tab/shift-tab, home, end, and function keys. I did not test with all possible combinations of shift/ctrl/meta, so it's entirely possible I've missed some. But those allow for basic navigation between widgets. If you find other missing/different key codes you care about, add them to the urwid.escape._keyconv dictionary, above.
  • I originally took a look at the raw_display module, which would have allowed high-colour modes. That would require a lot more work to port to Windows - probably a rewrite using msvcrt.
  • I'm not sure why unicode doesn't work: Python 3 and windows-curses do both support Unicode, and curses.addstr(u"string with unicode characters like é and ├─┤") does work with windows-curses, so it's possible this is an urwid bug (perhaps to do with default locales?), but that's speculation on my part.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FeatureFeature request/implementationWindowsWindows supportproposal

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions