# -*- coding: utf-8 -*-
"""Provides :class:`Screen`, which lets you write text to specific coordinates
in the dos command shell, with colors.
"""
# pylint:disable=R0201,R0902
# R0201: method could be a function
# R0902: too many instance attributes
import sys
import struct
import pprint
import threading
import colorama
colorama.init()
[docs]class ScreenInfo(object):
"""Information about the screen dimensions.
Calls SetConsoleMode and GetConsoleScreenBufferInfo on windows,
and tries various methods of getting the screen dimension on
non-windows platforms.
"""
def __init__(self, **kw):
self.width = kw.get('width', 0)
self.height = kw.get('height', 0)
self.x = kw.get('x', 0)
self.y = kw.get('y', 0)
self.left = kw.get('left', 0)
self.top = kw.get('top', 0)
self.right = kw.get('right', 0)
self.bottom = kw.get('bottom', 0)
self.maxx = kw.get('maxx', 0)
self.maxy = kw.get('maxy', 0)
self.xpos = kw.get('xpos', 0)
self.ypos = kw.get('ypos', 0)
if sys.platform == 'win32':
self.__set_from_screen_info_win32()
else:
self.__set_screen_info_nix()
def __get_screen_size_nix(self):
"""Try various methods of getting the screen size on *nixen.
From: http://stackoverflow.com/a/566752/75103
"""
import os
env = os.environ
def ioctl_GWINSZ(fd):
try:
import fcntl, termios, struct, os
cr = struct.unpack(
'hh',
fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')
)
except:
return
return cr
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_GWINSZ(fd)
os.close(fd)
except:
pass
if not cr:
cr = (env.get('LINES', 25), env.get('COLUMNS', 80))
return int(cr[1]), int(cr[0])
def __set_screen_info_nix(self):
cols, lines = self.__get_screen_size_nix()
self.width = cols - 1
self.height = lines - 1
self.right = self.maxx = self.width
self.bottom = self.maxy = self.height
def __set_from_screen_info_win32(self):
"""Call windows internals to get dimensions of dosbox.
typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
COORD dwSize;
COORD dwCursorPosition;
WORD wAttributes;
// A SMALL_RECT structure that contains the console screen buffer
// coordinates of the upper-left and lower-right corners of the
// display window.
SMALL_RECT srWindow;
// A COORD structure that contains the maximum size of the console
// window, in character columns and rows, given the current screen
// buffer size and font and the screen size.
COORD dwMaximumWindowSize;
} CONSOLE_SCREEN_BUFFER_INFO;
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
typedef struct _SMALL_RECT {
SHORT Left;
SHORT Top;
SHORT Right;
SHORT Bottom;
} SMALL_RECT;
"""
from ctypes import windll, create_string_buffer
# Disable line wrapping at bottom of terminal. Having it on means that
# anything written to the bottom-most/right-most spot causes the
# terminal to wrap -- that is most likely not what you want when
# doing absolute cursor positioning.
h = windll.kernel32.GetStdHandle(-11)
windll.kernel32.SetConsoleMode(h, 1)
h = windll.kernel32.GetStdHandle(-12)
csbi = create_string_buffer(22)
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
if not res:
raise RuntimeError("Error calling: GetConsoleScreenBufferInfo.")
vals = struct.unpack("hhhhHhhhhhh", csbi.raw)
self.width = vals[0]
self.height = vals[1]
self.x = vals[2]
self.y = vals[3]
self.left = vals[5]
self.top = vals[6]
self.right = vals[7]
self.bottom = vals[8]
self.maxx = vals[9]
self.maxy = vals[10]
screen_lock = threading.Lock()
[docs]class Window(object):
"""A window that will scroll text written to it.
The screen object is thread safe when used through Window objects.
"""
def __init__(self, screen, x, y, width, height):
# self.dbg = []
self.screen = screen
self.x = x
self.y = y
self.width = width
self.height = height
self.xpos, self.ypos = (0, 0)
self.content = ['' for _ in range(self.height)]
def __repr__(self):
t = self.__dict__.copy()
del t['content']
del t['screen']
# del t['dbg']
return "screen.Window(%r)" % t
def _paint_content(self):
self.cls()
with screen_lock:
self.screen.writelinesxy(
self.x, self.y, '\n'.join(self.content)
)
def _scroll_up(self, n=None):
if n is None:
n = self.height // 2
self.content = self.content[n:] + [''] * n
self._paint_content()
self.ypos -= n
def _write(self, txt):
if self.ypos >= self.height:
self._scroll_up()
self.content[self.ypos] = self.content[self.ypos][:self.xpos] + txt
self.writexy(self.xpos, self.ypos, txt)
self.xpos += len(txt)
[docs] def newline(self):
self.xpos = 0
self.ypos += 1
[docs] def writexy(self, x, y, txt):
"""Write to position x, y relative to the window.
"""
with screen_lock:
self.screen.writexy(
self.x + x,
self.y + y,
txt
)
[docs] def write(self, *args):
"""Write to current position in the window, scrolling
the contents as needed.
"""
txt = ' '.join(str(arg) for arg in args)
if txt == '\n':
self.newline()
else:
while txt:
avail_space = self.width - self.xpos
try:
nlpos = txt.index('\n')
if nlpos > avail_space:
raise ValueError()
rest_of_line = txt[:nlpos]
txt = txt[nlpos+1:]
except ValueError:
rest_of_line = txt[:avail_space]
txt = txt[avail_space:]
self._write(rest_of_line)
if txt:
self.newline()
[docs] def cls(self, color=None):
"""Clear window, fill it with the given color.
"""
args = {}
if color:
args['background'] = color
with screen_lock:
self.screen.fill(self.x, self.y, self.width, self.height, char=' ', **args)
[docs]class Screen(object):
"""Screen provides a interface for positioned writing, with color,
to the screen.
"""
colors = "black red green yellow blue magenta cyan white".split()
def __init__(self, screeninfo=None, **kw):
"""Default foreground and background colors can be specified as e.g.::
scr = Screen(fg='white', bg='black')
foreground color can be set with any of the following synonyms:
fg, foreground, color
background color can be set with any of the following synonyms:
bg, background, on
e.g.::
scr = Screen(color='red', on='black')
"""
s = screeninfo or ScreenInfo()
self.buffer_width = s.width
self.buffer_height = s.height
self.buffer_x = s.x
self.buffer_y = s.y
self.buffer_left = s.left
self.buffer_top = s.top
self.buffer_right = s.right
self.buffer_bottom = s.bottom
self.maxx = s.maxx
self.maxy = s.maxy
self.xpos = self.buffer_x - self.buffer_left + 1
self.ypos = self.buffer_y - self.buffer_top + 1
self.Fore = self.Back = ''
self.Fore, self.Back = self._get_colors(kw)
def _get_colors(self, kw):
"""Grab color synonyms from `kw`.
"""
kwkeys = set(kw.keys())
def getcolor(which, synonyms):
key = synonyms & kwkeys
if key:
fore_or_back = getattr(colorama, which)
return getattr(fore_or_back,
kw[key.pop()].upper(),
getattr(self, which))
return getattr(self, which)
fg = getcolor('Fore', {'foreground', 'color', 'fg'})
bg = getcolor('Back', {'background', 'on', 'bg'})
return fg, bg
[docs] def windows(self, xcount, ycount):
"""Returns a list of ``count`` symetrically created windows.
"""
wwidth = self.width // xcount
wheight = self.height // ycount
assert wwidth > 6 and wheight > 6
rows = []
for y in range(ycount):
cols = []
for x in range(xcount):
w = Window(self, x * wwidth, y * wheight, wwidth - 1, wheight - 1)
cols.append(w)
rows.append(cols)
return rows
@property
def left(self):
"""The first column of the screen (the visible part of the screen
buffer).
"""
return 0
@property
def top(self):
"""The first row of the screen.
"""
return 0
@property
def right(self):
"""The rightmost column of the screen.
"""
return self.width
@property
def bottom(self):
"""The last (bottom-most) row of the screen.
"""
return self.height - 1
@property
def center(self):
"""The horizontal center of the screen.
"""
return self.left + self.width // 2
@property
def middle(self):
"""The vertical middle of the screen.
"""
return self.top + self.height // 2
@property
def width(self):
"""The width of the visible portion of the screen buffer.
"""
return self.buffer_right - self.buffer_left + 1
@property
def height(self):
"""The height of the visible portion of the screen buffer.
"""
return self.buffer_bottom - self.buffer_top + 1
@property
def coords(self):
"""Mostly a convenience method for debugging.
"""
return dict(
left=self.left,
right=self.right,
top=self.top,
bottom=self.bottom,
center=self.center,
middle=self.middle,
width=self.width,
height=self.height,
)
def __repr__(self):
tmp = self.coords
tmp.update(self.__dict__)
return pprint.pformat(tmp, width=35)
def _xy(self, x, y):
"Position the cursor at x, y (where x, y are zero-based coordinates)."
return '\033[%d;%dH' % (y + 1, x + 1)
[docs] def gotoxy(self, x, y):
"""Put cursor at coordinates ``x``, ``y``.
"""
sys.stdout.write(self._xy(x, y) + '')
self.ypos = y
self.xpos = x
[docs] def writelinesxy(self, x, y, *args, **kw):
"""If the string resulting from prosessing `args` contains newlines,
then write the next line at x, y+1, etc.
"""
txt = ' '.join(str(a) for a in args)
lines = txt.split('\n')
for i, line in enumerate(lines):
self.writexy(x, y + i, line, **kw)
[docs] def write(self, *args, **kw):
"""Write args at current location, see writexy function for keyword
arguments.
"""
self.writexy(self.xpos, self.ypos, *args, **kw)
[docs] def writexy(self, x, y, *args, **kw):
"""Write args at position x, y.
Specify foreground and backround colors with keyword arguments.
Available colors:
- black
- red
- green
- yellow
- blue
- magenta
- cyan
- white
(Be aware that the color names can be mapped to entirely different
colors by e.g. changing values in the registry:
https://github.com/neilpa/cmd-colors-solarized)
"""
txt = ' '.join(str(a) for a in args)
fg, bg = self._get_colors(kw)
cmd = self._xy(x, y)
cmd += fg
cmd += bg
cmd += txt
cmd += colorama.Style.RESET_ALL # pylint:disable=E1101
sys.stdout.write(cmd)
self.ypos = y
self.xpos = x + len(txt)
[docs] def rightxy(self, x, y, *args, **kw):
"""Write text right justified at coordinates x, y.
The last character will be written at position
(x-1, y), which means that e.g.::
scr.rightxy(scr.right, scr.bottom, 'bottom right')
will be written flush in the bottom right corner, and::
scr.rightxy(scr.center, scr.middle, 'hello')
scr.writexy(scr.center, scr.middle, 'world')
will output `helloworld` (without a space) in the middle
of the screen.
"""
txt = ' '.join(str(a) for a in args)
self.writexy(x - len(txt), y, txt, **kw)
[docs] def centerxy(self, x, y, *args, **kw):
"""Write text centered around the x coordinate.
"""
txt = ' '.join(str(a) for a in args)
self.writexy(self.center - len(txt) // 2, y, txt, **kw)
[docs] def fill(self, x, y, width, height, char=' ', **kw): # pylint:disable=R0913
"""Fill rectangle with char, and leave the writing position at
the beginning of the rectangle (position x,y).
"""
for ypos in range(y, y + height):
self.writexy(x, ypos, char * width, **kw)
self.xpos = x
self.ypos = y
[docs] def cls(self, color=None):
"""Clear screen, fill it with the given color.
"""
args = {}
if color:
args['background'] = color
self.fill(0, 0, self.width, self.height, char=' ', **args)