Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Windows #110

Merged
merged 3 commits into from Oct 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion blessed/__init__.py
Expand Up @@ -7,7 +7,10 @@
import platform as _platform

# local
from blessed.terminal import Terminal
if _platform.system() == 'Windows':
from blessed.win_terminal import Terminal
else:
from blessed.terminal import Terminal

if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'):
# Good till 3.2.10
Expand Down
8 changes: 7 additions & 1 deletion blessed/formatters.py
@@ -1,10 +1,16 @@
"""Sub-module providing sequence-formatting functions."""
# standard imports
import curses
import platform

# 3rd-party
import six

# curses
if platform.system() == 'Windows':
import jinxed as curses # pylint: disable=import-error
else:
import curses


def _make_colors():
"""
Expand Down
13 changes: 10 additions & 3 deletions blessed/keyboard.py
@@ -1,8 +1,7 @@
"""Sub-module providing 'keyboard awareness'."""

# std imports
import curses.has_key
import curses
import platform
import time
import re

Expand All @@ -18,6 +17,15 @@
# Unable to import 'ordereddict'
from ordereddict import OrderedDict

# curses
if platform.system() == 'Windows':
# pylint: disable=import-error
import jinxed as curses
from jinxed.has_key import _capability_names as capability_names
else:
import curses
from curses.has_key import _capability_names as capability_names


class Keystroke(six.text_type):
"""
Expand Down Expand Up @@ -170,7 +178,6 @@ def get_keyboard_sequences(term):
# of a kermit or avatar terminal, for example, remains unchanged
# in its byte sequence values even when represented by unicode.
#
capability_names = curses.has_key._capability_names
sequence_map = dict((
(seq.decode('latin1'), val)
for (seq, val) in (
Expand Down
45 changes: 27 additions & 18 deletions blessed/terminal.py
Expand Up @@ -5,33 +5,18 @@
import codecs
import collections
import contextlib
import curses
import functools
import io
import locale
import os
import platform
import select
import struct
import sys
import time
import warnings
import re

try:
import termios
import fcntl
import tty
HAS_TTY = True
except ImportError:
_TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
_MSG_NOSUPPORT = (
"One or more of the modules: 'termios', 'fcntl', and 'tty' "
"are not found on your platform '{0}'. The following methods "
"of Terminal are dummy/no-op unless a deriving class overrides "
"them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS)))
warnings.warn(_MSG_NOSUPPORT)
HAS_TTY = False

try:
InterruptedError
except NameError:
Expand Down Expand Up @@ -74,6 +59,26 @@
_time_left,
)

if platform.system() == 'Windows':
import jinxed as curses # pylint: disable=import-error
HAS_TTY = True
else:
import curses

try:
import termios
import fcntl
import tty
HAS_TTY = True
except ImportError:
_TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
_MSG_NOSUPPORT = (
"One or more of the modules: 'termios', 'fcntl', and 'tty' "
"are not found on your platform '{0}'. The following methods "
"of Terminal are dummy/no-op unless a deriving class overrides "
"them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS)))
warnings.warn(_MSG_NOSUPPORT)
HAS_TTY = False

_CUR_TERM = None # See comments at end of file

Expand Down Expand Up @@ -196,7 +201,11 @@ def __init__(self, kind=None, stream=None, force_styling=False):
# The descriptor to direct terminal initialization sequences to.
self._init_descriptor = (sys.__stdout__.fileno() if stream_fd is None
else stream_fd)
self._kind = kind or os.environ.get('TERM', 'unknown')

if platform.system() == 'Windows':
self._kind = kind or curses.get_term(self._init_descriptor)
else:
self._kind = kind or os.environ.get('TERM', 'unknown')

if self.does_styling:
# Initialize curses (call setupterm).
Expand Down Expand Up @@ -428,7 +437,7 @@ def _height_and_width(self):
try:
if fd is not None:
return self._winsize(fd)
except IOError:
except (IOError, OSError, ValueError):
pass

return WINSZ(ws_row=int(os.getenv('LINES', '25')),
Expand Down
3 changes: 1 addition & 2 deletions blessed/tests/test_keyboard.py
Expand Up @@ -748,7 +748,6 @@ def child(kind):

def test_get_keyboard_sequence(monkeypatch):
"Test keyboard.get_keyboard_sequence. "
import curses.has_key
import blessed.keyboard

(KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3)
Expand All @@ -765,7 +764,7 @@ def test_get_keyboard_sequence(monkeypatch):
lambda cap: {CAP_SMALL: SEQ_SMALL,
CAP_LARGE: SEQ_LARGE}[cap])

monkeypatch.setattr(curses.has_key, '_capability_names',
monkeypatch.setattr(blessed.keyboard, 'capability_names',
dict(((KEY_SMALL, CAP_SMALL,),
(KEY_LARGE, CAP_LARGE,))))

Expand Down
157 changes: 157 additions & 0 deletions blessed/win_terminal.py
@@ -0,0 +1,157 @@
# encoding: utf-8
"""Module containing Windows version of :class:`Terminal`."""

from __future__ import absolute_import

import contextlib
import msvcrt # pylint: disable=import-error
import time

import jinxed.win32 as win32 # pylint: disable=import-error

from .terminal import WINSZ, Terminal as _Terminal


class Terminal(_Terminal):
"""Windows subclass of :class:`Terminal`."""

def getch(self):
r"""
Read, decode, and return the next byte from the keyboard stream.

:rtype: unicode
:returns: a single unicode character, or ``u''`` if a multi-byte
sequence has not yet been fully received.

For versions of Windows 10.0.10586 and later, the console is expected
to be in ENABLE_VIRTUAL_TERMINAL_INPUT mode and the default method is
called.

For older versions of Windows, msvcrt.getwch() is used. If the received
character is ``\x00`` or ``\xe0``, the next character is
automatically retrieved.
"""
if win32.VTMODE_SUPPORTED:
return super(Terminal, self).getch()

rtn = msvcrt.getwch()
if rtn in ('\x00', '\xe0'):
rtn += msvcrt.getwch()
return rtn

def kbhit(self, timeout=None, **_kwargs):
"""
Return whether a keypress has been detected on the keyboard.

This method is used by :meth:`inkey` to determine if a byte may
be read using :meth:`getch` without blocking. This is implemented
by wrapping msvcrt.kbhit() in a timeout.

:arg float timeout: When ``timeout`` is 0, this call is
non-blocking, otherwise blocking indefinitely until keypress
is detected when None (default). When ``timeout`` is a
positive number, returns after ``timeout`` seconds have
elapsed (float).
:rtype: bool
:returns: True if a keypress is awaiting to be read on the keyboard
attached to this terminal.
"""
end = time.time() + (timeout or 0)
while True:

if msvcrt.kbhit():
return True

if timeout is not None and end < time.time():
break

return False

@staticmethod
def _winsize(fd):
"""
Return named tuple describing size of the terminal by ``fd``.

:arg int fd: file descriptor queries for its window size.
:rtype: WINSZ

WINSZ is a :class:`collections.namedtuple` instance, whose structure
directly maps to the return value of the :const:`termios.TIOCGWINSZ`
ioctl return value. The return parameters are:

- ``ws_row``: width of terminal by its number of character cells.
- ``ws_col``: height of terminal by its number of character cells.
- ``ws_xpixel``: width of terminal by pixels (not accurate).
- ``ws_ypixel``: height of terminal by pixels (not accurate).
"""
window = win32.get_terminal_size(fd)
return WINSZ(ws_row=window.lines, ws_col=window.columns,
ws_xpixel=0, ws_ypixel=0)

@contextlib.contextmanager
def cbreak(self):
"""
Allow each keystroke to be read immediately after it is pressed.

This is a context manager for ``jinxed.w32.setcbreak()``.

.. note:: You must explicitly print any user input you would like
displayed. If you provide any kind of editing, you must handle
backspace and other line-editing control functions in this mode
as well!

**Normally**, characters received from the keyboard cannot be read
by Python until the *Return* key is pressed. Also known as *cooked* or
*canonical input* mode, it allows the tty driver to provide
line-editing before shuttling the input to your program and is the
(implicit) default terminal mode set by most unix shells before
executing programs.
"""
if self._keyboard_fd is not None:

filehandle = msvcrt.get_osfhandle(self._keyboard_fd)

# Save current terminal mode:
save_mode = win32.get_console_mode(filehandle)
save_line_buffered = self._line_buffered
win32.setcbreak(filehandle)
try:
self._line_buffered = False
yield
finally:
win32.set_console_mode(filehandle, save_mode)
self._line_buffered = save_line_buffered

else:
yield

@contextlib.contextmanager
def raw(self):
"""
A context manager for ``jinxed.w32.setcbreak()``.

Although both :meth:`break` and :meth:`raw` modes allow each keystroke
to be read immediately after it is pressed, Raw mode disables
processing of input and output.

In cbreak mode, special input characters such as ``^C`` are
interpreted by the terminal driver and excluded from the stdin stream.
In raw mode these values are receive by the :meth:`inkey` method.
"""
if self._keyboard_fd is not None:

filehandle = msvcrt.get_osfhandle(self._keyboard_fd)

# Save current terminal mode:
save_mode = win32.get_console_mode(filehandle)
save_line_buffered = self._line_buffered
win32.setraw(filehandle)
try:
self._line_buffered = False
yield
finally:
win32.set_console_mode(filehandle, save_mode)
self._line_buffered = save_line_buffered

else:
yield
4 changes: 4 additions & 0 deletions requirements.txt
@@ -1,2 +1,6 @@
wcwidth>=0.1.4
six>=1.9.0
# support python2.6 by using backport of 'orderedict'
ordereddict==1.1; python_version < "2.7"
# Windows requires jinxed
jinxed>=0.5.4; platform_system == "Windows"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I hope you will do nicely and not break jinxed any day in the future :)

I don't typically care for these >=, anything could happen in the future, nefarious, even, but I'm a bit FOSS tired and feeling more trusting these days.

6 changes: 1 addition & 5 deletions setup.py
Expand Up @@ -5,14 +5,9 @@


def _get_install_requires(fname):
import sys
result = [req_line.strip() for req_line in open(fname)
if req_line.strip() and not req_line.startswith('#')]

# support python2.6 by using backport of 'orderedict'
if sys.version_info < (2, 7):
result.append('ordereddict==1.1')

return result


Expand Down Expand Up @@ -53,6 +48,7 @@ def _get_long_description(fname):
'Environment :: Console :: Curses',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
Expand Down